diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 116b02c..ab014d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -363,7 +363,7 @@ dependencies { implementation("androidx.webkit:webkit:1.8.0") implementation("com.google.android.material:material:1.9.0") - implementation("com.mikepenz:aboutlibraries:10.8.3") + implementation("com.mikepenz:aboutlibraries:10.9.1") // Utils implementation("androidx.work:work-runtime:2.8.1") @@ -405,7 +405,7 @@ dependencies { implementation("androidx.security:security-crypto:1.1.0-alpha06") // some utils - implementation("commons-io:commons-io:2.13.0") + implementation("commons-io:commons-io:2.14.0") implementation("org.apache.commons:commons-compress:1.24.0") // analytics diff --git a/app/src/main/kotlin/com/fox2code/mmm/CrashHandler.kt b/app/src/main/kotlin/com/fox2code/mmm/CrashHandler.kt index ae69636..9c3f29c 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/CrashHandler.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/CrashHandler.kt @@ -8,6 +8,7 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.DialogInterface +import android.content.Intent import android.os.Bundle import android.view.View import android.widget.Toast @@ -32,20 +33,25 @@ class CrashHandler : AppCompatActivity() { val crashDetails = findViewById(R.id.crash_details) crashDetails.text = "" // get the exception from the intent - val exception = intent.getSerializableExtra("exception") as Throwable? - // get the crashReportingEnabled from the intent - intent.getBooleanExtra("crashReportingEnabled", false) + val exceptionFromIntent = intent.getSerializableExtra("exception") as Throwable? + var exception: String? = null + // parse the exception from the intent into exception if it is not null + if (exceptionFromIntent != null) { + val stringWriter = StringWriter() + exceptionFromIntent.printStackTrace(PrintWriter(stringWriter)) + var stacktrace = stringWriter.toString() + stacktrace = stacktrace.replace(",", "\n ") + exception = stacktrace + } + val sharedPreferences = MainApplication.getPreferences("mmm") + if (exception == null && sharedPreferences != null) { + exception = sharedPreferences.getString("pref_crash_stacktrace", null) + } // if the exception is null, set the crash details to "Unknown" if (exception == null) { crashDetails.setText(R.string.crash_details) } else { - // if the exception is not null, set the crash details to the exception and stacktrace - // stacktrace is an StacktraceElement, so convert it to a string and replace the commas with newlines - val stringWriter = StringWriter() - exception.printStackTrace(PrintWriter(stringWriter)) - var stacktrace = stringWriter.toString() - stacktrace = stacktrace.replace(",", "\n ") - crashDetails.text = getString(R.string.crash_full_stacktrace, stacktrace) + crashDetails.text = getString(R.string.crash_full_stacktrace, exception) } // handle reset button findViewById(R.id.reset).setOnClickListener { _: View? -> @@ -60,6 +66,16 @@ class CrashHandler : AppCompatActivity() { builder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> } builder.show() } + // restart button + findViewById(R.id.restart).setOnClickListener { _: View? -> + // restart the app + val intent = packageManager.getLaunchIntentForPackage(packageName) + intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + finish() + } + // remove pref_crashed from shared preferences + sharedPreferences?.edit()?.remove("pref_crashed")?.apply() } fun copyCrashDetails(view: View) { diff --git a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt index 7a8483b..47fc201 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt @@ -80,6 +80,7 @@ import java.io.InputStream import java.io.OutputStream import java.nio.file.Files.* import java.sql.Timestamp +import kotlin.math.roundToInt class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { @@ -203,6 +204,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } override fun onResume() { + super.onResume() onMainActivityResume(this) // check that installed or online is selected depending on which recyclerview is visible if (moduleList!!.visibility == View.VISIBLE) { @@ -211,8 +213,16 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { bottomNavigationView.selectedItemId = R.id.online_menu_item } // rescan modules - instance!!.scanAsync() - super.onResume() + if (!MainApplication.dirty) { + instance!!.scanAsync() + } else { + MainApplication.dirty = false + // same as onRefresh + // call onrefresh + swipeRefreshLayout!!.post { swipeRefreshLayout!!.isRefreshing = true } + this.onRefresh() + } + } @SuppressLint("RestrictedApi") @@ -224,6 +234,14 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { onMainActivityCreate(this) super.onCreate(savedInstanceState) INSTANCE = this + // check for pref_crashed and if so start crash handler + val sharedPreferences = MainApplication.getPreferences("mmm") + if (sharedPreferences?.getBoolean("pref_crashed", false) == true) { + val intent = Intent(this, CrashHandler::class.java) + startActivity(intent) + finish() + return + } // hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug if (BuildConfig.ENABLE_PROTECTION && !MainApplication.o && !BuildConfig.DEBUG) { @@ -252,10 +270,11 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { // use countly to track enabled repos val repoMap = HashMap() repoMap["repos"] = enabledRepos.toString() - Countly.sharedInstance().events().recordEvent( - "enabled_repos", - repoMap as Map?, 1 - ) + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events() + .recordEvent( + "enabled_repos", + repoMap as Map?, 1 + ) } }.start() val ts = Timestamp(System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000) @@ -284,6 +303,9 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { // 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 + progressIndicator?.setProgress(2, true) swipeRefreshLayout = findViewById(R.id.swipe_refresh) val swipeRefreshLayout = swipeRefreshLayout!! swipeRefreshLayoutOrigStartOffset = swipeRefreshLayout.progressViewStartOffset @@ -346,7 +368,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { // filter the appropriate list based on visibility if (initMode) return val query = s.toString() - Countly.sharedInstance().events() + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events() .recordEvent("search", HashMap().apply { put("query", query) } as Map?, 1) @@ -381,7 +403,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (actionId == EditorInfo.IME_ACTION_SEARCH) { // filter the appropriate list based on visibility val query = textInputEditText.text.toString() - Countly.sharedInstance().events() + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events() .recordEvent("search", HashMap().apply { put("query", query) } as Map?, 1) @@ -614,6 +636,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { instance!!.scan() instance!!.runAfterScan { moduleViewListBuilder.appendInstalledModules() } instance!!.runAfterScan { moduleViewListBuilderOnline.appendRemoteModules() } + progressIndicator?.setProgress(10, true) commonNext() } @@ -628,14 +651,18 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (BuildConfig.DEBUG) { moduleViewListBuilder.addNotification(NotificationType.DEBUG) } + NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline) val progressIndicator = progressIndicator!! + runOnUiThread { + progressIndicator.isIndeterminate = false + progressIndicator.setProgress(30, true) + } // hide progress bar is repo-manager says we have no internet if (!RepoManager.getINSTANCE()!!.hasConnectivity()) { if (MainApplication.forceDebugLogging) Timber.i("No connection, hiding progress") runOnUiThread { progressIndicator.visibility = View.GONE - progressIndicator.isIndeterminate = false progressIndicator.max = PRECISION } } @@ -670,18 +697,22 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (MainApplication.forceDebugLogging) Timber.i("Check Update Compat") appUpdateManager.checkUpdateCompat() if (MainApplication.forceDebugLogging) Timber.i("Check Update") - // update repos + // update repos. progress is from 30 to 80, so subtract 20 from max if (hasWebView()) { val updateListener: SyncManager.UpdateListener = object : SyncManager.UpdateListener { override fun update(value: Int) { + Timber.i("Update progress: %d", value) + // progress is out of a hundred (Int) and starts at 30 once we've reached this point runOnUiThread(if (max == 0) Runnable { - progressIndicator.setProgressCompat( - value, true + progressIndicator.setProgress( + 80, + true ) } else Runnable { - progressIndicator.setProgressCompat( - value, true + progressIndicator.setProgress( + 30 + value, + true ) }) } @@ -698,7 +729,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } else { if (!hasWebView()) { runOnUiThread { - progressIndicator.setProgressCompat(PRECISION, true) + progressIndicator.setProgress(PRECISION, true) progressIndicator.visibility = View.GONE } return @@ -726,15 +757,26 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } current++ val currentTmp = current + // progress starts at 80 and goes to 99. each module should add a equal amount of progress to the bar, rounded up to the nearest integer runOnUiThread { - progressIndicator.setProgressCompat( - currentTmp / max, true + progressIndicator.setProgress( + 80 + (currentTmp / max.toFloat() * 20).roundToInt(), + true ) + if (BuildConfig.DEBUG) { + Timber.i( + "Progress: %d", + 80 + (currentTmp / max.toFloat() * 20).roundToInt() + ) + } } } } } } + runOnUiThread { + progressIndicator.isIndeterminate = true + } if (MainApplication.forceDebugLogging) Timber.i("Apply") RepoManager.getINSTANCE() ?.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() } @@ -752,12 +794,13 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (MainApplication.forceDebugLogging) Timber.i("Badge applied") } } + maybeShowUpgrade() + if (MainApplication.forceDebugLogging) Timber.i("Finished app opening state!") runOnUiThread { - progressIndicator.setProgressCompat(PRECISION, true) + progressIndicator.isIndeterminate = false + progressIndicator.setProgress(PRECISION, true) progressIndicator.visibility = View.GONE } - maybeShowUpgrade() - if (MainApplication.forceDebugLogging) Timber.i("Finished app opening state!") } }, true) // if system lang is not in MainApplication.supportedLocales, show a snackbar to ask user to help translate @@ -772,7 +815,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } ExternalHelper.INSTANCE.refreshHelper(this) initMode = false - if (MainApplication.shouldShowFeedback()) { + if (MainApplication.shouldShowFeedback() && !doSetupNowRunning) { // wait a bit before showing feedback Handler(Looper.getMainLooper()).postDelayed({ showFeedback() @@ -784,7 +827,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } private fun showFeedback() { - Countly.sharedInstance().feedback() + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().feedback() .getAvailableFeedbackWidgets { retrievedWidgets, error -> if (MainApplication.forceDebugLogging) Timber.i( "Got feedback widgets: %s", @@ -793,33 +836,34 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (error == null) { if (retrievedWidgets.size > 0) { val feedbackWidget = retrievedWidgets[0] - Countly.sharedInstance().feedback().presentFeedbackWidget( - feedbackWidget, - this@MainActivity, - "Close", - object : ModuleFeedback.FeedbackCallback { - override fun onClosed() { - } + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().feedback() + .presentFeedbackWidget( + feedbackWidget, + this@MainActivity, + "Close", + object : ModuleFeedback.FeedbackCallback { + override fun onClosed() { + } - // maybe show a toast when the widget is closed - override fun onFinished(error: String?) { - // error handling here - if (!error.isNullOrEmpty()) { - Toast.makeText( - this@MainActivity, - "Error: $error", - Toast.LENGTH_LONG - ).show() - Timber.e(error, "Feedback error") - } else { - Toast.makeText( - this@MainActivity, - "Feedback sent", - Toast.LENGTH_LONG - ).show() + // maybe show a toast when the widget is closed + override fun onFinished(error: String?) { + // error handling here + if (!error.isNullOrEmpty()) { + Toast.makeText( + this@MainActivity, + "Error: $error", + Toast.LENGTH_LONG + ).show() + Timber.e(error, "Feedback error") + } else { + Toast.makeText( + this@MainActivity, + "Feedback sent", + Toast.LENGTH_LONG + ).show() + } } - } - }) + }) // update last feedback time MainApplication.getPreferences("mmm")?.edit() ?.putLong("last_feedback", System.currentTimeMillis())?.apply() @@ -852,7 +896,8 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } if (MainApplication.forceDebugLogging) Timber.i("Refresh") progressIndicator!!.visibility = View.VISIBLE - progressIndicator!!.setProgressCompat(0, false) + // progress starts at 30 and ends at 80 + progressIndicator!!.setProgress(20, true) swipeRefreshBlocker = System.currentTimeMillis() + 5000L MainApplication.INSTANCE!!.repoModules.clear() @@ -863,13 +908,21 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { val updateListener: SyncManager.UpdateListener = object : SyncManager.UpdateListener { override fun update(value: Int) { runOnUiThread(if (max == 0) Runnable { - progressIndicator!!.setProgressCompat( - value, true + progressIndicator!!.setProgress( + 80, true ) } else Runnable { - progressIndicator!!.setProgressCompat( - value, true + progressIndicator!!.setProgress( + // going from 30 to 80 as evenly as possible + 30 + value, + true ) + if (BuildConfig.DEBUG) { + Timber.i( + "Progress: %d", + 30 + value + ) + } }) } } @@ -891,6 +944,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (MainApplication.forceDebugLogging) Timber.i("Check Json Update") if (max != 0) { var current = 0 + val totalLocalModules = instance!!.modules.size for (localModuleInfo in instance!!.modules.values) { if (localModuleInfo.updateJson != null && localModuleInfo.flags and ModuleInfo.FLAG_MM_REMOTE_MODULE == 0) { if (MainApplication.forceDebugLogging) Timber.i(localModuleInfo.id) @@ -902,8 +956,10 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { current++ val currentTmp = current runOnUiThread { - progressIndicator!!.setProgressCompat( - currentTmp / max, true + progressIndicator!!.setProgress( + // from 80 to 99, divided by total modules + 80 + (currentTmp / totalLocalModules.toFloat() * 20).roundToInt(), + true ) } } @@ -911,10 +967,6 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } } if (MainApplication.forceDebugLogging) Timber.i("Apply") - runOnUiThread { - progressIndicator!!.visibility = View.GONE - swipeRefreshLayout!!.isRefreshing = false - } NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder) RepoManager.getINSTANCE()!!.updateEnabledStates() RepoManager.getINSTANCE() @@ -923,6 +975,11 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { ?.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() } moduleViewListBuilder.applyTo(moduleList!!, moduleViewAdapter!!) moduleViewListBuilderOnline.applyTo(moduleListOnline!!, moduleViewAdapterOnline!!) + runOnUiThread { + progressIndicator!!.setProgress(PRECISION, true) + progressIndicator!!.visibility = View.GONE + swipeRefreshLayout!!.isRefreshing = false + } }, "Repo update thread").start() } @@ -943,15 +1000,17 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { } } // if it's still null, but it's enabled, throw an error - if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == null) { + if (AndroidacyRepoData.instance.memberLevel == null) { Timber.e("AndroidacyRepoData is enabled, but member level is null") } if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == "Guest") { runtimeUtils!!.showUpgradeSnackbar(this, this) } else { - if (!AndroidacyRepoData.instance.isEnabled) { - if (MainApplication.forceDebugLogging) Timber.i("AndroidacyRepoData is disabled, not showing upgrade snackbar 1") - } else if (AndroidacyRepoData.instance.memberLevel != "Guest") { + if (AndroidacyRepoData.instance.memberLevel == null || !AndroidacyRepoData.instance.memberLevel.equals( + "Guest", + ignoreCase = true + ) + ) { if (MainApplication.forceDebugLogging) Timber.i( "AndroidacyRepoData is not Guest, not showing upgrade snackbar 1. Level: %s", AndroidacyRepoData.instance.memberLevel @@ -960,7 +1019,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper { if (MainApplication.forceDebugLogging) Timber.i("Unknown error, not showing upgrade snackbar 1") } } - } else if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == "Guest") { + } else if (AndroidacyRepoData.instance.memberLevel.equals("Guest", ignoreCase = true)) { runtimeUtils!!.showUpgradeSnackbar(this, this) } else { if (!AndroidacyRepoData.instance.isEnabled) { diff --git a/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt b/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt index ab78365..ac729f3 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt @@ -4,12 +4,14 @@ package com.fox2code.mmm +import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application import android.app.Application.ActivityLifecycleCallbacks +import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -17,12 +19,13 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build import android.os.Bundle -import android.os.Process import android.os.SystemClock import android.util.Log import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.emoji2.text.DefaultEmojiCompatConfig import androidx.emoji2.text.EmojiCompat @@ -48,6 +51,8 @@ import ly.count.android.sdk.Countly import ly.count.android.sdk.CountlyConfig import timber.log.Timber import java.io.File +import java.io.PrintWriter +import java.io.StringWriter import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.Date @@ -202,23 +207,49 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable -> clearCachedSharedPrefs() - // open crash handler and exit + // send high importance notification with pending intent to open CrashHandler activity with stacktrace val intent = Intent(this, CrashHandler::class.java) - // pass the entire exception to the crash handler intent.putExtra("exception", throwable) - // add stacktrace as string - intent.putExtra("stacktrace", throwable.stackTrace) - // serialize Sentry.captureException and pass it to the crash handler - intent.putExtra("sentryException", throwable) - // pass crashReportingEnabled to crash handler - intent.putExtra("crashReportingEnabled", isCrashReportingEnabled) - // add isCrashing to intent intent.putExtra("isCrashing", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - Timber.e("Starting crash handler") - startActivity(intent) - Timber.e("Exiting") - Process.killProcess(Process.myPid()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + // set pref + val sharedPreferences = getPreferences("mmm") + val editor = sharedPreferences!!.edit() + editor.putBoolean("pref_crashed", true) + val stringWriter = StringWriter() + throwable.printStackTrace(PrintWriter(stringWriter)) + val stacktrace = stringWriter.toString() + editor.putString("pref_crash_stacktrace", stacktrace) + editor.apply() + val crashreportingenabled = sharedPreferences.getBoolean( + "pref_crashreportingenabled", + true + ) + // send notification + val notificationManagerCompat = NotificationManagerCompat.from(this) + val notifBody = if (crashreportingenabled) getString(R.string.crash_notification_body) else getString( + R.string.crash_notification_body_noreport + ) + val notification = NotificationCompat.Builder(this, "crash") + .setSmallIcon(R.drawable.ic_baseline_error_24) + .setContentTitle(getString(R.string.crash_notification_title)) + .setContentText(notifBody) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .build() + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationManagerCompat.notify(0, notification) + } } supportedLocales.addAll( listOf( @@ -604,7 +635,6 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle } catch (e: Exception) { // try again five times, with a 250ms delay between each try. if we still can't get the shared preferences, throw an exception var i = 0 - var s = false while (i < 5) { try { Thread.sleep(250) @@ -623,7 +653,6 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) mSharedPrefs!![name] = sharedPreferences - s = true return sharedPreferences } catch (e: Exception) { Timber.e(e, "Failed to get shared preferences") @@ -714,7 +743,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle } val isCrashReportingEnabled: Boolean - get() = getPreferences("mmm")!!.getBoolean( + get() = analyticsAllowed() && getPreferences("mmm")!!.getBoolean( "pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING ) val bootSharedPreferences: SharedPreferences? @@ -736,6 +765,9 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle fun shouldShowFeedback(): Boolean { // should not have been shown in 14 days and only 1 in 5 chance + if (!analyticsAllowed()) { + return false + } val randChance = Random().nextInt(5) val lastShown = getPreferences("mmm")!!.getLong("last_feedback", 0) if (forceDebugLogging) Timber.d( @@ -745,6 +777,8 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle ) return System.currentTimeMillis() - lastShown > 1209600000 && randChance == 0 } + + var dirty = false } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { diff --git a/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt index 673be64..ef5c9a9 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt @@ -35,6 +35,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch +import com.google.android.material.textview.MaterialTextView import com.topjohnwu.superuser.internal.UiThreadHandler import org.apache.commons.io.FileUtils import timber.log.Timber @@ -54,6 +55,9 @@ class SetupActivity : AppCompatActivity(), LanguageActivity { this.window.navigationBarColor = this.getColor(R.color.black_transparent) createFiles() disableUpdateActivityForFdroidFlavor() + if (BuildConfig.DEBUG) { + Timber.d("Starting SetupActivity") + } // Set theme val prefs = MainApplication.getPreferences("mmm")!! when (prefs.getString("theme", "system")) { @@ -155,12 +159,6 @@ class SetupActivity : AppCompatActivity(), LanguageActivity { val crashReportingPii = view.findViewById(R.id.setup_crash_reporting_pii) setupCrashReporting.isChecked = BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING - // pref_crash_reporting_pii - crashReportingPii.isChecked = - BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING_PII - // pref_analytics_enabled - analyticsEnabled.isChecked = - BuildConfig.DEFAULT_ENABLE_ANALYTICS // if analytics is disabled, force disable crash reporting if (!view.findViewById(R.id.setup_app_analytics).isChecked) { setupCrashReporting.isEnabled = false @@ -168,24 +166,31 @@ class SetupActivity : AppCompatActivity(), LanguageActivity { setupCrashReporting.isChecked = false crashReportingPii.isChecked = false } + // switch summary for setup_app_analytics_summary + val setupAppAnalyticsSummary = view.findViewById(R.id.setup_app_analytics_summary) // listen for changes to the analytics switch analyticsEnabled.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + if (BuildConfig.DEBUG) Timber.i( + "Analytics: %s", + isChecked) // if analytics is disabled, force disable crash reporting if (!isChecked) { setupCrashReporting.isChecked = false - crashReportingPii.isChecked = false setupCrashReporting.isEnabled = false - crashReportingPii.isEnabled = false } else { setupCrashReporting.isEnabled = true - crashReportingPii.isEnabled = true + setupCrashReporting.isChecked = + BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING + } + if (!isChecked) { + setupAppAnalyticsSummary.setText(R.string.analytics_disabled_desc) + } else { + setupAppAnalyticsSummary.setText(R.string.analytics_enabled_desc) } } - // assert that both switches match the build config on debug builds - if (BuildConfig.DEBUG) { - assert((Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check)) as MaterialSwitch).isChecked == BuildConfig.ENABLE_AUTO_UPDATER) - assert(setupCrashReporting.isChecked == BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING) - } + // pref_analytics_enabled + analyticsEnabled.isChecked = + BuildConfig.DEFAULT_ENABLE_ANALYTICS // Repos are a little harder, as the enabled_repos build config is an arraylist val andRepoView = Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo)) as MaterialSwitch @@ -362,7 +367,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity { reposListDao.setEnabled(androidacyRepoRoomObj.id, androidacyRepoRoom) reposListDao.setEnabled(magiskAltRepoRoomObj.id, magiskAltRepoRoom) db.close() - editor.putString("last_shown_setup", "v5") + editor.putString("last_shown_setup", "v6") // Commit the changes editor.commit() // Log the changes @@ -395,6 +400,8 @@ class SetupActivity : AppCompatActivity(), LanguageActivity { // close the app finish() } + // log finish + if (MainApplication.forceDebugLogging) Timber.d("SetupActivity finished oncreate") } override fun getTheme(): Theme { diff --git a/app/src/main/kotlin/com/fox2code/mmm/androidacy/AndroidacyRepoData.kt b/app/src/main/kotlin/com/fox2code/mmm/androidacy/AndroidacyRepoData.kt index d4cd9b5..2b8b383 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/androidacy/AndroidacyRepoData.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/androidacy/AndroidacyRepoData.kt @@ -478,8 +478,22 @@ class AndroidacyRepoData(cacheRoot: File?, testMode: Boolean) : RepoData( OK_HTTP_URL_BUILDER.build() } + private var realInstance: AndroidacyRepoData? = null + get() { + if (field === null) { + field = AndroidacyRepoData(INSTANCE!!.cacheDir, false) + } + return field + } + val instance: AndroidacyRepoData - get() = RepoManager.getINSTANCE()!!.androidacyRepoData!! + get() { + return if (RepoManager.getINSTANCE()!!.androidacyRepoData !== null) { + RepoManager.getINSTANCE()!!.androidacyRepoData!! + } else { + realInstance!! + } + } private fun filterURL(url: String?): String? { return if (url.isNullOrEmpty() || isInvalidURL(url)) { diff --git a/app/src/main/kotlin/com/fox2code/mmm/background/BackgroundUpdateChecker.kt b/app/src/main/kotlin/com/fox2code/mmm/background/BackgroundUpdateChecker.kt index cae319e..9045572 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/background/BackgroundUpdateChecker.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/background/BackgroundUpdateChecker.kt @@ -378,7 +378,7 @@ class BackgroundUpdateChecker(context: Context, workerParams: WorkerParameters) fun onMainActivityCreate(context: Context) { // Refuse to run if first_launch pref is not false if (MainApplication.getPreferences("mmm")!! - .getString("last_shown_setup", null) != "v5" + .getString("last_shown_setup", null) != "v6" ) return // create notification channel group val groupName: CharSequence = context.getString(R.string.notification_group_updates) diff --git a/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerActivity.kt index 8ae9c32..1d08314 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerActivity.kt @@ -589,6 +589,8 @@ class InstallerActivity : AppCompatActivity() { if (suFile.exists() && !suFile.delete()) Timber.w("Failed to delete zip file") else toDelete = null } else toDelete = null + // set dirty in mainapp + MainApplication.dirty = true runOnUiThread { this.window.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 0) // release wakelock diff --git a/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerInitializer.kt b/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerInitializer.kt index 9536066..4a2de9a 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerInitializer.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/installer/InstallerInitializer.kt @@ -75,7 +75,6 @@ class InstallerInitializer { } fun tryGetMagiskPathAsync(callback: Callback, forceCheck: Boolean = false) { - val mgskPth = mgskPth val thread: Thread = object : Thread("Magisk GetPath Thread") { override fun run() { if (mgskPth != null && !forceCheck) { @@ -83,7 +82,6 @@ class InstallerInitializer { return } var error: Int - @Suppress("NAME_SHADOWING") var mgskPth: String? = null try { mgskPth = tryGetMagiskPath(forceCheck) error = ERROR_NO_PATH @@ -95,12 +93,14 @@ class InstallerInitializer { Timber.e(e) } if (forceCheck) { - Companion.mgskPth = mgskPth if (mgskPth == null) { mgskVerCode = 0 } } if (mgskPth != null) { + if (MainApplication.forceDebugLogging) { + Timber.i("Magisk path async: %s", mgskPth) + } MainApplication.setHasGottenRootAccess(true) callback.onPathReceived(mgskPth) } else { diff --git a/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt b/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt index ae597da..c2be80a 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt @@ -29,9 +29,9 @@ class ModuleManager private constructor() : SyncManager() { private var updatableModuleCount = 0 override fun scanInternal(updateListener: UpdateListener) { - // if last_shown_setup is not "v5", then refuse to continue + // if last_shown_setup is not "v6", then refuse to continue if (MainApplication.getPreferences("mmm")!! - .getString("last_shown_setup", "") != "v5" + .getString("last_shown_setup", "") != "v6" ) { return } diff --git a/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt b/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt index bc357ae..9773da3 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt @@ -50,9 +50,10 @@ enum class ActionButtonType { } // if analytics is enabled, track the event if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_description", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_description", HashMap().apply { + put("module", name ?: "null") + }) } val notesUrl = moduleHolder.repoModule?.notesUrl if (isAndroidacyLink(notesUrl)) { @@ -124,9 +125,12 @@ enum class ActionButtonType { } override fun doAction(button: Chip, moduleHolder: ModuleHolder) { - if (MainApplication.getPreferences("mmm")?.getBoolean("pref_require_security", false) == true) { + if (MainApplication.getPreferences("mmm") + ?.getBoolean("pref_require_security", false) == true + ) { // get safe status from either mainmoduleinfo or repo module - val safe = moduleHolder.mainModuleInfo.safe || moduleHolder.repoModule?.moduleInfo?.safe ?: false + val safe = + moduleHolder.mainModuleInfo.safe || moduleHolder.repoModule?.moduleInfo?.safe ?: false if (!safe) { // block local install for safety MaterialAlertDialogBuilder(button.context) @@ -149,26 +153,30 @@ enum class ActionButtonType { moduleHolder.repoModule?.moduleInfo?.name } // send event to countly - Countly.sharedInstance().events().recordEvent("view_update_install", HashMap().apply { - put("module", name ?: "null") - }) + if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events() + .recordEvent("view_update_install", HashMap().apply { + put("module", name ?: "null") + }) // if text is reinstall, we need to uninstall first - warn the user but don't proceed - if (moduleHolder.moduleInfo != null) { - // get the text - val text = button.text - // if the text is reinstall, warn the user - if (text == button.context.getString(R.string.reinstall)) { - val builder = MaterialAlertDialogBuilder(button.context) - builder.setTitle(R.string.reinstall) - .setMessage(R.string.reinstall_warning) - .setCancelable(true) - // ok button that does nothing - .setPositiveButton(R.string.ok, null) - .show() - return - } + if (moduleHolder.moduleInfo != null && moduleHolder.repoModule == null && button.text == button.context.getString(R.string.reinstall)) { + val builder = MaterialAlertDialogBuilder(button.context) + builder.setTitle(R.string.reinstall) + .setMessage(R.string.reinstall_warning_v2) + .setCancelable(true) + // ok button that does nothing + .setPositiveButton(R.string.ok, null) + .show() + return + } + // prefer repomodule if possible + var updateZipUrl = "" + if (moduleHolder.repoModule != null && moduleHolder.repoModule!!.zipUrl != null) { + updateZipUrl = moduleHolder.repoModule!!.zipUrl!! + } + // if repomodule is null, try localmoduleinfo + if (updateZipUrl.isEmpty() && moduleHolder.moduleInfo != null && moduleHolder.moduleInfo!!.updateZipUrl != null) { + updateZipUrl = moduleHolder.moduleInfo!!.updateZipUrl!! } - val updateZipUrl = moduleHolder.updateZipUrl ?: return // Androidacy manage the selection between download and install if (isAndroidacyLink(updateZipUrl)) { openUrlAndroidacy( @@ -268,11 +276,16 @@ enum class ActionButtonType { } // if analytics is enabled, track the event if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_uninstall", HashMap().apply { - put("module", name ?: "null") - }) - } - if (MainApplication.forceDebugLogging) Timber.i(Integer.toHexString(moduleHolder.moduleInfo?.flags ?: 0)) + Countly.sharedInstance().events() + .recordEvent("view_uninstall", HashMap().apply { + put("module", name ?: "null") + }) + } + if (MainApplication.forceDebugLogging) Timber.i( + Integer.toHexString( + moduleHolder.moduleInfo?.flags ?: 0 + ) + ) if (!instance!!.setUninstallState( moduleHolder.moduleInfo!!, !moduleHolder.hasFlag( ModuleInfo.FLAG_MODULE_UNINSTALLING @@ -327,9 +340,10 @@ enum class ActionButtonType { moduleHolder.repoModule?.moduleInfo?.name } if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_config", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_config", HashMap().apply { + put("module", name ?: "null") + }) } if (isAndroidacyLink(config)) { openUrlAndroidacy(button.context, config, true) @@ -354,9 +368,10 @@ enum class ActionButtonType { moduleHolder.repoModule?.moduleInfo?.name } if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_support", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_support", HashMap().apply { + put("module", name ?: "null") + }) } openUrl(button.context, Objects.requireNonNull(moduleHolder.mainModuleInfo.support)) } @@ -376,10 +391,11 @@ enum class ActionButtonType { } else { moduleHolder.repoModule?.moduleInfo?.name } -if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_donate", HashMap().apply { - put("module", name ?: "null") - }) + if (MainApplication.analyticsAllowed()) { + Countly.sharedInstance().events() + .recordEvent("view_donate", HashMap().apply { + put("module", name ?: "null") + }) } openUrl(button.context, moduleHolder.mainModuleInfo.donate) } @@ -397,14 +413,15 @@ if (MainApplication.analyticsAllowed()) { moduleHolder.repoModule?.moduleInfo?.name } if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_warning", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_warning", HashMap().apply { + put("module", name ?: "null") + }) } MaterialAlertDialogBuilder(button.context).setTitle(R.string.warning) .setMessage(R.string.warning_message).setPositiveButton( - R.string.understand - ) { _: DialogInterface?, _: Int -> } + R.string.understand + ) { _: DialogInterface?, _: Int -> } .create().show() } }, @@ -423,21 +440,25 @@ if (MainApplication.analyticsAllowed()) { moduleHolder.repoModule?.moduleInfo?.name } if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_safe", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_safe", HashMap().apply { + put("module", name ?: "null") + }) } MaterialAlertDialogBuilder(button.context).setTitle(R.string.safe_module) .setMessage(R.string.safe_message).setPositiveButton( - R.string.understand - ) { _: DialogInterface?, _: Int -> } + R.string.understand + ) { _: DialogInterface?, _: Int -> } .create().show() } }, REMOTE { @Suppress("NAME_SHADOWING") override fun doAction(button: Chip, moduleHolder: ModuleHolder) { - if (MainApplication.forceDebugLogging) Timber.d("doAction: remote module for %s", moduleHolder.moduleInfo?.name ?: "null") + if (MainApplication.forceDebugLogging) Timber.d( + "doAction: remote module for %s", + moduleHolder.moduleInfo?.name ?: "null" + ) // that module is from remote repo val name: String? = if (moduleHolder.moduleInfo != null) { moduleHolder.moduleInfo!!.name @@ -446,9 +467,10 @@ if (MainApplication.analyticsAllowed()) { } // positive button executes install logic and says reinstall. negative button does nothing if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_update_install", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_update_install", HashMap().apply { + put("module", name ?: "null") + }) } val madb = MaterialAlertDialogBuilder(button.context) madb.setTitle(R.string.remote_module) @@ -491,21 +513,35 @@ if (MainApplication.analyticsAllowed()) { } } if (!updateZipUrl.isNullOrEmpty()) { - madb.setMessage(Html.fromHtml(button.context.getString(R.string.remote_message, name), Html.FROM_HTML_MODE_COMPACT)) + madb.setMessage( + Html.fromHtml( + button.context.getString( + R.string.remote_message, + name + ), Html.FROM_HTML_MODE_COMPACT + ) + ) madb.setPositiveButton( R.string.reinstall ) { _: DialogInterface?, _: Int -> - if (MainApplication.forceDebugLogging) Timber.d("Set moduleinfo to %s", moduleInfo.name) + if (MainApplication.forceDebugLogging) Timber.d( + "Set moduleinfo to %s", + moduleInfo.name + ) val name: String? = if (moduleHolder.moduleInfo != null) { moduleHolder.moduleInfo!!.name } else { moduleHolder.repoModule?.moduleInfo?.name } - if (MainApplication.forceDebugLogging) Timber.d("doAction: remote module for %s", name) + if (MainApplication.forceDebugLogging) Timber.d( + "doAction: remote module for %s", + name + ) if (MainApplication.analyticsAllowed()) { - Countly.sharedInstance().events().recordEvent("view_update_install", HashMap().apply { - put("module", name ?: "null") - }) + Countly.sharedInstance().events() + .recordEvent("view_update_install", HashMap().apply { + put("module", name ?: "null") + }) } // Androidacy manage the selection between download and install if (isAndroidacyLink(updateZipUrl)) { diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt index 96f9405..c24aaa0 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt @@ -29,7 +29,7 @@ class CustomRepoManager internal constructor( init { repoCount = 0 // refuse to load if setup is not complete - if (getPreferences("mmm")!!.getString("last_shown_setup", "") == "v5") { + if (getPreferences("mmm")!!.getString("last_shown_setup", "") == "v6") { val i = 0 val lastFilled = intArrayOf(0) // now the same as above but for room database diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoManager.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoManager.kt index be96e71..8ced67e 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoManager.kt @@ -31,6 +31,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import timber.log.Timber import java.io.File import java.nio.charset.StandardCharsets +import kotlin.math.roundToInt @Suppress("NAME_SHADOWING") class RepoManager private constructor(mainApplication: MainApplication) : SyncManager() { @@ -55,7 +56,7 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa repoData = LinkedHashMap() modules = HashMap() // refuse to load if setup is not complete - if (getPreferences("mmm")!!.getString("last_shown_setup", "") == "v5") { + if (getPreferences("mmm")!!.getString("last_shown_setup", "") == "v6") { // We do not have repo list config yet. androidacyRepoData = addAndroidacyRepoData() val altRepo = addRepoData(MAGISK_ALT_REPO, "Magisk Modules Alt Repo") @@ -81,8 +82,8 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa } private fun populateDefaultCache(repoData: RepoData?) { - // if last_shown_setup is not "v5", them=n refuse to continue - if (getPreferences("mmm")!!.getString("last_shown_setup", "") != "v5") { + // if last_shown_setup is not "v6", them=n refuse to continue + if (getPreferences("mmm")!!.getString("last_shown_setup", "") != "v6") { return } // make sure repodata is not null @@ -146,15 +147,16 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa val repoUpdaters = arrayOfNulls(repoDatas.size) var moduleToUpdate = 0 if (!this.hasConnectivity()) { - updateListener.update(STEP3) + updateListener.update(50) return } for (i in repoDatas.indices) { - updateListener.update(STEP1 * (i / repoDatas.size)) + // we have 50% to work with, so divvy it up. getting ready to update should get 40% of the 50% we have + updateListener.update(50 * ((i / repoDatas.size) / 3)) if (MainApplication.forceDebugLogging) Timber.d("Preparing to fetch: %s", repoDatas[i].name) moduleToUpdate += RepoUpdater(repoDatas[i]).also { repoUpdaters[i] = it }.fetchIndex() // divvy the 40 of step1 to each repo - updateListener.update(STEP1 * ((i + 1) / repoDatas.size)) + updateListener.update(50 * ((i / repoDatas.size) / 2)) } if (MainApplication.forceDebugLogging) Timber.d("Updating meta-data") var updatedModules = 0 @@ -208,9 +210,12 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa Timber.e(e) } updatedModules++ - val repoProgressIncrement = STEP2 / repoDatas.size.toDouble() + val repoProgressIncrement = 50 / repoDatas.size.toDouble() val moduleProgressIncrement = repoProgressIncrement / repoModules.size.toDouble() - updateListener.update((STEP1 + moduleProgressIncrement * updatedModules).toInt()) + // we've already used half of our 50 steps, so we have 25 left. divide out the 25 by the number of modules we have to update and round to the nearest int + updateListener.update( + (50 + (repoProgressIncrement * i) + (moduleProgressIncrement * updatedModules).toInt()).roundToInt() + ) } for (repoModule in repoUpdaters[i]!!.toApply()!!) { if (repoModule.moduleInfo.flags and ModuleInfo.FLAG_METADATA_INVALID == 0) { @@ -276,11 +281,11 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa } repoLastErrorName = repoUpdaters[i]!!.repoData.name } - updateListener.update(STEP1 + (STEP2 * (i / repoUpdaters.size))) + updateListener.update(50 + (50 / repoDatas.size.toDouble() * i).roundToInt()) } } if (MainApplication.forceDebugLogging) Timber.i("Got " + modules.size + " modules!") - updateListener.update(STEP1 + STEP2 + STEP3) + updateListener.update(50) } fun updateEnabledStates() { @@ -350,9 +355,9 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa private const val MAGISK_REPO_MANAGER = "https://magisk-modules-repo.github.io/submission/modules.json" private val lock = Any() - private const val STEP1 = 20 - private const val STEP2 = 60 - private const val STEP3 = 20 + private const val STEP1 = 30 + private const val STEP2 = 80 + private const val STEP3 = 99 @Volatile private var INSTANCE: RepoManager? = null diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt index 1e23cca..5a6120d 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt @@ -10,7 +10,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import androidx.preference.TwoStatePreference +import androidx.preference.SwitchPreferenceCompat import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.fox2code.mmm.MainActivity @@ -50,7 +50,7 @@ class PrivacyFragment : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.privacy_preferences, rootKey) // Crash reporting val crashReportingPreference = - findPreference("pref_crash_reporting") + findPreference("pref_crash_reporting") crashReportingPreference!!.isChecked = MainApplication.isCrashReportingEnabled val initialValue: Any = MainApplication.isCrashReportingEnabled crashReportingPreference.onPreferenceChangeListener = @@ -78,18 +78,51 @@ class PrivacyFragment : PreferenceFragmentCompat() { if (MainApplication.forceDebugLogging) Timber.d("Restarting app to save crash reporting preference: %s", newValue) exitProcess(0) // Exit app process } - // Do not reverse the change if the user cancels the dialog - materialAlertDialogBuilder.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> } + // reverse the change if the user cancels the dialog + materialAlertDialogBuilder.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> + crashReportingPreference.isChecked = initialValue as Boolean + } materialAlertDialogBuilder.show() true } // on pref_analytics_enabled change, update pref_crash_reporting (switch must be off and disabled if analytics is off) - val analyticsPreference = findPreference("pref_analytics_enabled") + val analyticsPreference = findPreference("pref_analytics_enabled") analyticsPreference!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - if (initialValue === newValue) return@OnPreferenceChangeListener true - crashReportingPreference.isEnabled = newValue as Boolean - if (!newValue) crashReportingPreference.isChecked = false + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + @Suppress("NAME_SHADOWING") val newValue = newValue as Boolean + crashReportingPreference.isEnabled = newValue + if (!newValue) { + crashReportingPreference.isChecked = false + crashReportingPreference.isEnabled = false + } + // restart dialog + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + materialAlertDialogBuilder.setTitle(R.string.crash_reporting_restart_title) + materialAlertDialogBuilder.setMessage(R.string.crash_reporting_restart_message) + materialAlertDialogBuilder.setPositiveButton(R.string.restart) { _: DialogInterface?, _: Int -> + val mStartActivity = Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(AppCompatActivity.ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = mPendingIntent + if (MainApplication.forceDebugLogging) Timber.d("Restarting app to save analytics preference: %s", newValue) + exitProcess(0) // Exit app process + } + // reverse the change if the user cancels the dialog + materialAlertDialogBuilder.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> + analyticsPreference.isChecked = initialValue as Boolean + crashReportingPreference.isEnabled = initialValue + } + materialAlertDialogBuilder.show() true } // now, disable pref_crash_reporting if analytics is off diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt index 7ea9257..a0ce997 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt @@ -21,6 +21,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.CrashHandler import com.fox2code.mmm.ExpiredActivity import com.fox2code.mmm.MainActivity import com.fox2code.mmm.MainApplication @@ -75,6 +76,15 @@ class SettingsActivity : AppCompatActivity(), LanguageActivity, override fun onCreate(savedInstanceState: Bundle?) { devModeStep = 0 super.onCreate(savedInstanceState) + + // check for pref_crashed and if so start crash handler + val sharedPreferences = MainApplication.getPreferences("mmm") + if (sharedPreferences?.getBoolean("pref_crashed", false) == true) { + val intent = Intent(this, CrashHandler::class.java) + startActivity(intent) + finish() + return + } // get the active tab from the intent activeTabFromIntent = intent.getStringExtra("activeTab") ?: "installed" PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { preferenceFragmentCompat: PreferenceFragmentCompat, preference: Preference -> diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/RuntimeUtils.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/RuntimeUtils.kt index 3c176aa..03b8547 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/RuntimeUtils.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/RuntimeUtils.kt @@ -162,7 +162,7 @@ class RuntimeUtils { if (MainApplication.forceDebugLogging) Timber.i("Checking if we need to run setup") // Check if context is the first launch using prefs and if doSetupRestarting was passed in the intent val prefs = MainApplication.getPreferences("mmm")!! - var firstLaunch = prefs.getString("last_shown_setup", null) != "v5" + var firstLaunch = prefs.getString("last_shown_setup", null) != "v6" // First launch // context is intentionally separate from the above if statement, because it needs to be checked even if the first launch check is true due to some weird edge cases if (activity.intent.getBooleanExtra("doSetupRestarting", false)) { @@ -258,20 +258,17 @@ class RuntimeUtils { val prefs = MainApplication.getPreferences("mmm")!! // if last shown < 7 days ago if (prefs.getLong("ugsns4", 0) > System.currentTimeMillis() - 604800000) return - val snackbar: Snackbar = Snackbar.make( - context, - activity.findViewById(R.id.blur_frame), - activity.getString(R.string.upgrade_snackbar), - 7000 - ) - snackbar.setAction(R.string.upgrade_now) { + // rewrite that to use a material alert dialog + val builder = MaterialAlertDialogBuilder(context) + builder.setTitle(R.string.upgrade_now) + builder.setMessage(R.string.upgrade_dialog_message) + 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") activity.startActivity(intent) + dialog.dismiss() } - snackbar.setAnchorView(R.id.bottom_navigation) - snackbar.show() // do not show for another 7 days prefs.edit().putLong("ugsns4", System.currentTimeMillis()).apply() if (MainApplication.forceDebugLogging) Timber.i("showUpgradeSnackbar done") diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/io/net/Http.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/io/net/Http.kt index 6996499..76bc202 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/io/net/Http.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/io/net/Http.kt @@ -467,7 +467,7 @@ enum class Http {; url ).get().build() ).execute() - } catch (e: IOException) { + } catch (e: Exception) { Timber.e(e, "Failed to get %s", url) // detect ssl errors, i.e., cert authority invalid by looking at the message if (e.message != null && e.message!!.contains("_CERT_")) { @@ -478,6 +478,7 @@ enum class Http {; ).show() } } + // check if retrying is allowed throw HttpException(e.message, 0) } if (BuildConfig.DEBUG_HTTP) { @@ -805,7 +806,7 @@ enum class Http {; val respString = String(resp) // resp should include that scheme is https and h is production-api.androidacy.com respString.contains("scheme=https") && respString.contains("h=production-api.androidacy.com") - } catch (e: HttpException) { + } catch (e: Exception) { Timber.e(e, "Failed to check internet connection") false } diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index 48057d9..67d54a8 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -196,7 +196,7 @@ android:layout_margin="5dp" android:checked="false" android:key="pref_crash_reporting_enabled" - android:text="@string/setup_crash_reporting" + android:text="@string/setup_crash_reporting_generic" android:textAppearance="@android:style/TextAppearance.Material.Subhead" android:textSize="18sp" /> @@ -215,6 +215,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" + android:visibility="gone" android:checked="false" android:key="pref_crash_reporting_pii" android:text="@string/setup_crash_reporting_pii" @@ -227,6 +228,7 @@ android:layout_marginBottom="4dp" android:drawableStart="@drawable/ic_baseline_info_24" android:drawablePadding="8dp" + android:visibility="gone" android:text="@string/setup_crash_reporting_pii_summary" android:textAppearance="@android:style/TextAppearance.Material.Small" /> @@ -244,6 +246,7 @@ android:visibility="visible" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bcd6e5e..32e5ccf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -327,6 +327,7 @@ An error occurred reading shared preferences. Please reset the app. An app restart is required to enable showcase mode. You agree to be bound by the LGPL-3.0 (https://www.gnu.org/licenses/lgpl-3.0.en.html) license and the EULA (https://www.androidacy.com/foxmmm-eula/), in addition to any third party terms, and that the authors of this app bear no responsibility of your usage of it, nor do we offer any warranties express or implied. + By using this app, you agree to the EULA (https://www.androidacy.com/foxmmm-eula/) and the Androidacy Terms of Service (https://www.androidacy.com/terms-of-service/). You understand your data will be processed in accordance with the Androidacy Privacy Policy (https://www.androidacy.com/privacy-policy/). The source code of this app is available under the LGPL-3.0 (https://www.gnu.org/licenses/lgpl-3.0.en.html) license. Allow us to track app usage and installs. Fully GDPR compliant and uses Countly, hosted by Androidacy. Debugging News and updates @@ -412,4 +413,15 @@ Failed to download! Finished downloading and saved to downloads folder The file you picked is not a valid zip file. + AMM crashed! + AMM has encountered an error and has crashed. %s + AMM has crashed. The developers have been notified. Tap here to see details or restart the app. + AMM has crashed. If you keep seeing this, please report a bug. If you enabled crash reporting in settings, you can have this done for you. + You have disabled crash reporting. This may make it harder for us to find bugs and fix crashes. + Crash reporting is on, and the app will automatically send a report when it crashes or freezes. + You have analytics off. This may make it harder to improve and develop the app. If you enable this setting, no personal info will be sent! + You\'ve opted into analytics. We will use this data to develop and improve the app. No personal info is sent. + Report crashes + Purchasing a premium subscription helps support this app and our other services, and unlocks cool features like ad removal, fast unlimited downloads, and so much more! Plans start at $3.49 USD a month (subject to change). Cancel anytime.\n\nNote: The core features of this app remain free but may be limited. + This module is available from an online source and a local one. For security, you\'ll need to head over to the online tab to update. If there\'s no update available, you may need to remove this module before attemtping to reinstall. diff --git a/app/src/main/res/xml/privacy_preferences.xml b/app/src/main/res/xml/privacy_preferences.xml index 27a137d..b1c805a 100644 --- a/app/src/main/res/xml/privacy_preferences.xml +++ b/app/src/main/res/xml/privacy_preferences.xml @@ -11,12 +11,14 @@ app:icon="@drawable/ic_baseline_bug_report_24" app:key="pref_crash_reporting" app:singleLineTitle="false" - app:summary="@string/crash_reporting_desc" + app:summaryOff="@string/crash_reporting_disabled_desc" + app:summaryOn="@string/crash_reporting_enabled_desc" app:title="@string/crash_reporting" /> diff --git a/build.gradle.kts b/build.gradle.kts index 060e13e..45b875e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath("com.android.tools.build:gradle:8.1.1") + classpath("com.android.tools.build:gradle:8.1.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.8.3") }