Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ plugins {
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.rikka.tools.refine)
}

android {
Expand Down Expand Up @@ -282,6 +283,19 @@ dependencies {
implementation(libs.protobuf.javalite)
implementation(libs.protobuf.kotlin.lite)

// Shizuku for privileged installation
compileOnly(libs.rikka.hidden.stub)
implementation(libs.rikka.tools.refine.runtime)
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.lsposed.hiddenapibypass)

// libsu for root access
implementation(libs.libsu.core)

// Dhizuku for device owner installation
implementation(libs.dhizuku.api)
Comment on lines +286 to +297
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm izzy flavor disables updater
rg -n 'create\("izzy"\)|UPDATER_AVAILABLE' app/build.gradle.kts -C2

# Confirm updater deps are global in current file
rg -n 'compileOnly\(libs\.rikka\.hidden\.stub\)|implementation\(libs\.(rikka\.tools\.refine\.runtime|shizuku\.api|shizuku\.provider|lsposed\.hiddenapibypass|libsu\.core|dhizuku\.api)\)' app/build.gradle.kts -C2

# Check whether updater-specific APIs are imported from src/main (if yes, source-set split/no-op abstraction is needed)
rg -n --type=kt 'import (dev\.rikka|rikka\.shizuku|moe\.shizuku|com\.topjohnwu\.superuser|com\.rosan\.dhizuku|org\.lsposed\.hiddenapibypass)' app/src/main/kotlin -C1

Repository: MetrolistGroup/Metrolist

Length of output: 1343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Retry import check without the -type flag that caused error
echo "=== Checking for updater-specific API imports ==="
rg 'import (dev\.rikka|rikka\.shizuku|moe\.shizuku|com\.topjohnwu\.superuser|com\.rosan\.dhizuku|org\.lsposed\.hiddenapibypass)' app/src/main/ -C1 || echo "No updater API imports found in src/main"

# Check for existing flavor-scoped dependency examples in build.gradle.kts
echo -e "\n=== Checking for existing flavor-scoped dependency examples ==="
rg '"(foss|gms|izzy)(Implementation|CompileOnly|RuntimeOnly)"' app/build.gradle.kts -C1 | head -50

# Verify the three flavors are the complete list
echo -e "\n=== Confirm all product flavors in the file ==="
rg 'flavorDimensions|create\(' app/build.gradle.kts | grep -E '(flavorDimensions|create\()'

Repository: MetrolistGroup/Metrolist

Length of output: 2551


Scope updater installer dependencies to updater-enabled flavors only.

Lines 287-297 add privileged updater dependencies unconditionally, but the izzy flavor at Line 74 sets UPDATER_AVAILABLE = false. This causes izzy builds to unnecessarily package updater libraries (Dhizuku, Shizuku, libsu, HiddenApiBypass, Refine) despite disabling the updater feature. Updater-specific APIs are actively imported in src/main/kotlin/com/metrolist/music/utils/updater/AppInstaller.kt and UpdaterSettings.kt, so these dependencies have real usage. The codebase already uses flavor-scoped dependencies for similar cases (see gmsImplementation for Google Cast libs), establishing a precedent for this pattern.

Suggested fix
-    // Shizuku for privileged installation
-    compileOnly(libs.rikka.hidden.stub)
-    implementation(libs.rikka.tools.refine.runtime)
-    implementation(libs.shizuku.api)
-    implementation(libs.shizuku.provider)
-    implementation(libs.lsposed.hiddenapibypass)
-
-    // libsu for root access
-    implementation(libs.libsu.core)
-
-    // Dhizuku for device owner installation
-    implementation(libs.dhizuku.api)
+    // Updater stack only for updater-enabled flavors
+    "fossCompileOnly"(libs.rikka.hidden.stub)
+    "gmsCompileOnly"(libs.rikka.hidden.stub)
+
+    "fossImplementation"(libs.rikka.tools.refine.runtime)
+    "gmsImplementation"(libs.rikka.tools.refine.runtime)
+    "fossImplementation"(libs.shizuku.api)
+    "gmsImplementation"(libs.shizuku.api)
+    "fossImplementation"(libs.shizuku.provider)
+    "gmsImplementation"(libs.shizuku.provider)
+    "fossImplementation"(libs.lsposed.hiddenapibypass)
+    "gmsImplementation"(libs.lsposed.hiddenapibypass)
+
+    "fossImplementation"(libs.libsu.core)
+    "gmsImplementation"(libs.libsu.core)
+
+    "fossImplementation"(libs.dhizuku.api)
+    "gmsImplementation"(libs.dhizuku.api)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 286 - 297, The updater-related libs
(libs.dhizuku.api, libs.shizuku.api, libs.libsu.core,
libs.rikka.tools.refine.runtime, libs.lsposed.hiddenapibypass, etc.) are
currently added unconditionally; move them into flavor-scoped dependency
configurations for the updater-enabled flavors so the izzy flavor (with
UPDATER_AVAILABLE = false) won’t package them. Replace the unconditional
implementation/compileOnly entries with flavor-specific configurations (e.g.,
<flavorName>Implementation / <flavorName>CompileOnly) for all flavors that
should include the updater, ensuring AppInstaller.kt and UpdaterSettings.kt
remain compilable only in those updater-enabled flavors.


coreLibraryDesugaring(libs.desugaring)

implementation(libs.timber)
Expand Down
22 changes: 22 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,25 @@
-keepclasseswithmembers class com.metrolist.shazamkit.models.** {
kotlinx.serialization.KSerializer serializer(...);
}

## Shizuku & Hidden API Rules
-keep class rikka.shizuku.** { *; }
-keep class moe.shizuku.** { *; }
-keep class dev.rikka.tools.refine.** { *; }

# Hidden Android APIs accessed via Shizuku
-keep class android.content.pm.IPackageManager { *; }
-keep class android.content.pm.IPackageManager$Stub { *; }
-keep class android.content.pm.IPackageInstaller { *; }
-keep class android.content.pm.IPackageInstaller$Stub { *; }
-keep class android.content.pm.IPackageInstallerSession { *; }
-keep class android.content.pm.IPackageInstallerSession$Stub { *; }
-keep class android.content.pm.PackageInstallerHidden { *; }
-keep class android.content.pm.PackageInstallerHidden$* { *; }
-keep class android.content.pm.PackageManagerHidden { *; }

# libsu for root access
-keep class com.topjohnwu.superuser.** { *; }

# Dhizuku for device owner installation
-keep class com.rosan.dhizuku.** { *; }
18 changes: 17 additions & 1 deletion app/src/izzy/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Remove testOnly flag in Izzy build since Dhizuku is not available and builds must be distributable -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
tools:remove="android:testOnly">

<!-- Remove updater permission in Izzy build since updater is not available -->
<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:node="remove" />

<application>
<!-- Remove Cast provider in Izzy build since GMS is not available -->
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
tools:node="remove" />

<!-- Remove updater components in Izzy build since updater is not available -->
<receiver
android:name=".utils.updater.InstallReceiver"
tools:node="remove" />

<provider
android:name="rikka.shizuku.ShizukuProvider"
tools:node="remove" />
</application>
</manifest>
24 changes: 23 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
android:testOnly="true">
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<uses-sdk tools:overrideLibrary="rikka.shizuku.api, rikka.shizuku.provider, rikka.shizuku.shared, rikka.shizuku.aidl, com.rosan.dhizuku.api" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Expand Down Expand Up @@ -315,6 +319,24 @@
</intent-filter>
</service>

<!-- Install status receiver for session-based APK installation -->
<receiver
android:name=".utils.updater.InstallReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.metrolist.music.INSTALL_STATUS" />
</intent-filter>
</receiver>

<!-- Shizuku provider for privileged operations -->
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />

</application>

</manifest>
10 changes: 10 additions & 0 deletions app/src/main/kotlin/com/metrolist/music/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import okhttp3.Credentials
import timber.log.Timber
import java.net.Authenticator
import java.net.PasswordAuthentication
import org.lsposed.hiddenapibypass.HiddenApiBypass
import java.net.Proxy
import java.util.Locale
import javax.inject.Inject
Expand All @@ -65,6 +66,15 @@ class App :
// Install crash handler first
CrashHandler.install(this)

// Bypass hidden API restrictions for Shizuku installer (Android 9+)
if (BuildConfig.UPDATER_AVAILABLE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
runCatching {
HiddenApiBypass.addHiddenApiExemptions("I", "L")
}.onFailure {
Timber.w(it, "Hidden API bypass unavailable; privileged installers will be disabled")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Initialize cipher deobfuscator for WEB_REMIX streaming
CipherDeobfuscator.initialize(this)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ val SelectedYtmPlaylistsKey = stringPreferencesKey("selectedYtmPlaylists")
val CheckForUpdatesKey = booleanPreferencesKey("checkForUpdates")
val UpdateNotificationsEnabledKey = booleanPreferencesKey("updateNotifications")
val LastUpdateCheckTimeKey = longPreferencesKey("lastUpdateCheckTime")
val InstallerTypeKey = intPreferencesKey("installerType")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 '\bInstallerTypeKey\b' app/src/main/kotlin
rg -n -C3 '\.ordinal\b|entries\[[^]]+\]|valueOf\s*\(' app/src/main/kotlin

Repository: MetrolistGroup/Metrolist

Length of output: 17461


🏁 Script executed:

fd -n "Installer.kt" app/src/main/kotlin --exec cat -n {} \;

Repository: MetrolistGroup/Metrolist

Length of output: 298


🏁 Script executed:

fd "Installer.kt" app/src/main/kotlin --exec cat -n {} \;

Repository: MetrolistGroup/Metrolist

Length of output: 20260


Persist installer choice with a stable identifier.

An Int key here is very likely to end up storing InstallerType ordinals. That becomes brittle as soon as the enum changes shape—the installer list already changed during this PR—so a saved value can silently point at a different backend after an update.

🛠️ Suggested direction
-val InstallerTypeKey = intPreferencesKey("installerType")
+val InstallerTypeKey = stringPreferencesKey("installerType")

Store InstallerType.name (or an explicit persisted id) at the read/write sites, and migrate any existing int-backed value once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt` at line
87, The current InstallerTypeKey uses intPreferencesKey which leads to
persisting enum ordinals; change to storing InstallerType.name by replacing
intPreferencesKey("installerType") with a string-based key (e.g.,
stringPreferencesKey("installerType")) and update all read/write sites that use
InstallerTypeKey (places that parse/save InstallerType) to read/write by name
(use InstallerType.valueOf when reading) or an explicit persisted id; also add a
one-time migration that detects an existing integer value under
InstallerTypeKey, maps that ordinal to the correct InstallerType (using
InstallerType.values()) and writes the corresponding name string before removing
the old int value so existing installs are preserved.


val AudioQualityKey = stringPreferencesKey("audioQuality")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.metrolist.music.R
import com.metrolist.music.ui.screens.settings.MarkdownText
import com.metrolist.music.utils.Updater

@Composable
Expand All @@ -43,11 +44,7 @@ fun ReleaseNotesCard() {
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = releaseInfo.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
)
MarkdownText(releaseInfo.description)
}
}
Spacer(modifier = Modifier.height(16.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,25 +400,21 @@ fun AccountSettings(
Spacer(Modifier.height(4.dp))

if (BuildConfig.UPDATER_AVAILABLE && latestVersionName != BuildConfig.VERSION_NAME) {
val releaseInfo = Updater.getCachedLatestRelease()
val downloadUrl = releaseInfo?.let { Updater.getDownloadUrlForCurrentVariant(it) }

if (downloadUrl != null) {
PreferenceEntry(
title = {
Text(text = stringResource(R.string.new_version_available))
},
description = latestVersionName,
icon = {
BadgedBox(badge = { Badge() }) {
Icon(painterResource(R.drawable.update), null)
}
},
onClick = {
uriHandler.openUri(downloadUrl)
PreferenceEntry(
title = {
Text(text = stringResource(R.string.new_version_available))
},
description = latestVersionName,
icon = {
BadgedBox(badge = { Badge() }) {
Icon(painterResource(R.drawable.update), null)
}
)
}
},
onClick = {
onClose()
navController.navigate("settings/updater")
}
)
}
}
}
Expand Down
Loading
Loading