Compare commits

..
16 Commits
Author SHA1 Message Date
mat c9a712f692 im stupid 2024-01-27 13:21:46 -03:00
mat 6a0b3fc8bd we're stupid 2024-01-27 13:17:21 -03:00
alk 684fd99b83 add release as a build i think 2024-01-27 18:34:18 +03:00
qimiko fd83daa776 generate more specific warnings 2024-01-27 03:13:37 -07:00
qimiko 9a87716635 add fallback mode to log export 2024-01-27 02:30:36 -07:00
Chloe 31566ce64b Merge pull request #12 from dankmeme01/main
Add support for requesting permissions from native code
2024-01-26 15:24:48 -07:00
dankmeme01 d078879e7b change handling of failures 2024-01-26 23:04:33 +01:00
dankmeme01 5fa42b6ca5 remove unused imports 2024-01-26 22:35:28 +01:00
dankmeme01 e164b87436 make permission requesting use launchers and add callback 2024-01-26 22:35:08 +01:00
dankmeme01 433d6a0f46 add support for requesting permissions from mods or Geode 2024-01-26 21:30:30 +01:00
mat cf8b1edbe2 Merge pull request #11 from Fleeym/crashfix
Hopefully fix crashes when tabbing out and back in
2024-01-25 15:39:55 -03:00
Fleeym 500e00a9cc ask for more file permission stuff 2024-01-25 19:58:18 +02:00
Fleeym c3f80e9c74 fix opengl context being deleted after it no longer exists 2024-01-25 19:58:09 +02:00
Chloe 7eb06e175f Merge pull request #10 from Fleeym/flame-stuff
fix file utils, move them to a separate class
2024-01-24 17:20:52 -07:00
Chloe 141681be05 add license 2024-01-23 20:24:37 -07:00
Fleeym dcfab5e001 fix file utils, move them to a separate class 2024-01-24 04:45:09 +02:00
14 changed files with 554 additions and 259 deletions

No files matched your search

+15 -6
View File
@@ -7,6 +7,15 @@ on:
jobs: jobs:
build: build:
strategy:
matrix:
config:
- name: Release
lower: release
- name: Debug
lower: debug
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -27,7 +36,7 @@ jobs:
- name: Build project - name: Build project
run: | run: |
./gradlew app:assembleDebug \ ./gradlew app:assemble${{ matrix.config.name }} \
-Pandroid.injected.signing.store.file="$HOME/release.keystore" \ -Pandroid.injected.signing.store.file="$HOME/release.keystore" \
-Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \ -Pandroid.injected.signing.store.password="${{secrets.KEYSTORE_PASSWORD}}" \
-Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \ -Pandroid.injected.signing.key.alias="${{secrets.KEY_ALIAS}}" \
@@ -37,8 +46,8 @@ jobs:
id: rename_apk_universal id: rename_apk_universal
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-universal-debug.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-universal-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-$SHORT_SHA.apk OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
@@ -46,15 +55,15 @@ jobs:
id: rename_apk id: rename_apk
run: | run: |
SHORT_SHA=${GITHUB_SHA::7} SHORT_SHA=${GITHUB_SHA::7}
APK_PATH=${{github.workspace}}/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk APK_PATH=${{github.workspace}}/app/build/outputs/apk/${{ matrix.config.lower }}/app-armeabi-v7a-${{ matrix.config.lower }}.apk
OUT_PATH=${{github.workspace}}/geode-launcher-debug-android32-$SHORT_SHA.apk OUT_PATH=${{github.workspace}}/geode-launcher-${{ matrix.config.lower }}-android32-$SHORT_SHA.apk
mv $APK_PATH $OUT_PATH mv $APK_PATH $OUT_PATH
echo "path=$OUT_PATH" >> $GITHUB_OUTPUT echo "path=$OUT_PATH" >> $GITHUB_OUTPUT
- name: Upload development build - name: Upload development build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: geode-launcher-debug name: geode-launcher-${{ matrix.config.lower }}
path: | path: |
${{ steps.rename_apk_universal.outputs.path }} ${{ steps.rename_apk_universal.outputs.path }}
${{ steps.rename_apk.outputs.path }} ${{ steps.rename_apk.outputs.path }}
+23
View File
@@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "com.geode.launcher" applicationId "com.geode.launcher"
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode 7 versionCode 9
versionName "1.0.0" versionName "1.1.1"
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
+9 -2
View File
@@ -9,8 +9,15 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature <uses-feature
android:name="android.hardware.wifi" android:name="android.hardware.wifi"
@@ -65,8 +65,12 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e) val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e)
val errorMessage = if (abiMismatch) getString(R.string.load_failed_abi_error_description) else val is64bit = LaunchUtils.is64bit
getString(R.string.load_failed_link_error_description) val errorMessage = when {
abiMismatch && is64bit -> getString(R.string.load_failed_abi_error_need_32bit_description)
abiMismatch -> getString(R.string.load_failed_abi_error_need_64bit_description)
else -> getString(R.string.load_failed_link_error_description)
}
returnToMain( returnToMain(
getString(R.string.load_failed_link_error), getString(R.string.load_failed_link_error),
@@ -336,14 +340,19 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
override fun onRestart() {
super.onRestart()
mIsOnPause = false
BaseRobTopActivity.isPaused = false
if (!this.mIsRunning) {
resumeGame()
}
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
mIsOnPause = true mIsOnPause = true
BaseRobTopActivity.isPaused = true BaseRobTopActivity.isPaused = true
}
override fun onStop() {
super.onStop()
if (mIsRunning) { if (mIsRunning) {
pauseGame() pauseGame()
} }
@@ -252,9 +252,14 @@ fun UpdateWarning() {
Text(stringResource(R.string.launcher_unsupported_version_title)) Text(stringResource(R.string.launcher_unsupported_version_title))
}, },
text = { text = {
val message = if (gdVersionCode >= Constants.SUPPORTED_VERSION_CODE) {
R.string.launcher_unsupported_version_description
} else {
R.string.launcher_outdated_version_description
}
Text(stringResource( Text(stringResource(
R.string.launcher_unsupported_version_description, message, gdVersionString, Constants.SUPPORTED_VERSION_STRING
gdVersionString, Constants.SUPPORTED_VERSION_STRING
)) ))
}, },
confirmButton = { confirmButton = {
@@ -0,0 +1,36 @@
package com.geode.launcher.activityresult
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
open class GeodeOpenFileActivityResult : ActivityResultContract<GeodeOpenFileActivityResult.OpenFileParams, Uri?>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -0,0 +1,55 @@
package com.geode.launcher.activityresult
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
open class GeodeOpenFilesActivityResult : ActivityResultContract<GeodeOpenFilesActivityResult.OpenFileParams, List<@JvmSuppressWildcards Uri>>() {
class OpenFileParams(val extraMimes: Array<String>, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: OpenFileParams): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input.extraMimes)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.setType("*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: OpenFileParams
): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
// Gracefully stolen from ActivityResultContracts.Intent.getclipDataUris
val clipData = intent.takeIf {
resultCode == Activity.RESULT_OK
}?.clipData
val resultSet = LinkedHashSet<Uri>()
intent?.data?.let { data ->
resultSet.add(data)
}
if (clipData == null && resultSet.isEmpty()) {
return emptyList()
} else if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
if (uri != null) {
resultSet.add(uri)
}
}
}
return ArrayList(resultSet)
}
}
@@ -0,0 +1,34 @@
package com.geode.launcher.activityresult
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
open class GeodeSaveFileActivityResult : ActivityResultContract<GeodeSaveFileActivityResult.SaveFileParams, Uri?>() {
class SaveFileParams(val mimeType: String?, val defaultPath: Uri?) {}
override fun createIntent(context: Context, input: SaveFileParams): Intent {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setType(input.mimeType ?: "*/*")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (input.defaultPath != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.defaultPath)
}
}
return intent
}
final override fun getSynchronousResult(
context: Context,
input: SaveFileParams
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
@@ -13,7 +13,7 @@ import kotlinx.coroutines.yield
import okio.buffer import okio.buffer
import okio.source import okio.source
import java.io.EOFException import java.io.EOFException
import java.util.LinkedList import java.io.IOException
/** /**
* ViewModel to manage log output from logcat. * ViewModel to manage log output from logcat.
@@ -30,6 +30,8 @@ class LogViewModel: ViewModel() {
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
private var failedToLoad = false
private var logJob: Job? = null private var logJob: Job? = null
var filterCrashes = false var filterCrashes = false
@@ -43,14 +45,24 @@ class LogViewModel: ViewModel() {
} }
private suspend fun logOutput(): List<LogLine> { private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>() val logLines = ArrayList<LogLine>()
// -B = binary format, -d = dump logs
val logCommand = if (filterCrashes) "logcat -b crash -B -d" val logCommand = if (filterCrashes) "logcat -b crash -B -d"
else "logcat -B -d" else "logcat -B -d"
// -B = binary format, -d = dump logs val logProcess = try {
val logSource = Runtime.getRuntime().exec(logCommand) Runtime.getRuntime().exec(logCommand)
.inputStream.source().buffer() } catch (ioe: IOException) {
ioe.printStackTrace()
logLines += LogLine.showException(ioe)
failedToLoad = true
return logLines
}
val logSource = logProcess.inputStream.source().buffer()
try { try {
coroutineScope { coroutineScope {
@@ -71,6 +83,9 @@ class LogViewModel: ViewModel() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
logLines += LogLine.showException(e) logLines += LogLine.showException(e)
// technically it maybe didn't completely fail...
failedToLoad = true
} }
return logLines return logLines
@@ -88,10 +103,26 @@ class LogViewModel: ViewModel() {
_lineState.clear() _lineState.clear()
} }
fun getLogData(): String { private fun dumpLogcatText(): String {
return _lineState.joinToString("\n") { it.asSimpleString } val logCommand = if (filterCrashes) "logcat -b crash -d"
else "logcat -d"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
return ioe.stackTraceToString()
}
return logProcess.inputStream.bufferedReader().readText()
} }
fun getLogData(): String = if (failedToLoad) {
dumpLogcatText()
} else {
_lineState.joinToString("\n") { it.asSimpleString }
}
private fun loadLogs() { private fun loadLogs() {
_isLoading.value = true _isLoading.value = true
@@ -0,0 +1,196 @@
package com.geode.launcher.utils
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import java.io.File
class FileUtils {
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
// hello zmx i moved it
companion object {
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
return handleExternalStorageDocument(context, uri)
}
isDownloadsDocument(uri) -> {
return handleDownloadsDocument(context, uri)
}
isMediaDocument(uri) -> {
return handleMediaDocument(context, uri)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
// This happened with a directory
if (isExternalStorageDocument(uri)) {
return handleExternalStorageDocument(context, uri)
}
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
private fun handleMediaDocument(context: Context, uri: Uri): String? {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(
context,
contentUri,
selection,
selectionArgs
)
}
private fun handleDownloadsDocument(context: Context, uri: Uri): String? {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
private fun handleExternalStorageDocument(context: Context, uri: Uri): String {
val docId: String = if (DocumentsContract.isDocumentUri(context, uri)) {
DocumentsContract.getDocumentId(uri)
} else {
// please please please PLEASE let this be the only other case
DocumentsContract.getTreeDocumentId(uri)
}
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
private fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, null, null,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
} finally {
cursor?.close()
}
return null
}
private fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri, projection, selection, selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
val data = cursor.getString(index)
cursor.close()
return data
}
} catch (e: Exception) {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}
}
@@ -1,87 +1,95 @@
package com.geode.launcher.utils package com.geode.launcher.utils
import android.app.Activity import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.net.URI
import kotlin.system.exitProcess import kotlin.system.exitProcess
@Suppress("unused") @Suppress("unused")
object GeodeUtils { object GeodeUtils {
private lateinit var activity: WeakReference<AppCompatActivity> private lateinit var activity: WeakReference<AppCompatActivity>
private lateinit var fileResultLauncher: ActivityResultLauncher<Intent> private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
private lateinit var filesResultLauncher: ActivityResultLauncher<Intent> private lateinit var openDirectoryResultLauncher: ActivityResultLauncher<Uri?>
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
private lateinit var saveFileResultLauncher: ActivityResultLauncher<GeodeSaveFileActivityResult.SaveFileParams>
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
fun setContext(activity: AppCompatActivity) { fun setContext(activity: AppCompatActivity) {
this.activity = WeakReference(activity) this.activity = WeakReference(activity)
fileResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri ->
if (result.resultCode == Activity.RESULT_OK) { if (uri != null) {
val data = result.data val path = FileUtils.getRealPathFromURI(activity, uri)
if (data != null) { if (path != null) {
val uri = data.data selectFileCallback(path)
if (uri != null) { return@registerForActivityResult
val path = getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
}
}
} }
} }
failedCallback() failedCallback()
} }
filesResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> openDirectoryResultLauncher = activity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (result.resultCode == Activity.RESULT_OK) { if (it != null) {
val data = result.data val path = FileUtils.getRealPathFromURI(activity, it)
if (data != null) { if (path != null)
val clipData = data.clipData selectFileCallback(path)
if (clipData != null) { return@registerForActivityResult
val paths = Array(clipData.itemCount) { "" } }
for (i in 0 until clipData.itemCount) { failedCallback()
val path = getRealPathFromURI(activity, clipData.getItemAt(i).uri) }
if (path != null) { openFilesResultLauncher = activity.registerForActivityResult(GeodeOpenFilesActivityResult()) { result ->
paths[i] = path if (result.isEmpty()) {
} failedCallback()
} return@registerForActivityResult
selectFilesCallback(paths) }
return@registerForActivityResult val paths: Array<String> = Array(result.size) {"n = $it"}
} for (i in result.indices) {
val path = FileUtils.getRealPathFromURI(activity, result[i])
if (path != null) {
paths[i] = path
}
}
selectFilesCallback(paths)
return@registerForActivityResult
}
saveFileResultLauncher = activity.registerForActivityResult(GeodeSaveFileActivityResult()) { uri ->
if (uri != null) {
val path = FileUtils.getRealPathFromURI(activity, uri)
if (path != null) {
selectFileCallback(path)
return@registerForActivityResult
} }
} }
failedCallback() failedCallback()
} }
requestPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
permissionCallback(isGranted)
}
} }
@JvmStatic @JvmStatic
fun getLogcatCrashBuffer(): String { fun getLogcatCrashBuffer(): String {
try { return try {
val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d") val logcatProcess = Runtime.getRuntime().exec("logcat -v brief -b crash -d")
val logcatOutput = logcatProcess.inputStream.bufferedReader().readText() logcatProcess.inputStream.bufferedReader().readText()
} catch (e: Exception) {
return logcatOutput
}
catch (e: Exception) {
Log.e("Geode", "Failed to get logcat crash buffer", e) Log.e("Geode", "Failed to get logcat crash buffer", e)
return "" ""
} }
} }
@@ -111,18 +119,19 @@ object GeodeUtils {
activity.get()?.run { activity.get()?.run {
packageManager.getLaunchIntentForPackage(packageName)?.also { packageManager.getLaunchIntentForPackage(packageName)?.also {
val mainIntent = Intent.makeRestartActivityTask(it.component) val mainIntent = Intent.makeRestartActivityTask(it.component)
mainIntent.putExtra("restarted", true); mainIntent.putExtra("restarted", true)
startActivity(mainIntent) startActivity(mainIntent)
exitProcess(0) exitProcess(0)
} }
} }
} }
// TODO As of now this is unused
@JvmStatic @JvmStatic
fun openFolder(path: String): Boolean { fun openFolder(path: String): Boolean {
activity.get()?.run { activity.get()?.run {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri)
@@ -137,28 +146,27 @@ object GeodeUtils {
return false return false
} }
@Suppress("KotlinJniMissingFunction")
external fun selectFileCallback(path: String) external fun selectFileCallback(path: String)
@Suppress("KotlinJniMissingFunction")
external fun selectFilesCallback(paths: Array<String>) external fun selectFilesCallback(paths: Array<String>)
@Suppress("KotlinJniMissingFunction")
external fun failedCallback() external fun failedCallback()
@JvmStatic @JvmStatic
fun selectFile(path: String): Boolean { fun selectFile(path: String): Boolean {
activity.get()?.run { activity.get()?.run {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) var uri: Uri?
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.setType("*/*") try {
openFileResultLauncher.launch(GeodeOpenFileActivityResult.OpenFileParams(arrayOf("*/*"), uri))
if (intent.resolveActivity(packageManager) != null) {
print("Geode Selectfile")
fileResultLauncher.launch(intent)
print("Geode launched ")
return true return true
} catch (e: ActivityNotFoundException) {
return false
} }
} }
return false return false
@@ -167,18 +175,15 @@ object GeodeUtils {
@JvmStatic @JvmStatic
fun selectFiles(path: String): Boolean { fun selectFiles(path: String): Boolean {
activity.get()?.run { activity.get()?.run {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) var uri: Uri?
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) try {
intent.setType("*/*") openFilesResultLauncher.launch(GeodeOpenFilesActivityResult.OpenFileParams(arrayOf("*/*"), uri))
if (intent.resolveActivity(packageManager) != null) {
filesResultLauncher.launch(intent)
return true return true
} catch (e: ActivityNotFoundException) {
return false
} }
} }
return false return false
@@ -187,17 +192,15 @@ object GeodeUtils {
@JvmStatic @JvmStatic
fun selectFolder(path: String): Boolean { fun selectFolder(path: String): Boolean {
activity.get()?.run { activity.get()?.run {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) var uri: Uri?
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.setType("*/*") try {
openDirectoryResultLauncher.launch(uri)
if (intent.resolveActivity(packageManager) != null) {
fileResultLauncher.launch(intent)
return true return true
} catch (e: ActivityNotFoundException) {
return false
} }
} }
return false return false
@@ -206,17 +209,16 @@ object GeodeUtils {
@JvmStatic @JvmStatic
fun createFile(path: String): Boolean { fun createFile(path: String): Boolean {
activity.get()?.run { activity.get()?.run {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) var uri: Uri?
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_OPENABLE)
DocumentFile.fromFile(File(path)).also { DocumentFile.fromFile(File(path)).also {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it.uri) uri = it.uri
} }
intent.setType("*/*")
if (intent.resolveActivity(packageManager) != null) { try {
fileResultLauncher.launch(intent) saveFileResultLauncher.launch(GeodeSaveFileActivityResult.SaveFileParams(null, uri))
return true return true
} catch (e: ActivityNotFoundException) {
return false
} }
} }
return false return false
@@ -250,155 +252,38 @@ object GeodeUtils {
return "" return ""
} }
// copied from https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
// i am actually very lazy to move this to a separate class
fun getRealPathFromURI(context: Context, uri: Uri): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(context, uri) -> {
when {
// ExternalStorageProvider
isExternalStorageDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
isDownloadsDocument(uri) -> {
val fileName = getFilePath(context, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
}
isMediaDocument(uri) -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
// TODO: add the geode provider
}
}
"content".equals(uri.scheme, ignoreCase = true) -> {
// Return the remote address
if (isGooglePhotosUri(uri)) return uri.lastPathSegment
return getDataColumn(context, uri, null, null)
}
"file".equals(uri.scheme, ignoreCase = true) -> {
return uri.path
}
}
return null
}
fun getDataColumn(context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME
)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, null, null,
null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
fun isGeodeUri(uri: Uri): Boolean { fun isGeodeUri(uri: Uri): Boolean {
return "com.geode.launcher.user" == uri.authority return "com.geode.launcher.user" == uri.authority
} }
@JvmStatic
fun getPermissionStatus(permission: String): Boolean {
return ContextCompat.checkSelfPermission(
activity.get()?.applicationContext ?: return false,
permission
) == PackageManager.PERMISSION_GRANTED
}
@JvmStatic
fun requestPermission(permission: String) {
if (getPermissionStatus(permission)) {
permissionCallback(true)
return
}
activity.get()?.run {
try {
requestPermissionLauncher.launch(permission)
} catch (e: ActivityNotFoundException) {
permissionCallback(false)
}
return
}
permissionCallback(false)
}
@Suppress("KotlinJniMissingFunction")
external fun permissionCallback(granted: Boolean)
} }
@@ -89,8 +89,9 @@ object LaunchUtils {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val applicationArchitecture: String = Build.CPU_ABI val applicationArchitecture: String = Build.CPU_ABI
val platformName: String = if (applicationArchitecture == "arm64-v8a") val is64bit = applicationArchitecture == "arm64-v8a"
"android64" else "android32"
val platformName: String = if (is64bit) "android64" else "android32"
val geodeFilename: String = "Geode.$platformName.so" val geodeFilename: String = "Geode.$platformName.so"
+6 -2
View File
@@ -22,8 +22,11 @@
<string name="launcher_warning_icon_alt">warning</string> <string name="launcher_warning_icon_alt">warning</string>
<string name="launcher_error_icon_alt">error</string> <string name="launcher_error_icon_alt">error</string>
<string name="launcher_unsupported_version_title">Unsupported version</string> <string name="launcher_unsupported_version_title">Unsupported version</string>
<string name="launcher_outdated_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). You must update Geometry Dash to fix this.\nLaunch will continue, but crashes may occur!
</string>
<string name="launcher_unsupported_version_description"> <string name="launcher_unsupported_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). A launcher update is required to fix this.\nLaunch will continue, but crashes may occur! The current version of Geometry Dash is not compatible with the version supported by the launcher. A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
</string> </string>
<string name="launcher_update_available">A new launcher update is available!</string> <string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string> <string name="launcher_update_dismiss">Dismiss</string>
@@ -44,7 +47,8 @@
<!-- GeometryDashActivity strings --> <!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string> <string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string> <string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
<string name="load_failed_abi_error_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the \"android32\" version of the launcher to resolve this issue.\nCheck application logs for more details.</string> <string name="load_failed_abi_error_need_64bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 64-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
<string name="load_failed_abi_error_need_32bit_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the 32-bit version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
<string name="load_failed_generic_error">Load failed</string> <string name="load_failed_generic_error">Load failed</string>
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string> <string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>