From c95bb0633fd8a8b1c74a053dd4967fc7a9f14576 Mon Sep 17 00:00:00 2001 From: meowarex Date: Thu, 21 May 2026 00:38:27 +1000 Subject: [PATCH] Code Review for UI Rewrite --- .github/workflows/release.yml | 3 +- .../meowarex/rlmobile/ManagerApplication.kt | 10 +++- .../network/utils/CommitsPagingSource.kt | 5 +- .../patcher/steps/patch/SmaliPatchStep.kt | 12 ++++- .../rlmobile/patcher/util/ManifestPatcher.kt | 7 +-- .../rlmobile/ui/screens/home/HomeModel.kt | 1 + .../rlmobile/ui/screens/home/HomeScreen.kt | 47 +++++++++++++------ .../ui/screens/patchopts/PatchOptions.kt | 2 +- .../updatechecker/UpdateCheckWorker.kt | 4 +- Manager/app/src/main/res/values/strings.xml | 1 + 10 files changed, 66 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b57f5f..e8ad9a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,8 @@ jobs: fi if [[ "$tidal_src" == *.apkm ]]; then echo "Merging splits from $tidal_src via APKEditor" - curl -sLo /tmp/APKEditor.jar https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar + curl --fail --location --retry 3 --retry-delay 2 -sSo /tmp/APKEditor.jar \ + https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk echo "Merged tidal-stock.apk:" ls -la ./dist/tidal-stock.apk diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ManagerApplication.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ManagerApplication.kt index 5ca7c30..a190155 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ManagerApplication.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ManagerApplication.kt @@ -26,6 +26,7 @@ import com.meowarex.rlmobile.ui.screens.settings.SettingsModel import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel import com.meowarex.rlmobile.updatechecker.UpdateCheckWorker import kotlinx.coroutines.Dispatchers +import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import org.koin.core.module.dsl.* @@ -101,7 +102,12 @@ class ManagerApplication : Application() { .build() } - // Schedule periodic update check - UpdateCheckWorker.schedule(this) + // Schedule periodic update check only when the user has opted in, + // so the disabled state survives app restarts instead of being re-enqueued. + if (get().autoUpdateCheck) { + UpdateCheckWorker.schedule(this) + } else { + UpdateCheckWorker.cancel(this) + } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/network/utils/CommitsPagingSource.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/network/utils/CommitsPagingSource.kt index 2eec401..bd9c32b 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/network/utils/CommitsPagingSource.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/network/utils/CommitsPagingSource.kt @@ -10,7 +10,10 @@ class CommitsPagingSource( ) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? = - state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } + state.anchorPosition?.let { + val page = state.closestPageToPosition(it) ?: return null + page.prevKey?.plus(1) ?: page.nextKey?.minus(1) + } override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 0 diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt index 9892273..e323289 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt @@ -51,8 +51,16 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent { if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) { val relative = patchFile.removePrefix("extension/") val out = smaliDir.resolve(relative) - out.parentFile?.mkdirs() - out.writeBytes(zip.openEntry(patchFile)!!.read()) + // Guard against zip-slip: a crafted entry could otherwise escape smaliDir. + val baseCanonical = smaliDir.canonicalPath + File.separator + val outCanonical = out.canonicalPath + if (!outCanonical.startsWith(baseCanonical)) { + throw SecurityException("Zip entry escapes target directory: $patchFile") + } + val entry = zip.openEntry(patchFile) + ?: throw FileNotFoundException("Missing zip entry: $patchFile") + out.canonicalFile.parentFile?.mkdirs() + out.writeBytes(entry.read()) container.log("Extracted extension smali: $relative") continue } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt index dba3ccd..a727118 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt @@ -35,7 +35,8 @@ object ManifestPatcher { } } }) - val origPkg = originalPackage ?: "com.aspiro.tidal" + val origPkg = originalPackage + ?: throw IllegalStateException("Manifest has no package attribute; refusing to rewrite authorities/permissions without a known originalPackage") val reader = AxmlReader(manifestBytes) val writer = AxmlWriter() @@ -104,7 +105,7 @@ object ManifestPatcher { super.attr( ns, name, resourceId, type, when (name) { - "name" -> (value as String).replace(origPkg, packageName) + "name" -> (value as? String)?.replace(origPkg, packageName) ?: value else -> value } ) @@ -155,7 +156,7 @@ object ManifestPatcher { super.attr( ns, name, resourceId, type, if (name == "authorities") { - (value as String).replace(origPkg, packageName) + (value as? String)?.replace(origPkg, packageName) ?: value } else { value } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt index a10a1ce..228baa3 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt @@ -97,6 +97,7 @@ class HomeModel( fun openApp(packageName: String) { val launchIntent = application.packageManager.getLaunchIntentForPackage(packageName) if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) application.startActivity(launchIntent) } else { application.showToast(R.string.launch_app_fail) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt index fe83378..d565ab1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt @@ -12,10 +12,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.screen.Screen @@ -55,16 +60,28 @@ class HomeScreen : Screen, Parcelable { title = { Text(stringResource(R.string.navigation_home)) }, actions = { IconButton(onClick = { model.refresh() }) { - Icon(painterResource(R.drawable.ic_refresh), contentDescription = null) + Icon( + painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.navigation_refresh), + ) } IconButton(onClick = { navigator.push(AboutScreen()) }) { - Icon(painterResource(R.drawable.ic_info), contentDescription = null) + Icon( + painterResource(R.drawable.ic_info), + contentDescription = stringResource(R.string.navigation_about), + ) } IconButton(onClick = { navigator.push(LogsListScreen()) }) { - Icon(painterResource(R.drawable.ic_receipt), contentDescription = null) + Icon( + painterResource(R.drawable.ic_receipt), + contentDescription = stringResource(R.string.navigation_logs), + ) } IconButton(onClick = { navigator.push(SettingsScreen()) }) { - Icon(painterResource(R.drawable.ic_settings), contentDescription = null) + Icon( + painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings), + ) } }, ) @@ -117,17 +134,19 @@ private fun ColumnScope.HomeContent( val currentVersionName = install?.version?.let { "v${it.toString()}" } val latestVersionName = state.latestTidalVersionCode?.let { "build $it" } - if (install?.icon != null) { + val fallbackPainter = if (install?.icon == null) { + // R.mipmap.ic_launcher is an adaptive-icon XML on API 26+, which painterResource cannot decode. + val context = LocalContext.current + remember { + val drawable = ContextCompat.getDrawable(context, R.mipmap.ic_launcher) + drawable?.toBitmap()?.asImageBitmap()?.let(::BitmapPainter) + } + } else null + + val iconPainter = install?.icon ?: fallbackPainter + if (iconPainter != null) { Image( - painter = install.icon, - contentDescription = null, - modifier = Modifier - .size(60.dp) - .clip(CircleShape), - ) - } else { - Image( - painter = painterResource(R.mipmap.ic_launcher), + painter = iconPainter, contentDescription = null, modifier = Modifier .size(60.dp) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt index 624f9f0..9e5bf27 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt @@ -38,7 +38,7 @@ data class PatchOptions( companion object { val Default = PatchOptions( appName = "TIDAL", - packageName = "com.tidal.music", + packageName = "com.aspiro.tidal", debuggable = false, customInjector = null, customPatches = null, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/updatechecker/UpdateCheckWorker.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/updatechecker/UpdateCheckWorker.kt index a7b2886..cca1163 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/updatechecker/UpdateCheckWorker.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/updatechecker/UpdateCheckWorker.kt @@ -7,6 +7,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.util.Log import androidx.core.app.ActivityCompat @@ -14,7 +15,6 @@ import androidx.core.app.NotificationCompat import androidx.core.content.getSystemService import androidx.work.* import com.meowarex.rlmobile.BuildConfig -import com.meowarex.rlmobile.MainActivity import com.meowarex.rlmobile.R import com.meowarex.rlmobile.manager.PreferencesManager import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService @@ -81,7 +81,7 @@ class UpdateCheckWorker( val pendingIntent = PendingIntent.getActivity( applicationContext, 0, - Intent(applicationContext, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, + Intent(Intent.ACTION_VIEW, Uri.parse(releaseUrl)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index 6ceb37c..fb4a489 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -117,6 +117,7 @@ About Logs Settings + Refresh Lead Contributors