(chore) AMM v2.3.8 - last supported version before v3

This update includes:

- CronetLoader: Added utility for loading Cronet engine builder via reflection without direct dependency on Google Play Services.
- Cronet Provider: Attempted to install GMS Cronet provider using reflection.
- Notification: Added a notification for the last supported version before v3.
- Strings: Added strings related to the last supported version message.
- Code Cleanup: Refactored code for readability and maintainability.
- Library Updates: Updated dependencies and libraries.
- UI Improvements: Minor UI adjustments and improvements.

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/169/head v2.3.8
androidacy-user 11 months ago
parent c998bc969b
commit 5678753944

@ -45,6 +45,8 @@ android {
defaultConfig {
applicationId = "com.fox2code.mmm"
minSdk = 26
// app is in maintenance mode, we do not intend to update it to target newer SDKs
//noinspection OldTargetApi
targetSdk = 34
versionCode = 93
versionName = "2.3.8"
@ -52,33 +54,34 @@ android {
useSupportLibrary = true
}
multiDexEnabled = true
resourceConfigurations.addAll(
listOf(
"ar",
"bs",
"cs",
"de",
"es-rMX",
"es",
"el",
"fr",
"hu",
"id",
"it",
"ja",
"nl",
"pl",
"pt",
"pt-rBR",
"ru",
"tr",
"uk",
"vi",
"zh",
"zh-rTW",
"en"
androidResources {
localeFilters.addAll(
listOf(
"ar",
"bs",
"cs",
"de",
"es-rMX",
"es",
"el",
"fr",
"hu",
"id",
"it",
"ja",
"nl",
"pl",
"pt",
"pt-rBR",
"ru",
"tr",
"uk",
"vi",
"zh",
"zh-rTW"
)
)
)
}
ksp {
arg("room.schemaLocation", "$projectDir/roomSchemas")
}
@ -161,8 +164,7 @@ android {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
propertiesA.setProperty(
"client_id", propertiesA.getProperty(
"client_id",
default
"client_id", default
)
)
} else {
@ -214,8 +216,7 @@ android {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
propertiesA.setProperty(
"client_id", propertiesA.getProperty(
"client_id",
default
"client_id", default
)
)
} else {
@ -370,6 +371,7 @@ dependencies {
// logging interceptor
debugImplementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14")
// Chromium cronet from androidacy
// implementation("com.google.android.gms:play-services-cronet:18.1.0")
implementation("org.chromium.net:cronet-embedded:119.6045.31")
val libsuVersion = "6.0.0"

@ -12,7 +12,10 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import cat.ereza.customactivityoncrash.CustomActivityOnCrash
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
@ -22,13 +25,20 @@ class CrashHandler : AppCompatActivity() {
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
if (MainApplication.forceDebugLogging) Timber.i(
"CrashHandler.onCreate(%s)",
savedInstanceState
"CrashHandler.onCreate(%s)", savedInstanceState
)
// log intent with extras
if (MainApplication.forceDebugLogging) Timber.d("CrashHandler.onCreate: intent=%s", intent)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_crash_handler)
val view = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, insets.top, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
// set crash_details MaterialTextView to the exception passed in the intent or unknown if null
// convert stacktrace from array to string, and pretty print it (first line is the exception, the rest is the stacktrace, with each line indented by 4 spaces)
val crashDetails = findViewById<MaterialTextView>(R.id.crash_details)

@ -29,11 +29,14 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.room.Room
@ -66,7 +69,6 @@ import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
import com.fox2code.mmm.utils.room.ReposListDatabase
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputEditText
@ -79,7 +81,6 @@ import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import kotlin.math.roundToInt
import androidx.core.view.isVisible
class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
@ -208,6 +209,8 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
doSetupRestarting = false
}
onMainActivityCreate(this)
enableEdgeToEdge()
// make sure we don't draw behind the status bar
super.onCreate(savedInstanceState)
INSTANCE = this
// check for pref_crashed and if so start crash handler
@ -254,9 +257,16 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}.start()
MainApplication.getInstance().check(this)
setContentView(R.layout.activity_main)
val view = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
overScrollInsetTop = insets.top
overScrollInsetBottom = insets.bottom
view.setPadding(0, insets.top, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
this.setTitle(R.string.app_name_v2)
// set navigation bar color based on surfacecolors
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
progressIndicator = findViewById(R.id.progress_bar)
progressIndicator?.max = PRECISION
progressIndicator?.min = 0
@ -270,12 +280,10 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
moduleListOnline = findViewById(R.id.module_list_online)
searchTextInputEditText = findViewById(R.id.search_input)
val textInputEditText = searchTextInputEditText!!
val view = findViewById<View>(R.id.root_container)
var startBottom = 0f
var endBottom = 0f
ViewCompat.setWindowInsetsAnimationCallback(
view,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
// Override methods…
override fun onProgress(
insets: WindowInsetsCompat,
@ -567,6 +575,8 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}
// reset update module and update module count in main application
MainApplication.getInstance().resetUpdateModule()
moduleViewListBuilder.addNotification(NotificationType.LAST_VER)
moduleViewListBuilderOnline.addNotification(NotificationType.LAST_VER)
tryGetMagiskPathAsync(object : InstallerInitializer.Callback {
override fun onPathReceived(path: String?) {
if (MainApplication.forceDebugLogging) Timber.i("Got magisk path: %s", path)
@ -630,7 +640,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
moduleViewListBuilder.addNotification(NotificationType.NO_WEB_VIEW)
// disable online tab
runOnUiThread {
bottomNavigationView.menu.getItem(1).isEnabled = false
bottomNavigationView.menu[1].isEnabled = false
bottomNavigationView.selectedItemId = R.id.installed_menu_item
}
}
@ -825,7 +835,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}
override fun onRefresh() {
if (swipeRefreshBlocker > System.currentTimeMillis() || initMode || progressIndicator == null || progressIndicator!!.visibility == View.VISIBLE || doSetupNowRunning) {
if (swipeRefreshBlocker > System.currentTimeMillis() || initMode || progressIndicator == null || progressIndicator!!.isVisible || doSetupNowRunning) {
swipeRefreshLayout!!.isRefreshing = false
return // Do not double scan
}

@ -554,12 +554,10 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
private var shellBuilder: Shell.Builder? = null
// Is application wrapped, and therefore must reduce it's feature set.
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
const val IS_WRAPPED = false
init {
assert(!IS_WRAPPED) { "This application is not wrapped!" }
assert(!IS_WRAPPED) { "This application is wrapped!" }
}
private val callers = ArrayList<String>()
@ -737,9 +735,8 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
if (updateCheckBg != null) {
return java.lang.Boolean.parseBoolean(updateCheckBg)
}
val wrapped = IS_WRAPPED
val updateCheckBgTemp = !wrapped && getPreferences("mmm")!!.getBoolean(
"pref_background_update_check", true
@Suppress("KotlinConstantConditions") val updateCheckBgTemp = getPreferences("mmm")!!.getBoolean(
"pref_background_update_check", BuildConfig.ENABLE_AUTO_UPDATER
)
updateCheckBg = updateCheckBgTemp.toString()
return java.lang.Boolean.parseBoolean(updateCheckBg)
@ -753,6 +750,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
getPreferences("mmm")!!.edit { putBoolean("has_root_access", bool) }
}
@Suppress("KotlinConstantConditions")
val isCrashReportingEnabled: Boolean
get() = analyticsAllowed() && getPreferences("mmm")!!.getBoolean(
"pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING
@ -768,6 +766,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
val isNotificationPermissionGranted: Boolean
get() = NotificationManagerCompat.from((INSTANCE)!!).areNotificationsEnabled()
@Suppress("KotlinConstantConditions")
fun analyticsAllowed(): Boolean {
return getPreferences("mmm")!!.getBoolean(
"pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS

@ -88,7 +88,6 @@ enum class NotificationType(
}
},
MAGISK_OUTDATED(
R.string.magisk_outdated,
R.drawable.ic_baseline_update_24,
@ -225,6 +224,19 @@ enum class NotificationType(
return !BuildConfig.DEBUG && (MainApplication.isShowcaseMode || InstallerInitializer.peekMagiskPath() == null)
}
},
LAST_VER(
R.string.last_ver,
R.drawable.ic_baseline_info_24,
com.google.android.material.R.attr.colorSurfaceBright,
com.google.android.material.R.attr.colorOnSurface,
View.OnClickListener { v: View ->
MaterialAlertDialogBuilder(v.context)
.setTitle(R.string.last_ver)
.setMessage(R.string.last_ver_message)
.setPositiveButton(android.R.string.ok, null)
.show()
}
),
KSU_EXPERIMENTAL(
R.string.ksu_experimental,
R.drawable.ic_baseline_warning_24,

@ -11,14 +11,18 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources.Theme
import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.view.View
import android.webkit.CookieManager
import android.widget.CompoundButton
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.FragmentActivity
import androidx.room.Room
import com.fox2code.mmm.databinding.ActivitySetupBinding
@ -49,9 +53,9 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
@SuppressLint("ApplySharedPref", "RestrictedApi")
@Suppress("KotlinConstantConditions", "NAME_SHADOWING")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
this.setTitle(R.string.setup_title)
this.window.navigationBarColor = this.getColor(R.color.black_transparent)
createFiles()
disableUpdateActivityForFdroidFlavor()
if (BuildConfig.DEBUG) {
@ -68,6 +72,12 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
}
val binding = ActivitySetupBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, insets.top, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
MainApplication.getInstance().check(this)
val view: View = binding.root
// if our application id is "com.androidacy.mmm" or begins with it, check if com.fox2code.mmm is installed and offer to uninstall it. if we're com.fox2code.mmm, check if com.fox2code.mmm.fdroid or com.fox2code.mmm.debug is installed and offer to uninstall it
@ -93,14 +103,13 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
materialAlertDialogBuilder.setTitle(R.string.setup_uninstall_title)
materialAlertDialogBuilder.setMessage(
getString(
R.string.setup_uninstall_message,
packageName
R.string.setup_uninstall_message, packageName
)
)
materialAlertDialogBuilder.setPositiveButton(R.string.uninstall) { _: DialogInterface?, _: Int ->
// start uninstall intent
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:$packageName")
intent.data = "package:$packageName".toUri()
startActivity(intent)
}
materialAlertDialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
@ -113,14 +122,13 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
materialAlertDialogBuilder.setTitle(R.string.setup_uninstall_title)
materialAlertDialogBuilder.setMessage(
getString(
R.string.setup_uninstall_message,
packageName
R.string.setup_uninstall_message, packageName
)
)
materialAlertDialogBuilder.setPositiveButton(R.string.uninstall) { _: DialogInterface?, _: Int ->
// start uninstall intent
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:$packageName")
intent.data = "package:$packageName".toUri()
startActivity(intent)
}
materialAlertDialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
@ -133,14 +141,13 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
materialAlertDialogBuilder.setTitle(R.string.setup_uninstall_title)
materialAlertDialogBuilder.setMessage(
getString(
R.string.setup_uninstall_message,
packageName
R.string.setup_uninstall_message, packageName
)
)
materialAlertDialogBuilder.setPositiveButton(R.string.uninstall) { _: DialogInterface?, _: Int ->
// start uninstall intent
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:$packageName")
intent.data = "package:$packageName".toUri()
startActivity(intent)
}
materialAlertDialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
@ -250,7 +257,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
themeNames, checkedItem
) { dialog: DialogInterface, which: Int ->
// Set the theme
prefs.edit().putString("pref_theme", themeValues[which]).commit()
prefs.edit(commit = true) { putString("pref_theme", themeValues[which]) }
// Dismiss the dialog
dialog.dismiss()
// Set the theme
@ -356,21 +363,17 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
editor.commit()
// Log the changes
if (MainApplication.forceDebugLogging) Timber.d(
"Setup finished. Preferences: %s",
prefs.all
"Setup finished. Preferences: %s", prefs.all
)
if (MainApplication.forceDebugLogging) Timber.d(
"Androidacy repo: %s",
androidacyRepoRoom
"Androidacy repo: %s", androidacyRepoRoom
)
if (MainApplication.forceDebugLogging) Timber.d(
"Magisk Alt repo: %s",
magiskAltRepoRoom
"Magisk Alt repo: %s", magiskAltRepoRoom
)
// log last shown setup
if (MainApplication.forceDebugLogging) Timber.d(
"Last shown setup: %s",
prefs.getString("last_shown_setup", "v0")
"Last shown setup: %s", prefs.getString("last_shown_setup", "v0")
)
// Restart the activity
MainActivity.doSetupRestarting = true
@ -544,8 +547,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
db.close()
db2.close()
if (MainApplication.forceDebugLogging) Timber.d(
"Databases created in %s ms",
System.currentTimeMillis() - startTime
"Databases created in %s ms", System.currentTimeMillis() - startTime
)
}
thread.start()

@ -12,6 +12,7 @@ import android.view.View
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.FileProvider
@ -37,6 +38,7 @@ class UpdateActivity : AppCompatActivity() {
@SuppressLint("RestrictedApi", "SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_update)
MainApplication.getInstance().check(this)

@ -26,8 +26,11 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.webkit.WebResourceErrorCompat
import androidx.webkit.WebSettingsCompat
@ -71,6 +74,7 @@ class AndroidacyActivity : AppCompatActivity() {
)
override fun onCreate(savedInstanceState: Bundle?) {
moduleFile = File(this.cacheDir, "module.zip")
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val intent = this.intent
@ -127,6 +131,13 @@ class AndroidacyActivity : AppCompatActivity() {
val config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG)
val compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0)
this.setContentView(R.layout.webview)
val view = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, insets.top, 0, 0)
WindowInsetsCompat.CONSUMED
}
if (title.isNullOrEmpty()) {
title = "Androidacy"
}

@ -8,6 +8,7 @@ package com.fox2code.mmm.androidacy
import android.net.Uri
import com.fox2code.mmm.BuildConfig
import androidx.core.net.toUri
@Suppress("MemberVisibilityCanBePrivate", "MemberVisibilityCanBePrivate")
enum class AndroidacyUtil {
@ -79,8 +80,17 @@ enum class AndroidacyUtil {
// Strip non alphanumeric
moduleId = moduleId.replace("[^a-zA-Z\\d]".toRegex(), "")
return moduleId
}// fallback to last sergment minus .zip
// Get the last segment
val lastSegment = moduleUrl.toUri().lastPathSegment
// Check if it ends with .zip
if (lastSegment != null && lastSegment.endsWith(".zip")) {
// Strip the .zip
moduleId = lastSegment.substring(0, lastSegment.length - 4)
// Strip non alphanumeric
moduleId = moduleId.replace("[^a-zA-Z\\d]".toRegex(), "")
return moduleId
}
require(!BuildConfig.DEBUG) { "Invalid module url: $moduleUrl" }
return null
}
@ -96,6 +106,17 @@ enum class AndroidacyUtil {
Uri.decode(moduleUrl.substring(i + 13, j))
}
}
// fallback to last sergment minus .zip
// Get the last segment
val lastSegment = moduleUrl.toUri().lastPathSegment
// Check if it ends with .zip
if (lastSegment != null && lastSegment.endsWith(".zip")) {
// Strip the .zip
val moduleId = lastSegment.substring(0, lastSegment.length - 4)
// Strip non alphanumeric
val moduleTitle = moduleId.replace("[^a-zA-Z\\d]".toRegex(), "")
return moduleTitle
}
return null
}

@ -12,7 +12,6 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
@ -21,8 +20,13 @@ import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.fox2code.androidansi.AnsiConstants
import com.fox2code.androidansi.AnsiParser
@ -65,16 +69,16 @@ import java.io.InputStreamReader
import java.util.Enumeration
import java.util.concurrent.Executor
import java.util.zip.ZipEntry
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isVisible
class InstallerActivity : AppCompatActivity() {
private var canGoBack: Boolean = false
private val isLightTheme: Boolean
get() = MainApplication.getInstance().isLightTheme
private var progressIndicator: LinearProgressIndicator? = null
@SuppressLint("RestrictedApi")
private var rebootFloatingButton: BottomNavigationItemView? = null
@SuppressLint("RestrictedApi")
private var cancelFloatingButton: BottomNavigationItemView? = null
private var installerTerminal: InstallerTerminal? = null
@ -87,10 +91,11 @@ class InstallerActivity : AppCompatActivity() {
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
warnReboot = false
moduleCache = File(this.cacheDir, "installer")
if (!moduleCache!!.exists() && !moduleCache!!.mkdirs()) Timber.e("Failed to mkdir module cache dir!")
super.onCreate(savedInstanceState)
val intent = this.intent
val target: String
@ -142,6 +147,12 @@ class InstallerActivity : AppCompatActivity() {
title = name
textWrap = MainApplication.isTextWrapEnabled
setContentView(if (textWrap) R.layout.installer_wrap else R.layout.installer)
val view = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, insets.top, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
val background: Int
val foreground: Int
if (MainApplication.getInstance().isLightTheme && !MainApplication.isForceDarkTerminal) {
@ -181,14 +192,16 @@ class InstallerActivity : AppCompatActivity() {
if (urlMode) installerTerminal!!.addLine("- Downloading $name")
// track install
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("install", mapOf(
"name" to name,
"url" to target,
"checksum" to checksum,
"noExtensions" to noExtensions,
"rootless" to rootless,
"mmtReborn" to mmtReborn
))
Countly.sharedInstance().events().recordEvent(
"install", mapOf(
"name" to name,
"url" to target,
"checksum" to checksum,
"noExtensions" to noExtensions,
"rootless" to rootless,
"mmtReborn" to mmtReborn
)
)
}
Thread(Runnable {
// ensure module cache is is in our cache dir
@ -229,7 +242,10 @@ class InstallerActivity : AppCompatActivity() {
}
if (canceled) return@Runnable
if (!checksum.isNullOrEmpty()) {
if (MainApplication.forceDebugLogging) Timber.i("Checking for checksum: %s", checksum.toString())
if (MainApplication.forceDebugLogging) Timber.i(
"Checking for checksum: %s",
checksum.toString()
)
runOnUiThread { installerTerminal!!.addLine("- Checking file integrity") }
if (!checkSumMatch(rawModule, checksum)) {
setInstallStateFinished(false, "! File integrity check failed", "")
@ -342,12 +358,12 @@ class InstallerActivity : AppCompatActivity() {
)
val installerMonitor: InstallerMonitor
val installJob: Shell.Job
val mgskPath = InstallerInitializer.peekMagiskPath()
val ashExec = if (!InstallerInitializer.isKsu) {
"$mgskPath/magisk/busybox ash"
} else {
"$mgskPath/ksu/busybox ash"
}
val mgskPath = InstallerInitializer.peekMagiskPath()
val ashExec = if (!InstallerInitializer.isKsu) {
"$mgskPath/magisk/busybox ash"
} else {
"$mgskPath/ksu/busybox ash"
}
if (rootless) { // rootless is only used for debugging
val installScript = extractInstallScript("module_installer_test.sh")

@ -16,9 +16,12 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.FragmentTransaction
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@ -76,6 +79,7 @@ class SettingsActivity : AppCompatActivity(), LanguageActivity,
@SuppressLint("RestrictedApi", "CommitTransaction")
override fun onCreate(savedInstanceState: Bundle?) {
devModeStep = 0
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// check for pref_crashed and if so start crash handler
@ -101,6 +105,12 @@ class SettingsActivity : AppCompatActivity(), LanguageActivity,
}
setContentView(R.layout.settings_activity)
val view = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, insets.top, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
setTitle(R.string.app_name_v2)
MainApplication.getInstance().check(this)
//hideActionBar();

@ -20,13 +20,14 @@ import com.fox2code.mmm.Constants
import com.fox2code.mmm.MainApplication
import com.topjohnwu.superuser.internal.UiThreadHandler
import timber.log.Timber
import androidx.core.net.toUri
class ExternalHelper private constructor() {
private var fallback: ComponentName? = null
private var label: CharSequence? = null
private var multi = false
fun refreshHelper(context: Context) {
val intent = Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip"))
val intent = Intent(FOX_MMM_OPEN_EXTERNAL, "https://fox2code.com/module.zip".toUri())
val resolveInfos =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(

@ -27,6 +27,8 @@ import com.fox2code.mmm.SetupActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.topjohnwu.superuser.Shell
import timber.log.Timber
import androidx.core.content.edit
import androidx.core.net.toUri
@Suppress("UNUSED_PARAMETER")
class RuntimeUtils {
@ -64,9 +66,11 @@ class RuntimeUtils {
checkBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
PreferenceManager.getDefaultSharedPreferences(
context
).edit().putBoolean(
"pref_dont_ask_again_notification_permission", isChecked
).apply()
).edit {
putBoolean(
"pref_dont_ask_again_notification_permission", isChecked
)
}
}
builder.setView(view)
builder.setPositiveButton(R.string.permission_notification_grant) { _, _ ->
@ -79,7 +83,7 @@ class RuntimeUtils {
builder.setNegativeButton(R.string.cancel) { dialog, _ ->
// Set pref_background_update_check to false and dismiss dialog
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putBoolean("pref_background_update_check", false).apply()
prefs.edit { putBoolean("pref_background_update_check", false) }
dialog.dismiss()
MainActivity.doSetupNowRunning = false
}
@ -118,9 +122,9 @@ class RuntimeUtils {
checkBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
PreferenceManager.getDefaultSharedPreferences(
context
).edit()
.putBoolean("pref_dont_ask_again_notification_permission", isChecked)
.apply()
).edit {
putBoolean("pref_dont_ask_again_notification_permission", isChecked)
}
}
builder.setView(view)
builder.setPositiveButton(R.string.permission_notification_grant) { _, _ ->
@ -135,7 +139,7 @@ class RuntimeUtils {
builder.setNegativeButton(R.string.cancel) { dialog, _ ->
// Set pref_background_update_check to false and dismiss dialog
val prefs = MainApplication.getPreferences("mmm")!!
prefs.edit().putBoolean("pref_background_update_check", false).apply()
prefs.edit { putBoolean("pref_background_update_check", false) }
dialog.dismiss()
MainActivity.doSetupNowRunning = false
}
@ -223,12 +227,12 @@ class RuntimeUtils {
builder.setPositiveButton(R.string.upgrade_now) { dialog, _ ->
val intent = Intent(Intent.ACTION_VIEW)
intent.data =
Uri.parse("https://androidacy.com/membership-join/#utm_source=AMMM&utm_medium=app&utm_campaign=upgrade_snackbar")
"https://androidacy.com/membership-join/#utm_source=AMMM&utm_medium=app&utm_campaign=upgrade_snackbar".toUri()
activity.startActivity(intent)
dialog.dismiss()
}
// do not show for another 7 days
prefs.edit().putLong("ugsns4", System.currentTimeMillis()).apply()
prefs.edit { putLong("ugsns4", System.currentTimeMillis()) }
if (MainApplication.forceDebugLogging) Timber.i("showUpgradeSnackbar done")
}

@ -0,0 +1,220 @@
package com.fox2code.mmm.utils.io
import android.content.Context
import android.content.pm.PackageManager
import timber.log.Timber
/**
* Utility for loading Cronet engine builder through reflection
* without direct dependency on Google Play Services
*
* Of course, actually getting the provider does rely on GMS but we have a fallback packaged in the app.
*/
class CronetLoader {
companion object {
private const val GMS_PROVIDER_NAME = "Google-Play-Services-Cronet-Provider"
private const val GMS_PACKAGE = "com.google.android.gms"
private const val DYNAMITE_MODULE_CLASS = "com.google.android.gms.dynamite.DynamiteModule"
private const val DYNAMITE_MODULE_ID = "com.google.android.gms.cronet_dynamite"
/**
* Get a Cronet engine builder via reflection, attempting to install GMS provider first
*
* @param context Application context
* @return CronetEngine.Builder instance or null if unavailable
*/
fun getCronetEngineBuilder(context: Context): Any? {
// Check if GMS is installed
val gmsInstalled = isGmsInstalled(context)
if (gmsInstalled) {
Timber.d("GMS is installed, attempting to load Cronet provider")
// Try to initialize GMS provider
try {
installGmsProviderViaReflection(context)
} catch (e: Exception) {
Timber.w("Failed to install GMS Cronet provider: ${e.message}")
false
}
} else {
Timber.d("GMS is not installed, skipping GMS provider initialization")
}
// Whether GMS was initialized or not, try to get available providers
return getProviderBuilder(context)
}
/**
* Check if Google Play Services is installed on the device
*/
private fun isGmsInstalled(context: Context): Boolean {
return try {
// Try to create a package context for GMS
context.createPackageContext(
GMS_PACKAGE, Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY
)
true
} catch (e: PackageManager.NameNotFoundException) {
Timber.d("Google Play Services not installed")
false
} catch (e: Exception) {
Timber.d("Error checking for Google Play Services: ${e.message}")
false
}
}
/**
* Attempt to install GMS Cronet provider using reflection
*/
private fun installGmsProviderViaReflection(context: Context): Boolean {
try {
// Try to load Dynamite module class
val dynamiteClass = try {
Class.forName(DYNAMITE_MODULE_CLASS)
} catch (e: ClassNotFoundException) {
Timber.d("DynamiteModule class not found")
return false
}
// Find the load method
val loadMethod = try {
// We need to find the right version of the load method
dynamiteClass.getDeclaredMethods().firstOrNull { method ->
method.name == "load" && method.parameterTypes.size == 3 && method.parameterTypes[0] == Context::class.java && method.parameterTypes[2] == String::class.java
}
} catch (e: Exception) {
Timber.d("Could not find appropriate load method: ${e.message}")
return false
}
if (loadMethod == null) {
Timber.d("Could not find appropriate load method")
return false
}
// Get the PREFER_REMOTE constant
val preferRemoteObj = try {
val fields = dynamiteClass.declaredFields
val preferRemoteField = fields.firstOrNull { field ->
field.name == "PREFER_REMOTE"
}
if (preferRemoteField == null) {
Timber.d("PREFER_REMOTE field not found")
return false
}
preferRemoteField.get(null)
} catch (e: Exception) {
Timber.d("Error getting PREFER_REMOTE: ${e.message}")
return false
}
try {
// Attempt to load the Cronet dynamite module
loadMethod.invoke(null, context, preferRemoteObj, DYNAMITE_MODULE_ID)
Timber.d("Successfully loaded GMS Cronet dynamite module")
return true
} catch (e: Exception) {
Timber.d("Failed to load Cronet dynamite module: ${e.message}")
return false
}
} catch (e: Exception) {
Timber.d("Error during GMS provider installation: ${e.message}")
return false
}
}
/**
* Get a builder from available providers, prioritizing GMS
*/
private fun getProviderBuilder(context: Context): Any? {
try {
// Try to access the CronetProvider class
val providerClass = try {
Class.forName("org.chromium.net.CronetProvider")
} catch (e: ClassNotFoundException) {
Timber.d("Cronet API not available")
return null
}
// Get all available providers
val getAllProvidersMethod =
providerClass.getMethod("getAllProviders", Context::class.java)
val providers =
getAllProvidersMethod.invoke(null, context) as? List<*> ?: emptyList<Any>()
if (providers.isEmpty()) {
Timber.d("No Cronet providers found")
return null
}
// Get method references we'll need
val isEnabledMethod = providerClass.getMethod("isEnabled")
val getNameMethod = providerClass.getMethod("getName")
val createBuilderMethod = providerClass.getMethod("createBuilder")
// Get fallback name constant
val fallbackName = try {
val field = providerClass.getField("PROVIDER_NAME_FALLBACK")
field.get(null) as String
} catch (e: Exception) {
"fallback"
}
// Get native name constant
val nativeName = try {
val field = providerClass.getField("PROVIDER_NAME_APP_PACKAGED")
field.get(null) as String
} catch (e: Exception) {
"APP_PACKAGED"
}
// Filter and categorize providers
val gmsProviders = mutableListOf<Any>()
val nativeProviders = mutableListOf<Any>()
val otherProviders = mutableListOf<Any>()
for (provider in providers) {
try {
val isEnabled = isEnabledMethod.invoke(provider) as Boolean
val name = getNameMethod.invoke(provider) as String
if (isEnabled && name != fallbackName && provider != null) {
when (name) {
GMS_PROVIDER_NAME -> gmsProviders.add(provider)
nativeName -> nativeProviders.add(provider)
else -> otherProviders.add(provider)
}
Timber.d("Found enabled provider: $name")
}
} catch (e: Exception) {
Timber.d("Error checking provider: ${e.message}")
}
}
// Try providers in order of preference
val orderedProviders = gmsProviders + nativeProviders + otherProviders
for (provider in orderedProviders) {
try {
val name = getNameMethod.invoke(provider) as String
val builder = createBuilderMethod.invoke(provider)
Timber.d("Successfully created Cronet builder using: $name")
return builder
} catch (e: Exception) {
Timber.w("Failed to create builder from provider: ${e.message}")
}
}
Timber.d("Could not create Cronet builder from any provider")
} catch (e: Exception) {
Timber.e(e, "Error getting Cronet providers")
}
return null
}
}
}

@ -223,7 +223,7 @@ enum class Files {
// unzip
if (MainApplication.forceDebugLogging) Timber.d("Unzipping module to %s", tempUnzipDir.absolutePath)
try {
ZipFile(tempFile).use { zipFile ->
ZipFile.builder().setFile(tempFile).get().use { zipFile ->
var files = zipFile.entries
// check if there is only one folder in the top level
var folderCount = 0

@ -36,6 +36,14 @@ enum class GMSProviderInstaller {
remote.classLoader.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
cl.getDeclaredMethod("insertProvider", Context::class.java).invoke(null, remote)
if (MainApplication.forceDebugLogging) Timber.i("Installed GMS security providers!")
// next, try cronet provider
val cronet = context.createPackageContext(
"com.google.android.gms",
Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY
)
val cronetCl =
cronet.classLoader.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
cronetCl.getDeclaredMethod("insertProvider", Context::class.java).invoke(null, cronet)
} catch (e: PackageManager.NameNotFoundException) {
Timber.w("No GMS Implementation are installed on this device")
} catch (e: Exception) {

@ -28,6 +28,7 @@ import com.fox2code.mmm.R
import com.fox2code.mmm.androidacy.AndroidacyUtil
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
import com.fox2code.mmm.utils.io.CronetLoader
import com.fox2code.mmm.utils.io.Files.Companion.makeBuffer
import com.google.net.cronet.okhttptransport.CronetInterceptor
import ly.count.android.sdk.Countly
@ -190,19 +191,6 @@ enum class Http {;
init {
val mainApplication = MainApplication.getInstance()
if (mainApplication == null) {
val error = Error("Initialized Http too soon!")
error.fillInStackTrace()
Timber.e(error, "Initialized Http too soon!")
System.out.flush()
System.err.flush()
try {
Os.kill(Os.getpid(), 9)
} catch (e: ErrnoException) {
exitProcess(9)
}
throw error
}
var cookieManager: CookieManager? = null
try {
cookieManager = CookieManager.getInstance()
@ -318,9 +306,14 @@ enum class Http {;
// Add cronet interceptor
// init cronet
try {
// Load the cronet library
val builder: CronetEngine.Builder =
CronetEngine.Builder(mainApplication.applicationContext)
val cronetEngine = try {
// Load the cronet library
CronetLoader.getCronetEngineBuilder(MainApplication.getInstance().applicationContext) as CronetEngine.Builder
} catch (e: Exception) {
Timber.e(e, "Failed to load cronet library from GMS")
null
}
val builder = cronetEngine ?: CronetEngine.Builder(MainApplication.getInstance().applicationContext)
builder.enableBrotli(true)
builder.enableHttp2(true)
builder.enableQuic(true)

@ -430,4 +430,6 @@
<string name="invalid_update_url">No update URL found</string>
<string name="invalid_update_url_message">Could not determine a suitable URL to reinstall or update this module from. Please check with the source you got this module from.</string>
<string name="show_apps">Our other apps</string>
<string name="last_ver">AMM V3 coming soon!</string>
<string name="last_ver_message">V3 of AMM is launching soon! This version (2.3.8) is the last supported version of any version before v3, and will be deprecated shortly after launching v3.</string>
</resources>

Loading…
Cancel
Save