diff --git a/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt b/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt index 79a4c2f..bdf00c8 100644 --- a/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt +++ b/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt @@ -9,13 +9,28 @@ import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.NetworkCapabilities +import android.net.Uri import android.os.Build +import android.provider.DocumentsContract +import android.provider.OpenableColumns import android.provider.Settings import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri +import com.customRobTop.contracts.ImportFileActivityContract +import com.customRobTop.contracts.ShareFileActivityContract import com.geode.launcher.R +import com.geode.launcher.utils.DownloadUtils +import com.geode.launcher.utils.LaunchUtils +import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.OutputStream import java.lang.ref.WeakReference import java.util.UUID @@ -24,6 +39,7 @@ object BaseRobTopActivity { const val KEYBOARD_INPUT_MULTILINE = 1 const val KEYBOARD_INPUT_NUMERIC = 4 const val KEYBOARD_INPUT_PASSWORD = 2 + const val LEVEL_EXTENSION: String = ".gdlvl" private var isLoaded = false var blockBackButton = false @@ -35,8 +51,57 @@ object BaseRobTopActivity { lateinit var me: WeakReference private var shouldResumeSound = true - fun setCurrentActivity(currentActivity: Activity) { + private var pendingExportPaths = emptyList() + + private lateinit var importFileActivityContract: ActivityResultLauncher + private lateinit var shareFileActivityContract: ActivityResultLauncher + private lateinit var shareFolderActivityContract: ActivityResultLauncher + + fun setCurrentActivity(currentActivity: AppCompatActivity) { me = WeakReference(currentActivity) + + importFileActivityContract = currentActivity.registerForActivityResult(ImportFileActivityContract()) { uris -> + val filePaths = copyPickedFiles(uris) + + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.filesPicked(filePaths.toTypedArray()) + } + } + + shareFileActivityContract = currentActivity.registerForActivityResult(ShareFileActivityContract()) { uri -> + val currentlyPending = pendingExportPaths + pendingExportPaths = emptyList() + + val exportPath = currentlyPending.firstOrNull() + if (exportPath == null || uri == null) { + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + return@registerForActivityResult + } + + val result = saveToDocument(exportPath, uri) + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(result) + } + } + + shareFolderActivityContract = currentActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + val currentlyPending = pendingExportPaths + pendingExportPaths = emptyList() + + if (uri == null) { + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + return@registerForActivityResult + } + + val result = saveToFolder(currentlyPending, uri) > 0 + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(result) + } + } } @SuppressLint("HardwareIds") @@ -289,4 +354,159 @@ object BaseRobTopActivity { fun setKeyboardInputFlags(flags: Int) { curKeyboardInputFlags = flags } + + @JvmStatic + fun pickImportFiles() { + try { + importFileActivityContract.launch(Unit) + } catch (_: ActivityNotFoundException) { + me.get()?.apply { + runOnUiThread { + Toast.makeText( + this, + R.string.no_activity_found, + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + + private fun copyPickedFiles(uris: List): List { + val me = me.get() ?: return emptyList() + + val importsDir = File(LaunchUtils.getSaveDirectory(me), "imports") + importsDir.mkdirs() + + return uris.mapIndexedNotNull { index, uri -> + val displayName = displayName(uri) + ?: "import_$index$LEVEL_EXTENSION" + if (!displayName.endsWith(LEVEL_EXTENSION)) { + return@mapIndexedNotNull null + } + + var outputFile = File(importsDir, displayName) + var counter = 2 + while (outputFile.exists()) { + outputFile = File(importsDir, "${counter}_$displayName") + counter++ + } + + try { + me.contentResolver.openInputStream(uri)?.use { input -> + outputFile.outputStream().use { output -> + DownloadUtils.copyFile(input, output) + } + + outputFile.absolutePath + } + } catch (_: Exception) { + outputFile.delete() + null + } + } + } + + private fun displayName(uri: Uri): String? { + me.get()?.contentResolver?.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursorQuery -> + if (cursorQuery.moveToFirst()) { + val displayName = cursorQuery.getString(0) + if (displayName.isNotEmpty()) { + return File(displayName).name + } + } + } + + return null + } + + @JvmStatic + fun shareFiles(files: Array) { + pendingExportPaths = files.toList() + + try { + if (files.size == 1) { + shareFileActivityContract.launch(files.first()) + return + } else if (files.size > 1) { + shareFolderActivityContract.launch(null) + return + } + } catch (_: ActivityNotFoundException) { + me.get()?.apply { + runOnUiThread { + Toast.makeText( + this, + R.string.no_activity_found, + Toast.LENGTH_SHORT, + ).show() + } + } + + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + } + + } + + private fun saveToFolder(exportPaths: List, treeUri: Uri): Int { + val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) + + val me = me.get() ?: return 0 + + return exportPaths.count { path -> + val filename = File(path).name + val documentUri = DocumentsContract.createDocument(me.contentResolver, uri, "application/octet-stream", filename) + ?: return@count false + + saveToDocument(path, documentUri) + } + } + + private fun saveToDocument(exportPath: String, uri: Uri): Boolean { + try { + FileInputStream(exportPath).use { inputStream -> + openOutput(uri).use { outputStream -> + if (outputStream != null) { + DownloadUtils.copyFile(inputStream, outputStream) + } + } + } + + ensureLevelExtension(uri) + return true + } catch (_: IOException) { + return false + } + } + + private fun openOutput(uri: Uri): OutputStream? { + val me = me.get() ?: return null + return try { + me.contentResolver.openOutputStream(uri, "wt") + } catch (_: IOException) { + me.contentResolver.openOutputStream(uri) + } + } + + private fun ensureLevelExtension(uri: Uri) { + val displayName = displayName(uri) + if (displayName == null || displayName.endsWith(LEVEL_EXTENSION)) { + return + } + val me = me.get() ?: return + + val extensionIdx = displayName.lastIndexOf(".gdlvl.") + val substring = if (extensionIdx < 0) { + displayName + LEVEL_EXTENSION + } else { + displayName.substring(0, extensionIdx + 6) + } + + try { + DocumentsContract.renameDocument(me.contentResolver, uri, substring) + } catch (_: IOException) { + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/JniToCpp.kt b/app/src/main/java/com/customRobTop/JniToCpp.kt index f952b1d..4f547b1 100644 --- a/app/src/main/java/com/customRobTop/JniToCpp.kt +++ b/app/src/main/java/com/customRobTop/JniToCpp.kt @@ -55,4 +55,16 @@ object JniToCpp { @JvmStatic external fun videoAdShowed() + + @JvmStatic + external fun filesPicked(files: Array) + + @JvmStatic + external fun fileShareFinished(success: Boolean) + + @JvmStatic + external fun textInputCaretToEnd() + + @JvmStatic + external fun textInputDone() } \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt b/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt new file mode 100644 index 0000000..7fed2b5 --- /dev/null +++ b/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt @@ -0,0 +1,40 @@ +package com.customRobTop.contracts + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +class ImportFileActivityContract : ActivityResultContract>() { + override fun createIntent( + context: Context, + input: Unit + ): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): List { + if (resultCode != Activity.RESULT_OK || intent == null) { + return emptyList() + } + + val clipData = intent.clipData + return if (clipData != null) { + List(clipData.itemCount) { i -> clipData.getItemAt(i).uri } + } else { + val intentData = intent.data + if (intentData != null) + listOf(intentData) + else + emptyList() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt b/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt new file mode 100644 index 0000000..ca69eb9 --- /dev/null +++ b/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt @@ -0,0 +1,40 @@ +package com.customRobTop.contracts + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import androidx.activity.result.contract.ActivityResultContract +import java.io.File + +class ShareFileActivityContract : ActivityResultContract() { + override fun createIntent( + context: Context, + input: String + ): Intent { + return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/octet-stream" + putExtra(Intent.EXTRA_TITLE, File(input).name) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, + DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Download") + ) + } + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): Uri? { + if (resultCode != Activity.RESULT_OK || intent == null) { + return null + } + + return intent.data + } +} \ No newline at end of file