Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 3 years ago
parent d64efa6b58
commit f6da136bf0

@ -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

@ -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<MaterialTextView>(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<View>(R.id.reset).setOnClickListener { _: View? ->
@ -60,6 +66,16 @@ class CrashHandler : AppCompatActivity() {
builder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
builder.show()
}
// restart button
findViewById<View>(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) {

@ -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<String, String>()
repoMap["repos"] = enabledRepos.toString()
Countly.sharedInstance().events().recordEvent(
"enabled_repos",
repoMap as Map<String, Any>?, 1
)
if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events()
.recordEvent(
"enabled_repos",
repoMap as Map<String, Any>?, 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<String, String>().apply {
put("query", query)
} as Map<String, Any>?, 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<String, String>().apply {
put("query", query)
} as Map<String, Any>?, 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) {

@ -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?) {

@ -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<MaterialSwitch>(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<MaterialSwitch>(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<MaterialTextView>(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<Any>(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<Any>(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 {

@ -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)) {

@ -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)

@ -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

@ -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 {

@ -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
}

@ -50,9 +50,10 @@ enum class ActionButtonType {
}
// if analytics is enabled, track the event
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_description", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_description", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
if (MainApplication.analyticsAllowed()) Countly.sharedInstance().events()
.recordEvent("view_update_install", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
}
if (MainApplication.forceDebugLogging) Timber.i(Integer.toHexString(moduleHolder.moduleInfo?.flags ?: 0))
Countly.sharedInstance().events()
.recordEvent("view_uninstall", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_config", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_support", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events()
.recordEvent("view_donate", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_warning", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_safe", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_update_install", HashMap<String, Any>().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<String, Any>().apply {
put("module", name ?: "null")
})
Countly.sharedInstance().events()
.recordEvent("view_update_install", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
// Androidacy manage the selection between download and install
if (isAndroidacyLink(updateZipUrl)) {

@ -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

@ -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<RepoUpdater>(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

@ -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<TwoStatePreference>("pref_crash_reporting")
findPreference<SwitchPreferenceCompat>("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<TwoStatePreference>("pref_analytics_enabled")
val analyticsPreference = findPreference<SwitchPreferenceCompat>("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

@ -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 ->

@ -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")

@ -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
}

@ -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" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/setup_app_analytics_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
@ -318,6 +321,7 @@
android:layout_marginHorizontal="2dp"
android:layout_marginVertical="4dp"
android:checked="false"
android:autoLink="web"
android:text="@string/eula_agree_v2"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" />

@ -327,6 +327,7 @@
<string name="error_encrypted_shared_preferences">An error occurred reading shared preferences. Please reset the app.</string>
<string name="showcase_mode_dialogue_message">An app restart is required to enable showcase mode.</string>
<string name="eula_agree_v2">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.</string>
<string name="eula_agree_v3">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.</string>
<string name="analytics_desc">Allow us to track app usage and installs. Fully GDPR compliant and uses Countly, hosted by Androidacy.</string>
<string name="debug_cat">Debugging</string>
<string name="announcements">News and updates</string>
@ -412,4 +413,15 @@
<string name="download_failed">Failed to download!</string>
<string name="download_finished">Finished downloading and saved to downloads folder</string>
<string name="file_picker_not_zip">The file you picked is not a valid zip file.</string>
<string name="crash_notification_title">AMM crashed!</string>
<string name="crash_notification_text">AMM has encountered an error and has crashed. %s</string>
<string name="crash_notification_body">AMM has crashed. The developers have been notified. Tap here to see details or restart the app.</string>
<string name="crash_notification_body_noreport">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.</string>
<string name="crash_reporting_disabled_desc">You have disabled crash reporting. This may make it harder for us to find bugs and fix crashes.</string>
<string name="crash_reporting_enabled_desc">Crash reporting is on, and the app will automatically send a report when it crashes or freezes.</string>
<string name="analytics_disabled_desc">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!</string>
<string name="analytics_enabled_desc">You\'ve opted into analytics. We will use this data to develop and improve the app. No personal info is sent.</string>
<string name="setup_crash_reporting_generic">Report crashes</string>
<string name="upgrade_dialog_message">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\n<i>Note: The core features of this app remain free but may be limited.</i></string>
<string name="reinstall_warning_v2">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.</string>
</resources>

@ -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" />
<!-- allow pii in crash reports -->
<SwitchPreferenceCompat
android:widgetLayout="@layout/preference_material_switch"
app:defaultValue="false"
app:isPreferenceVisible="false"
app:dependency="pref_crash_reporting"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting_pii"
@ -30,7 +32,8 @@
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_analytics_enabled"
app:singleLineTitle="false"
app:summary="@string/analytics_desc"
app:summaryOff="@string/analytics_disabled_desc"
app:summaryOn="@string/analytics_enabled_desc"
app:title="@string/setup_app_analytics" />
</PreferenceCategory>

@ -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")
}

Loading…
Cancel
Save