misc fixes and upgrades

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 2 years ago
parent c4ea88c927
commit 6d22c3c885

@ -62,33 +62,33 @@ jobs:
# UPLOAD ARTIFACT SECTION
# Will be shorter, when https://github.com/actions/upload-artifact/pull/354 will be merged
# FoxMMM-default-debug
- name: Upload FoxMMM-default-arm64-v8a-debug
# AMMM-default-debug
- name: Upload AMMM-default-arm64-v8a-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-arm64-v8a-debug
name: AMMM-default-arm64-v8a-debug
path: app/build/outputs/apk/default/debug/*-default-arm64-v8a-debug.apk
- name: Upload FoxMMM-default-armeabi-v7a-debug
- name: Upload AMMM-default-armeabi-v7a-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-armeabi-v7a-debug
name: AMMM-default-armeabi-v7a-debug
path: app/build/outputs/apk/default/debug/*-default-armeabi-v7a-debug.apk
- name: Upload FoxMMM-default-universal-debug
- name: Upload AMMM-default-universal-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-universal-debug
name: AMMM-default-universal-debug
path: app/build/outputs/apk/default/debug/*-default-universal-debug.apk
- name: Upload FoxMMM-default-x86-debug
- name: Upload AMMM-default-x86-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-x86-debug
name: AMMM-default-x86-debug
path: app/build/outputs/apk/default/debug/*-default-x86-debug.apk
- name: Upload FoxMMM-default-x86_64-debug
- name: Upload AMMM-default-x86_64-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-x86_64-debug
name: AMMM-default-x86_64-debug
path: app/build/outputs/apk/default/debug/*-default-x86_64-debug.apk

@ -5,7 +5,6 @@
@file:Suppress("UnstableApiUsage", "SpellCheckingInspection")
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel
import java.util.Properties
plugins {
@ -14,11 +13,8 @@ plugins {
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
id("com.google.devtools.ksp") version "1.8.22-1.0.11"
id("io.sentry.android.gradle") version "3.12.0"
id("com.google.devtools.ksp") version "1.9.10-1.0.13"
}
val hasSentryConfig = File(rootProject.projectDir, "sentry.properties").exists()
android {
// functions to get git info: gitCommitHash, gitBranch, gitRemote
val gitCommitHash = providers.exec {
@ -52,8 +48,8 @@ android {
applicationId = "com.fox2code.mmm"
minSdk = 26
targetSdk = 34
versionCode = 85
versionName = "2.3.1"
versionCode = 86
versionName = "2.3.2"
vectorDrawables {
useSupportLibrary = true
}
@ -85,10 +81,6 @@ android {
"en"
)
)
// ksp room processor
room {
schemaLocationDir.set(file("roomSchemas"))
}
}
splits {
@ -159,39 +151,23 @@ android {
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "true")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Get the androidacy client ID from the androidacy.properties
val propertiesA = Properties()
// If androidacy.properties doesn"t exist, use the default client ID which is heavily
// rate limited to 30 requests per minute
val default = "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
if (project.rootProject.file("androidacy.properties").exists()) {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
properties.setProperty(
"client_id", "\"" + propertiesA.getProperty(
propertiesA.setProperty(
"client_id", propertiesA.getProperty(
"client_id",
"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
) + "\""
default
)
)
} else {
properties.setProperty(
"client_id", "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
)
propertiesA.setProperty("client_id", "\"" + default + "\"")
}
buildConfigField(
"String", "ANDROIDACY_CLIENT_ID", "\"" + propertiesA.getProperty("client_id") + "\""
)
@ -228,38 +204,21 @@ android {
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "true")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Get the androidacy client ID from the androidacy.properties
val propertiesA = Properties()
// If androidacy.properties doesn"t exist, use the default client ID which is heavily
// rate limited to 30 requests per minute
val default = "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
if (project.rootProject.file("androidacy.properties").exists()) {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
properties.setProperty(
"client_id", "\"" + propertiesA.getProperty(
"play_client_id",
"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
) + "\""
propertiesA.setProperty(
"client_id", propertiesA.getProperty(
"client_id",
default
)
)
} else {
properties.setProperty(
"client_id", "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
)
propertiesA.setProperty("client_id", "\"" + default + "\"")
}
buildConfigField(
"String", "ANDROIDACY_CLIENT_ID", "\"" + propertiesA.getProperty("client_id") + "\""
@ -300,20 +259,6 @@ android {
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "false")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "false")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "false")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Repo with ads or tracking feature are disabled by default for the
@ -355,44 +300,6 @@ android {
}
}
sentry {
includeProguardMapping.set(true)
autoUploadProguardMapping.set(hasSentryConfig)
experimentalGuardsquareSupport.set(true)
uploadNativeSymbols.set(hasSentryConfig)
includeNativeSources.set(hasSentryConfig)
tracingInstrumentation {
enabled.set(true)
logcat {
enabled.set(true)
minLevel.set(LogcatLevel.WARNING)
}
}
autoInstallation {
enabled.set(true)
}
includeDependenciesReport.set(true)
includeSourceContext.set(hasSentryConfig)
// Includes additional source directories into the source bundle.
// These directories are resolved relative to the project directory.
additionalSourceDirsForSourceContext.set(setOf("src/main/java", "src/main/kotlin"))
org.set("androidacy")
projectName.set("foxmmm")
uploadNativeSymbols.set(hasSentryConfig)
}
val abiCodes = mapOf("armeabi-v7a" to 1, "x86" to 2, "x86_64" to 3, "arm64-v8a" to 4)
// For per-density APKs, create a similar map:
@ -452,11 +359,7 @@ dependencies {
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.webkit:webkit:1.8.0")
implementation("com.google.android.material:material:1.9.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("com.github.KieronQuinn:MonetCompat:0.4.1")
implementation("com.github.Fox2Code.FoxCompat:foxcompat:1.2.14")
implementation("com.github.Fox2Code.FoxCompat:hiddenapis:1.2.14")
implementation("com.mikepenz:aboutlibraries:10.8.3")
// Utils
@ -481,9 +384,6 @@ dependencies {
implementation("com.github.Fox2Code:RosettaX:1.0.9")
implementation("com.github.Fox2Code:AndroidANSI:1.2.1")
// sentry
implementation("io.sentry:sentry-android:6.29.0")
// Markdown
// TODO: switch to an updated implementation
implementation("io.noties.markwon:core:4.6.2")
@ -493,10 +393,6 @@ dependencies {
implementation("com.google.net.cronet:cronet-okhttp:0.1.0")
implementation("com.caverock:androidsvg:1.4")
implementation("dev.rikka.rikkax.core:core:1.4.1")
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
implementation("com.github.tiann:FreeReflection:3.1.0")
implementation("androidx.core:core-ktx:1.12.0")
// timber
@ -510,7 +406,7 @@ dependencies {
implementation("org.apache.commons:commons-compress:1.24.0")
// analytics
implementation("com.github.matomo-org:matomo-sdk-android:HEAD")
implementation("ly.count.android:sdk:23.8.2")
// annotations
implementation("org.jetbrains:annotations-java5:24.0.1")

@ -4,8 +4,6 @@
tools:ignore="QueryAllPackagesPermission"
tools:targetApi="tiramisu">
<uses-sdk tools:overrideLibrary="io.sentry.android" />
<queries>
<intent>
<action android:name="com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL" />
@ -32,9 +30,12 @@
<!-- Open config apps for applications -->
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- Open and read zips -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Write to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<!-- Post background notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Install updates -->
@ -187,31 +188,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/shared_file_paths" />
</provider>
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<meta-data
android:name="io.sentry.dsn"
android:value="https://09652942e9c042e39daed0bc5a8a98c3@g-fe.androidacy.com/3" /> <!-- enable view hierarchy for crashes -->
<meta-data
android:name="io.sentry.attach-view-hierarchy"
android:value="true" /> <!-- Sane value, but feel free to lower it -->
<meta-data
android:name="io.sentry.traces.sample-rate"
android:value="0.2" /> <!-- Doesn't actually monitor anything, just used to get the activities the user went through -->
<meta-data
android:name="io.sentry.traces.user-interaction.enable"
android:value="true" /> <!-- Just a screenshot of ONLY the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" /> <!-- Just the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-stacktrace"
android:value="true" /> <!-- Performance profiling -->
<meta-data
android:name="io.sentry.traces.profiling.sample-rate"
android:value="0.2" />
</application>
</manifest>

@ -10,20 +10,16 @@ import android.content.ClipboardManager
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
import io.sentry.Sentry
import io.sentry.UserFeedback
import io.sentry.protocol.SentryId
import timber.log.Timber
import java.io.PrintWriter
import java.io.StringWriter
class CrashHandler : AppCompatActivity() {
@Suppress("DEPRECATION", "KotlinConstantConditions")
@Suppress("DEPRECATION")
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("CrashHandler.onCreate(%s)", savedInstanceState)
@ -38,7 +34,7 @@ class CrashHandler : AppCompatActivity() {
// get the exception from the intent
val exception = intent.getSerializableExtra("exception") as Throwable?
// get the crashReportingEnabled from the intent
val crashReportingEnabled = intent.getBooleanExtra("crashReportingEnabled", false)
intent.getBooleanExtra("crashReportingEnabled", false)
// if the exception is null, set the crash details to "Unknown"
if (exception == null) {
crashDetails.setText(R.string.crash_details)
@ -51,113 +47,6 @@ class CrashHandler : AppCompatActivity() {
stacktrace = stacktrace.replace(",", "\n ")
crashDetails.text = getString(R.string.crash_full_stacktrace, stacktrace)
}
// force sentry to send all events
Sentry.flush(2000)
var lastEventId = intent.getStringExtra("lastEventId")
// if event id is all zeros, set it to "". test this by matching the regex ^0+$ (all zeros)
if (lastEventId?.matches("^0+$".toRegex()) == true) {
lastEventId = ""
}
if (BuildConfig.DEBUG) Timber.d(
"CrashHandler.onCreate: lastEventId=%s, crashReportingEnabled=%s",
lastEventId,
crashReportingEnabled
)
// get name, email, and message fields
val name = findViewById<EditText>(R.id.feedback_name)
val email = findViewById<EditText>(R.id.feedback_email)
val description = findViewById<EditText>(R.id.feedback_message)
val submit = findViewById<View>(R.id.feedback_submit)
if (lastEventId.isNullOrEmpty() && crashReportingEnabled) {
// if lastEventId is null, hide the feedback button
if (BuildConfig.DEBUG) Timber.d("CrashHandler.onCreate: lastEventId is null but crash reporting is enabled. This may indicate a bug in the crash reporting system.")
submit.visibility = View.GONE
findViewById<MaterialTextView>(R.id.feedback_text).setText(R.string.no_sentry_id)
} else {
// if lastEventId is not null, enable the feedback name, email, message, and submit button
email.isEnabled = true
name.isEnabled = true
description.isEnabled = true
submit.isEnabled = true
}
// disable feedback if sentry is disabled
if (crashReportingEnabled && lastEventId != null) {
// get submit button
findViewById<View>(R.id.feedback_submit).setOnClickListener { _: View? ->
// require the feedback_message, rest is optional
if (description.text.toString() == "") {
Toast.makeText(this, R.string.sentry_dialogue_empty_message, Toast.LENGTH_LONG)
.show()
return@setOnClickListener
}
// if email or name is empty, use "Anonymous"
val nameString =
arrayOf(if (name.text.toString() == "") "Anonymous" else name.text.toString())
val emailString =
arrayOf(if (email.text.toString() == "") "Anonymous" else email.text.toString())
Thread {
try {
val userFeedback =
UserFeedback(SentryId(lastEventId))
// Setups the JSON body
if (nameString[0] == "") nameString[0] = "Anonymous"
if (emailString[0] == "") emailString[0] = "Anonymous"
userFeedback.name = nameString[0]
userFeedback.email = emailString[0]
userFeedback.comments = description.text.toString()
Sentry.captureUserFeedback(userFeedback)
Timber.i(
"Submitted user feedback: name %s email %s comment %s",
nameString[0],
emailString[0],
description.text.toString()
)
runOnUiThread {
Toast.makeText(
this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG
).show()
}
// Close the activity
finish()
// start the main activity
startActivity(packageManager.getLaunchIntentForPackage(packageName))
} catch (e: Exception) {
Timber.e(e, "Failed to submit user feedback")
// Show a toast if the user feedback could not be submitted
runOnUiThread {
Toast.makeText(
this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG
).show()
}
}
}.start()
}
// get restart button
findViewById<View>(R.id.restart).setOnClickListener { _: View? ->
// Restart the app and submit sans feedback
val sentryException = intent.getSerializableExtra("sentryException") as Throwable?
if (crashReportingEnabled) Sentry.captureException(sentryException!!)
finish()
startActivity(packageManager.getLaunchIntentForPackage(packageName))
}
} else if (!crashReportingEnabled) {
// set feedback_text to "Crash reporting is disabled"
(findViewById<View>(R.id.feedback_text) as MaterialTextView).setText(R.string.sentry_enable_nag)
submit.setOnClickListener { _: View? ->
Toast.makeText(
this, R.string.sentry_dialogue_disabled, Toast.LENGTH_LONG
).show()
}
// handle restart button
// we have to explicitly enable it because it's disabled by default
findViewById<View>(R.id.restart).isEnabled = true
findViewById<View>(R.id.restart).setOnClickListener { _: View? ->
// Restart the app
finish()
startActivity(packageManager.getLaunchIntentForPackage(packageName))
}
}
// handle reset button
findViewById<View>(R.id.reset).setOnClickListener { _: View? ->
// show a confirmation material dialog

@ -35,7 +35,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.room.Room
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.AppUpdateManager.Companion.appUpdateManager
import com.fox2code.mmm.OverScrollManager.OverScrollHelper
import com.fox2code.mmm.androidacy.AndroidacyRepoData
@ -65,7 +64,8 @@ import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputEditText
import org.matomo.sdk.extra.TrackHelper
import ly.count.android.sdk.Countly
import ly.count.android.sdk.ModuleFeedback.FeedbackCallback
import timber.log.Timber
import java.sql.Timestamp
@ -118,7 +118,7 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
onMainActivityCreate(this)
super.onCreate(savedInstanceState)
INSTANCE = this
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
// 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) {
throw RuntimeException("This is not an official build of AMM")
@ -143,8 +143,11 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
db.close()
if (enabledRepos.isNotEmpty()) {
enabledRepos.delete(enabledRepos.length - 2, enabledRepos.length)
TrackHelper.track().event("Enabled Repos", enabledRepos.toString())
.with(MainApplication.INSTANCE!!.tracker)
// 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)
}
}.start()
val ts = Timestamp(System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000)
@ -235,7 +238,9 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
// filter the appropriate list based on visibility
if (initMode) return
val query = s.toString()
TrackHelper.track().search(query).with(MainApplication.INSTANCE!!.tracker)
Countly.sharedInstance().events().recordEvent("search", HashMap<String, String>().apply {
put("query", query)
} as Map<String, Any>?, 1)
Thread {
if (moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query)
@ -261,7 +266,9 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// filter the appropriate list based on visibility
val query = textInputEditText.text.toString()
TrackHelper.track().search(query).with(MainApplication.INSTANCE!!.tracker)
Countly.sharedInstance().events().recordEvent("search", HashMap<String, String>().apply {
put("query", query)
} as Map<String, Any>?, 1)
Thread {
if (moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query)
@ -396,7 +403,6 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}
}
})
textInputEditText.minimumHeight = FoxDisplay.dpToPixel(16f)
textInputEditText.imeOptions =
EditorInfo.IME_ACTION_SEARCH or EditorInfo.IME_FLAG_NO_FULLSCREEN
@ -405,8 +411,6 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
bottomNavigationView.setOnItemSelectedListener { item: MenuItem ->
when (item.itemId) {
R.id.settings_menu_item -> {
TrackHelper.track().event("view_list", "settings")
.with(MainApplication.INSTANCE!!.tracker)
// start settings activity so that when user presses back, they go back to main activity and on api34 they see a preview of the main activity. tell settings activity current active tab so that it can be selected when user goes back to main activity
val intent = Intent(this@MainActivity, SettingsActivity::class.java)
when (bottomNavigationView.selectedItemId) {
@ -417,8 +421,6 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}
R.id.online_menu_item -> {
TrackHelper.track().event("view_list", "online_modules")
.with(MainApplication.INSTANCE!!.tracker)
searchTextInputEditText!!.clearFocus()
searchTextInputEditText!!.text?.clear()
// set module_list_online as visible and module_list as gone. fade in/out
@ -439,8 +441,6 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
}
R.id.installed_menu_item -> {
TrackHelper.track().event("view_list", "installed_modules")
.with(MainApplication.INSTANCE!!.tracker)
searchTextInputEditText!!.clearFocus()
searchTextInputEditText!!.text?.clear()
// set module_list_online as gone and module_list as visible. fade in/out
@ -747,6 +747,44 @@ class MainActivity : AppCompatActivity(), OnRefreshListener, OverScrollHelper {
moduleViewListBuilder.applyTo(moduleList!!, moduleViewAdapter!!)
moduleViewListBuilderOnline.applyTo(moduleListOnline!!, moduleViewAdapterOnline!!)
}, "Repo update thread").start()
if (MainApplication.shouldShowFeedback()) {
Countly.sharedInstance().feedback()
.getAvailableFeedbackWidgets { retrievedWidgets, error ->
if (error == null) {
if (retrievedWidgets.size > 0) {
val feedbackWidget = retrievedWidgets[0]
Countly.sharedInstance().feedback().presentFeedbackWidget(
feedbackWidget,
this@MainActivity,
"Close",
object : FeedbackCallback {
override fun onClosed() {
}
// maybe show a toast when the widget is closed
override fun onFinished(error: String) {
// error handling here
if (error.isNotEmpty()) {
Toast.makeText(
this@MainActivity,
"Error: $error",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(
this@MainActivity,
"Feedback sent",
Toast.LENGTH_LONG
).show()
}
}
})
}
} else {
Timber.e(error)
}
}
}
}
fun maybeShowUpgrade() {

@ -17,6 +17,7 @@ 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
@ -28,7 +29,6 @@ import androidx.emoji2.text.EmojiCompat
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.work.Configuration
import com.fox2code.foxcompat.app.internal.FoxProcessExt
import com.fox2code.mmm.installer.InstallerInitializer
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
import com.fox2code.mmm.manager.LocalModuleInfo
@ -37,8 +37,6 @@ import com.fox2code.mmm.utils.TimberUtils.configTimber
import com.fox2code.mmm.utils.io.FileUtils
import com.fox2code.mmm.utils.io.GMSProviderInstaller.Companion.installIfNeeded
import com.fox2code.mmm.utils.io.net.Http.Companion.getHttpClientWithCache
import com.fox2code.mmm.utils.sentry.SentryMain
import com.fox2code.mmm.utils.sentry.SentryMain.initialize
import com.fox2code.rosettax.LanguageSwitcher
import com.google.common.hash.Hashing
import com.topjohnwu.superuser.Shell
@ -46,12 +44,8 @@ import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
import io.sentry.Sentry
import io.sentry.SentryLevel
import org.matomo.sdk.Matomo
import org.matomo.sdk.Tracker
import org.matomo.sdk.TrackerBuilder
import org.matomo.sdk.extra.TrackHelper
import ly.count.android.sdk.Countly
import ly.count.android.sdk.CountlyConfig
import timber.log.Timber
import java.io.File
import java.security.SecureRandom
@ -60,6 +54,7 @@ import java.util.Date
import java.util.Random
import kotlin.math.abs
@Suppress("unused", "MemberVisibilityCanBePrivate")
class MainApplication : Application(), Configuration.Provider, ActivityLifecycleCallbacks {
@ -97,18 +92,6 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
return field
}
private var existingKey: CharArray? = null
var tracker: Tracker? = null
get() {
if (field == null) {
field = TrackerBuilder.createDefault(BuildConfig.ANALYTICS_ENDPOINT, 1)
.build(Matomo.getInstance(this))
val tracker = field!!
tracker.startNewSession()
tracker.dispatchInterval = 1000
}
return field
}
private var makingNewKey = false
private var isCrashHandler = false
@ -216,6 +199,27 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
get() = !this.isLightTheme
override fun onCreate() {
Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable ->
clearCachedSharedPrefs()
// open crash handler and exit
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())
}
supportedLocales.addAll(
listOf(
"ar",
@ -254,7 +258,6 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
Timber.e(e, "Failed to register activity lifecycle callbacks")
}
}
initialize(this)
// Initialize Timber
configTimber()
Timber.i(
@ -276,19 +279,25 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
if (BuildConfig.DEBUG) Timber.d("Started from background: %s", !isInForeground)
if (BuildConfig.DEBUG) Timber.d("AMM is running in debug mode")
// analytics
if (BuildConfig.DEBUG) Timber.d("Initializing matomo")
if (!isMatomoAllowed()) {
if (BuildConfig.DEBUG) Timber.d("Matomo is not allowed")
tracker!!.isOptOut = true
} else {
tracker!!.isOptOut = false
}
if (getSharedPreferences("matomo")!!.getBoolean("install_tracked", false)) {
TrackHelper.track().download().with(INSTANCE!!.tracker)
if (BuildConfig.DEBUG) Timber.d("Sent install event to matomo")
getSharedPreferences("matomo")!!.edit().putBoolean("install_tracked", true).apply()
if (BuildConfig.DEBUG) Timber.d("Initializing countly")
if (!analyticsAllowed()) {
if (BuildConfig.DEBUG) Timber.d("countly is not allowed")
} else {
if (BuildConfig.DEBUG) Timber.d("Matomo already has install")
val config = CountlyConfig(
this,
"ff1dc022295f64a7a5f6a5ca07c0294400c71b60",
"https://ctly.androidacy.com"
)
if (isCrashReportingEnabled) {
config.enableCrashReporting()
}
config.enableAutomaticViewTracking()
config.setPushIntentAddMetadata(true)
config.setLoggingEnabled(BuildConfig.DEBUG)
config.setRequiresConsent(false)
config.setRecordAppStartTime(true)
Countly.sharedInstance().init(config)
Countly.applicationOnCreate()
}
try {
@Suppress("DEPRECATION") @SuppressLint("PackageManagerGetSignatures") val s =
@ -454,17 +463,12 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
// prepend [TAINTED] to the message
message = "[TAINTED] $message"
}
if (priority >= Log.WARN) {
if (priority >= Log.ERROR) {
if (t != null) {
Log.println(priority, tag, message)
t.printStackTrace()
Sentry.captureException(t)
} else {
Log.println(priority, tag, message)
when (priority) {
Log.ERROR -> Sentry.captureMessage(message, SentryLevel.ERROR)
Log.WARN -> Sentry.captureMessage(message, SentryLevel.WARNING)
}
}
}
}
@ -481,8 +485,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
// Is application wrapped, and therefore must reduce it's feature set.
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
@JvmField
val isWrapped = !FoxProcessExt.isRootLoader()
const val isWrapped = false
private val callers = ArrayList<String>()
@JvmField
@ -656,7 +659,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
}
val isCrashReportingEnabled: Boolean
get() = SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm")!!.getBoolean(
get() = getSharedPreferences("mmm")!!.getBoolean(
"pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING
)
val bootSharedPreferences: SharedPreferences?
@ -670,11 +673,37 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
val isNotificationPermissionGranted: Boolean
get() = NotificationManagerCompat.from((INSTANCE)!!).areNotificationsEnabled()
fun isMatomoAllowed(): Boolean {
fun analyticsAllowed(): Boolean {
return getSharedPreferences("mmm")!!.getBoolean(
"pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS
)
}
fun shouldShowFeedback(): Boolean {
// should not have been shown in 30 days and only 1 in 5 chance
return if (getSharedPreferences("mmm")!!.getBoolean("pref_feedback_shown", false)) {
false
} else {
val random = Random()
val chance = random.nextInt(5)
if (chance == 0) {
val lastFeedback = getSharedPreferences("mmm")!!.getLong(
"pref_last_feedback", 0
)
val now = System.currentTimeMillis()
if (now - lastFeedback > 2592000000L) {
val editor = getSharedPreferences("mmm")!!.edit()
editor.putLong("pref_last_feedback", now)
editor.apply()
true
} else {
false
}
} else {
false
}
}
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
@ -683,6 +712,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
}
override fun onActivityStarted(activity: Activity) {
Countly.sharedInstance().onStart(activity)
}
override fun onActivityResumed(activity: Activity) {
@ -694,6 +724,7 @@ class MainApplication : Application(), Configuration.Provider, ActivityLifecycle
}
override fun onActivityStopped(activity: Activity) {
Countly.sharedInstance().onStop()
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {

@ -4,7 +4,6 @@
@file:Suppress(
"KotlinConstantConditions",
"UNINITIALIZED_ENUM_COMPANION_WARNING",
"ktConcatNullable"
)
@ -207,14 +206,15 @@ enum class NotificationType(
compatActivity.cacheDir, "installer" + File.separator + "module.zip"
)
IntentHelper.openFileTo(compatActivity, module) { d: File, u: Uri, s: Int ->
val companion = NotificationType.Companion
if (s == IntentHelper.RESPONSE_FILE) {
try {
if (needPatch(d)) {
if (companion.needPatch(d)) {
patchModuleSimple(
read(d), FileOutputStream(d)
)
}
if (needPatch(d)) {
if (companion.needPatch(d)) {
if (d.exists() && !d.delete()) Timber.w("Failed to delete non module zip")
Toast.makeText(
compatActivity, R.string.invalid_format, Toast.LENGTH_SHORT

@ -150,18 +150,41 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
}
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_background_update_check)) as MaterialSwitch).isChecked =
BuildConfig.ENABLE_AUTO_UPDATER
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_crash_reporting)) as MaterialSwitch).isChecked =
val setupCrashReporting = view.findViewById<MaterialSwitch>(R.id.setup_crash_reporting)
val analyticsEnabled = view.findViewById<MaterialSwitch>(R.id.setup_app_analytics)
val crashReportingPii = view.findViewById<MaterialSwitch>(R.id.setup_crash_reporting_pii)
setupCrashReporting.isChecked =
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING
// pref_crash_reporting_pii
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_crash_reporting_pii)) as MaterialSwitch).isChecked =
crashReportingPii.isChecked =
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING_PII
// pref_analytics_enabled
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_app_analytics)) as MaterialSwitch).isChecked =
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
crashReportingPii.isEnabled = false
setupCrashReporting.isChecked = false
crashReportingPii.isChecked = false
}
// listen for changes to the analytics switch
analyticsEnabled.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
// 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
}
}
// 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((Objects.requireNonNull<Any>(view.findViewById(R.id.setup_crash_reporting)) as MaterialSwitch).isChecked == BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING)
assert(setupCrashReporting.isChecked == BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING)
}
// Repos are a little harder, as the enabled_repos build config is an arraylist
val andRepoView =
@ -178,7 +201,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
isChecked
)
}
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_crash_reporting)) as MaterialSwitch).setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setupCrashReporting.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
Timber.i(
"Crash Reporting: %s",
isChecked
@ -302,7 +325,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
// Set the crash reporting pref
editor.putBoolean(
"pref_crash_reporting",
(Objects.requireNonNull<Any>(view.findViewById(R.id.setup_crash_reporting)) as MaterialSwitch).isChecked
setupCrashReporting.isChecked
)
// Set the crash reporting PII pref
editor.putBoolean(
@ -339,7 +362,7 @@ class SetupActivity : AppCompatActivity(), LanguageActivity {
reposListDao.setEnabled(androidacyRepoRoomObj.id, androidacyRepoRoom)
reposListDao.setEnabled(magiskAltRepoRoomObj.id, magiskAltRepoRoom)
db.close()
editor.putString("last_shown_setup", "v4")
editor.putString("last_shown_setup", "v5")
// Commit the changes
editor.commit()
// Log the changes

@ -26,7 +26,6 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textview.MaterialTextView
import org.json.JSONException
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
@ -64,9 +63,6 @@ class UpdateActivity : AppCompatActivity() {
}
}
chgWv = findViewById(R.id.changelog_webview)
if (MainApplication.isMatomoAllowed()) {
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
}
val changelogWebView = chgWv!!
val webSettings = changelogWebView.settings
webSettings.userAgentString = Http.androidacyUA

@ -46,7 +46,6 @@ import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
import com.fox2code.mmm.utils.io.net.Http.Companion.markCaptchaAndroidacySolved
import com.google.android.material.progressindicator.LinearProgressIndicator
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.File
@ -76,7 +75,7 @@ class AndroidacyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
moduleFile = File(this.cacheDir, "module.zip")
super.onCreate(savedInstanceState)
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
val intent = this.intent
var uri: Uri? = intent.data
@Suppress("KotlinConstantConditions")

@ -8,15 +8,13 @@ package com.fox2code.mmm.androidacy
import android.net.Uri
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import java.io.IOException
@Suppress("MemberVisibilityCanBePrivate", "MemberVisibilityCanBePrivate")
enum class AndroidacyUtil {
;
companion object {
const val REFERRER = "utm_source=FoxMMM&utm_medium=app"
const val REFERRER = "utm_source=AMMM&utm_medium=app"
fun isAndroidacyLink(uri: Uri?): Boolean {
return uri != null && isAndroidacyLink(uri.toString(), uri)
}
@ -109,39 +107,5 @@ enum class AndroidacyUtil {
}
return null
}
/**
* Check if the url is a premium direct download link
* @param url url to check
* @return true if it is a premium direct download link
* @noinspection unused
*/
fun isPremiumDirectDownloadLink(url: String): Boolean {
return url.contains("/magisk/ddl/")
}
/**
* Returns the markdown directly from the API for rendering. Premium only, and internal testing only currently.
* /#blocked-by: A#F-0815
* @param url URL to get markdown from
* @return String of markdown
* @noinspection unused
*/
fun getMarkdownFromAPI(url: String?): String? {
val md: ByteArray = try {
doHttpGet(url!!, false)
} catch (ignored: IOException) {
return null
}
return String(md)
}
fun getMarkdownForModule(moduleId: String): String? {
try {
return getMarkdownFromAPI("https://production-api.androidacy.com/magisk/$moduleId/markdown")
} catch (ignored: IOException) {
}
return null
}
}
}

@ -16,7 +16,6 @@ import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
@ -158,7 +157,7 @@ class AndroidacyWebAPI(
return@injectButton null
}
}, "androidacy_repo")
val dim5dp = FoxDisplay.dpToPixel(5f)
val dim5dp = activity.resources.getDimensionPixelSize(R.dimen.dim5dp)
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp)
activity.runOnUiThread {
val alertDialog = builder.show()

@ -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.getSharedPreferences("mmm")!!
.getString("last_shown_setup", null) != "v4"
.getString("last_shown_setup", null) != "v5"
) return
// create notification channel group
val groupName: CharSequence = context.getString(R.string.notification_group_updates)

@ -46,8 +46,6 @@ import com.fox2code.mmm.utils.io.Files.Companion.write
import com.fox2code.mmm.utils.io.Hashes.Companion.checkSumMatch
import com.fox2code.mmm.utils.io.PropUtils
import com.fox2code.mmm.utils.io.net.Http
import com.fox2code.mmm.utils.sentry.SentryBreadcrumb
import com.fox2code.mmm.utils.sentry.SentryMain
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -56,8 +54,8 @@ import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
import ly.count.android.sdk.Countly
import org.apache.commons.compress.archivers.zip.ZipFile
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.BufferedReader
import java.io.File
@ -89,7 +87,7 @@ class InstallerActivity : AppCompatActivity() {
moduleCache = File(this.cacheDir, "installer")
if (!moduleCache!!.exists() && !moduleCache!!.mkdirs()) Timber.e("Failed to mkdir module cache dir!")
super.onCreate(savedInstanceState)
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
val intent = this.intent
val target: String
val name: String?
@ -131,15 +129,9 @@ class InstallerActivity : AppCompatActivity() {
finish()
return
}
// Note: Sentry only send this info on crash.
if (MainApplication.isCrashReportingEnabled) {
val breadcrumb = SentryBreadcrumb()
breadcrumb.setType("install")
breadcrumb.setData("target", target)
breadcrumb.setData("name", name)
breadcrumb.setData("checksum", checksum)
breadcrumb.setCategory("app.action.preinstall")
SentryMain.addSentryBreadcrumb(breadcrumb)
// set countly breadcrumb
Countly.sharedInstance().crashes().addCrashBreadcrumb("InstallerActivity.onCreate")
}
val urlMode = target.startsWith("http://") || target.startsWith("https://")
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -183,9 +175,18 @@ class InstallerActivity : AppCompatActivity() {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Fox:Installer")
prgInd?.visibility = View.VISIBLE
if (urlMode) installerTerminal!!.addLine("- Downloading $name")
TrackHelper.track().event("installer_start", name).with(MainApplication.INSTANCE!!.tracker)
// track install
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("install", mapOf(
"name" to name,
"url" to target,
"checksum" to checksum,
"noExtensions" to noExtensions,
"rootless" to rootless,
"mmtReborn" to mmtReborn
))
}
Thread(Runnable {
// ensure module cache is is in our cache dir
if (urlMode && !moduleCache!!.absolutePath.startsWith(MainApplication.INSTANCE!!.cacheDir.absolutePath)) throw SecurityException(
"Module cache is not in cache dir!"
@ -533,18 +534,7 @@ class InstallerActivity : AppCompatActivity() {
}
// Note: Sentry only send this info on crash.
if (MainApplication.isCrashReportingEnabled) {
val breadcrumb = SentryBreadcrumb()
breadcrumb.setType("install")
breadcrumb.setData("moduleId", if (moduleId == null) "<null>" else moduleId)
breadcrumb.setData("mmtReborn", if (mmtReborn) "true" else "false")
breadcrumb.setData("isAnyKernel3", if (anyKernel3) "true" else "false")
breadcrumb.setData("noExtensions", if (noExtensions) "true" else "false")
breadcrumb.setData("magiskCmdLine", if (magiskCmdLine) "true" else "false")
breadcrumb.setData(
"ansi", if (installerTerminal!!.isAnsiEnabled) "enabled" else "disabled"
)
breadcrumb.setCategory("app.action.install")
SentryMain.addSentryBreadcrumb(breadcrumb)
Countly.sharedInstance().crashes().addCrashBreadcrumb("InstallerActivity.doInstall")
}
if (mmtReborn && magiskCmdLine) {
Timber.w("mmtReborn and magiskCmdLine may not work well together")

@ -11,6 +11,7 @@ import com.fox2code.mmm.NotificationType
import com.fox2code.mmm.utils.io.Files.Companion.existsSU
import com.topjohnwu.superuser.NoShellException
import com.topjohnwu.superuser.Shell
import ly.count.android.sdk.Countly
import timber.log.Timber
import java.io.File
@ -125,7 +126,7 @@ class InstallerInitializer {
if (Shell.isAppGrantedRoot() == null || !Shell.isAppGrantedRoot()!!) {
// if Shell.isAppGrantedRoot() == null loop until it's not null
return if (Shell.isAppGrantedRoot() == null) {
Thread.sleep(100)
Thread.sleep(150)
tryGetMagiskPath(forceCheck)
} else {
null
@ -162,33 +163,53 @@ class InstallerInitializer {
if (BuildConfig.DEBUG) {
Timber.i("Magisk path 1: %s", mgskPth)
}
} else if (Shell.cmd("if [ -d /data/adb/ksu ] && [ -f /data/adb/ksud ]; then echo true; else echo false; fi", "su -V").to(
} else if (Shell.cmd("if [ -d /data/adb/ksu ]; then echo true; else echo false; fi", "su -V").to(
output
).exec().isSuccess && "true" == output[0]
) {
mgskPth = "/data/adb"
isKsu = true
// check su -v for kernelsu
val suVer: ArrayList<String> = ArrayList()
Shell.cmd("su -v").to(suVer).exec()
if (suVer.size > 0 && suVer[0].contains("ksu") || suVer[0].contains("Kernelsu", true)) {
if (BuildConfig.DEBUG) {
Timber.i("Kernelsu detected")
}
mgskPth = "/data/adb"
isKsu = true
// if analytics enabled, set breadcrumb for countly
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().crashes().addCrashBreadcrumb("ksu detected")
}
} else {
if (BuildConfig.DEBUG) {
Timber.e("[ANOMALY] Kernelsu not detected but /data/adb/ksu exists")
}
return null
}
} else {
if (BuildConfig.DEBUG) {
Timber.e("Failed to get Magisk path")
}
return null
}
Timber.i("Magisk runtime path: %s", mgskPth)
mgskVerCode = output[1].toInt()
Timber.i("Magisk version code: %s", mgskVerCode)
if (mgskPth != null) {
if (mgskVerCode >= Constants.MAGISK_VER_CODE_FLAT_MODULES && mgskVerCode < Constants.MAGISK_VER_CODE_PATH_SUPPORT && (mgskPth.isEmpty() || !File(
mgskPth
).exists())
) {
mgskPth = "/sbin"
}
if (mgskVerCode >= Constants.MAGISK_VER_CODE_FLAT_MODULES && mgskVerCode < Constants.MAGISK_VER_CODE_PATH_SUPPORT && (mgskPth.isEmpty() || !File(
mgskPth
).exists())
) {
mgskPth = "/sbin"
}
if (mgskPth != null) {
if (mgskPth.isNotEmpty() && existsSU(File(mgskPth))) {
Companion.mgskPth = mgskPth
} else {
Timber.e("Failed to get Magisk path (Got $mgskPth)")
mgskPth = null
}
if (mgskPth.isNotEmpty() && existsSU(File(mgskPth))) {
Companion.mgskPth = mgskPth
} else {
Timber.e("Failed to get Magisk path (Got null or other)")
Timber.e("Failed to get Magisk path (Got $mgskPth)")
mgskPth = null
}
// if mgskPth is null, but we're granted root, log an error
if (mgskPth == null && Shell.isAppGrantedRoot() == true) {
Timber.e("[ANOMALY] Failed to get Magisk path but granted root")
}
Companion.mgskVerCode = mgskVerCode
return mgskPth

@ -17,7 +17,7 @@ import com.fox2code.mmm.utils.room.ModuleListCacheDatabase
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import org.matomo.sdk.extra.TrackHelper
import ly.count.android.sdk.Countly
import timber.log.Timber
import java.io.BufferedReader
import java.io.IOException
@ -30,9 +30,9 @@ class ModuleManager private constructor() : SyncManager() {
private var updatableModuleCount = 0
override fun scanInternal(updateListener: UpdateListener) {
// if last_shown_setup is not "v4", then refuse to continue
// if last_shown_setup is not "v5", then refuse to continue
if (MainApplication.getSharedPreferences("mmm")!!
.getString("last_shown_setup", "") != "v4"
.getString("last_shown_setup", "") != "v5"
) {
return
}
@ -145,9 +145,12 @@ class ModuleManager private constructor() : SyncManager() {
if (modulesList.isNotEmpty()) {
modulesList.deleteCharAt(modulesList.length - 1)
}
// send list to matomo
TrackHelper.track().event("installed_modules", modulesList.toString())
.with(MainApplication.INSTANCE!!.tracker)
// send list to countly if analytics is enabled
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("modules", HashMap<String, Any?>().apply {
put("modules", modulesList.toString())
})
}
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG) Timber.d("Scan update")
val modulesUpdate = SuFile("/data/adb/modules_update").list()
if (modulesUpdate != null) {

@ -25,7 +25,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.IOException
import java.nio.charset.StandardCharsets
@ -35,7 +34,7 @@ class MarkdownActivity : AppCompatActivity() {
private var footer: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
val intent = this.intent
if (!MainApplication.checkSecret(intent)) {
Timber.e("Impersonation detected!")

@ -11,7 +11,6 @@ import android.text.Spanned
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
@ -31,7 +30,7 @@ import com.fox2code.mmm.utils.IntentHelper.Companion.openUrlAndroidacy
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.noties.markwon.Markwon
import org.matomo.sdk.extra.TrackHelper
import ly.count.android.sdk.Countly
import timber.log.Timber
import java.util.Objects
@ -50,7 +49,12 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("view_notes", name).with(INSTANCE!!.tracker)
// if analytics is enabled, track the event
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_description", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
val notesUrl = moduleHolder.repoModule?.notesUrl
if (isAndroidacyLink(notesUrl)) {
try {
@ -145,7 +149,10 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("view_update_install", name).with(INSTANCE!!.tracker)
// send event to countly
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
@ -216,7 +223,9 @@ enum class ActionButtonType {
{ Uri.parse(updateZipUrl) },
moduleHolder.updateZipRepo
)
val dim5dp = FoxDisplay.dpToPixel(5f)
val dim5dp = INSTANCE!!.lastActivity?.resources!!.getDimensionPixelSize(
R.dimen.dim5dp
)
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp)
val alertDialog = builder.show()
for (i in -3..-1) {
@ -255,7 +264,12 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("uninstall_module", name).with(INSTANCE!!.tracker)
// if analytics is enabled, track the event
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_uninstall", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
Timber.i(Integer.toHexString(moduleHolder.moduleInfo?.flags ?: 0))
if (!instance!!.setUninstallState(
moduleHolder.moduleInfo!!, !moduleHolder.hasFlag(
@ -310,7 +324,11 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("config_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_config", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
if (isAndroidacyLink(config)) {
openUrlAndroidacy(button.context, config, true)
} else {
@ -333,7 +351,11 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("support_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_support", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
openUrl(button.context, Objects.requireNonNull(moduleHolder.mainModuleInfo.support))
}
},
@ -352,7 +374,11 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("donate_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
Countly.sharedInstance().events().recordEvent("view_donate", HashMap<String, Any>().apply {
put("module", name ?: "null")
})
}
openUrl(button.context, moduleHolder.mainModuleInfo.donate)
}
},
@ -368,7 +394,11 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("warning_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
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
@ -390,7 +420,11 @@ enum class ActionButtonType {
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("safe_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
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
@ -409,7 +443,11 @@ enum class ActionButtonType {
moduleHolder.repoModule?.moduleInfo?.name
}
// positive button executes install logic and says reinstall. negative button does nothing
TrackHelper.track().event("remote_module", name).with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
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)
val moduleInfo: ModuleInfo = if (moduleHolder.mainModuleInfo != null) {
@ -462,8 +500,11 @@ enum class ActionButtonType {
moduleHolder.repoModule?.moduleInfo?.name
}
if (BuildConfig.DEBUG) Timber.d("doAction: remote module for %s", name)
TrackHelper.track().event("view_update_install", name)
.with(INSTANCE!!.tracker)
if (MainApplication.analyticsAllowed()) {
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)) {
if (BuildConfig.DEBUG) Timber.d("Androidacy link detected")
@ -518,7 +559,9 @@ enum class ActionButtonType {
{ Uri.parse(updateZipUrl) },
moduleHolder.updateZipRepo
)
val dim5dp = FoxDisplay.dpToPixel(5f)
val dim5dp = INSTANCE!!.lastActivity?.resources!!.getDimensionPixelSize(
R.dimen.dim5dp
)
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp)
val alertDialog = builder.show()
for (i in -3..-1) {

@ -26,7 +26,6 @@ import androidx.annotation.StringRes
import androidx.cardview.widget.CardView
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
@ -74,7 +73,6 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
private val creditText: TextView
private val descriptionText: TextView
private val moduleOptionsHolder: HorizontalScrollView
private val moduleLayoutHelper: TextView
private val updateText: TextView
private val actionsButtons: Array<Chip?>
private val actionButtonsTypes: ArrayList<ActionButtonType?>
@ -93,7 +91,6 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
creditText = itemView.findViewById(R.id.credit_text)
descriptionText = itemView.findViewById(R.id.description_text)
moduleOptionsHolder = itemView.findViewById(R.id.module_options_holder)
moduleLayoutHelper = itemView.findViewById(R.id.module_layout_helper)
updateText = itemView.findViewById(R.id.updated_text)
actionsButtons = arrayOfNulls(6)
actionsButtons[0] = itemView.findViewById(R.id.button_action1)
@ -194,7 +191,6 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
}
creditText.visibility = View.VISIBLE
moduleOptionsHolder.visibility = View.VISIBLE
moduleLayoutHelper.visibility = View.VISIBLE
descriptionText.visibility = View.VISIBLE
val moduleInfo = moduleHolder.mainModuleInfo
moduleInfo.verify()
@ -249,7 +245,6 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
descriptionText.text = moduleInfo.description
}
val updateText = moduleHolder.updateTimeText
var hasUpdateText = true
if (updateText.isNotEmpty()) {
val repoModule = moduleHolder.repoModule
this.updateText.visibility = View.VISIBLE
@ -267,7 +262,6 @@ ${getString(R.string.module_repo)} ${moduleHolder.repoName}""" + if ((repoModule
this.updateText.setText(R.string.substratum_builtin_module)
} else {
this.updateText.visibility = View.GONE
hasUpdateText = false
}
actionButtonsTypes.clear()
moduleHolder.getButtons(itemView.context, actionButtonsTypes, showCaseMode)
@ -290,12 +284,6 @@ ${getString(R.string.module_repo)} ${moduleHolder.repoName}""" + if ((repoModule
}
if (actionButtonsTypes.isEmpty()) {
moduleOptionsHolder.visibility = View.GONE
moduleLayoutHelper.visibility = View.GONE
} else if (actionButtonsTypes.size > 2 || !hasUpdateText) {
moduleLayoutHelper.minHeight = FoxDisplay.dpToPixel(36f)
.coerceAtLeast(moduleOptionsHolder.height - FoxDisplay.dpToPixel(14f))
} else {
moduleLayoutHelper.minHeight = FoxDisplay.dpToPixel(4f)
}
cardView.isClickable = false
if (moduleHolder.isModuleHolder && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) {
@ -316,7 +304,6 @@ ${getString(R.string.module_repo)} ${moduleHolder.repoName}""" + if ((repoModule
switchMaterial.visibility = View.GONE
creditText.visibility = View.GONE
moduleOptionsHolder.visibility = View.GONE
moduleLayoutHelper.visibility = View.GONE
descriptionText.visibility = View.GONE
updateText.visibility = View.GONE
titleText.text = " "
@ -331,7 +318,9 @@ ${getString(R.string.module_repo)} ${moduleHolder.repoName}""" + if ((repoModule
}
if (type === ModuleHolder.Type.NOTIFICATION) {
val notificationType = moduleHolder.notificationType
titleText.setText(notificationType?.textId ?: 0)
if (notificationType?.textId != null) {
titleText.setText(notificationType.textId)
}
// set title text appearance
titleText.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
if (notificationType != null) {
@ -353,7 +342,9 @@ ${getString(R.string.module_repo)} ${moduleHolder.repoName}""" + if ((repoModule
}
}
if (type === ModuleHolder.Type.SEPARATOR) {
titleText.setText(if (moduleHolder.separator != null) moduleHolder.separator.title else 0)
if (moduleHolder.separator != null) {
titleText.text = getString(moduleHolder.separator.title)
}
}
if (DEBUG) {
if (vType != null) {

@ -29,7 +29,7 @@ class CustomRepoManager internal constructor(
init {
repoCount = 0
// refuse to load if setup is not complete
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") == "v4") {
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") == "v5") {
val i = 0
val lastFilled = intArrayOf(0)
// now the same as above but for room database

@ -56,7 +56,7 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa
repoData = LinkedHashMap()
modules = HashMap()
// refuse to load if setup is not complete
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") == "v4") {
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") == "v5") {
// We do not have repo list config yet.
androidacyRepoData = addAndroidacyRepoData()
val altRepo = addRepoData(MAGISK_ALT_REPO, "Magisk Modules Alt Repo")
@ -82,8 +82,8 @@ class RepoManager private constructor(mainApplication: MainApplication) : SyncMa
}
private fun populateDefaultCache(repoData: RepoData?) {
// if last_shown_setup is not "v4", them=n refuse to continue
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "v4") {
// if last_shown_setup is not "v5", them=n refuse to continue
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "v5") {
return
}
// make sure repodata is not null

@ -16,7 +16,6 @@ import com.fox2code.mmm.Constants
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
import com.fox2code.mmm.installer.InstallerInitializer
import com.fox2code.mmm.utils.sentry.SentryMain
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.apache.commons.io.FileUtils
import timber.log.Timber
@ -90,7 +89,7 @@ class DebugFragment : PreferenceFragmentCompat() {
}.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> }.show()
true
}
if (!SentryMain.IS_SENTRY_INSTALLED || !BuildConfig.DEBUG || InstallerInitializer.peekMagiskPath() == null) {
if (!BuildConfig.DEBUG || InstallerInitializer.peekMagiskPath() == null) {
// Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app
Timber.i(InstallerInitializer.peekMagiskPath())
findPreference<Preference?>("pref_test_crash")!!.isVisible = false

@ -152,7 +152,7 @@ class InfoFragment : PreferenceFragmentCompat() {
// open androidacy
IntentHelper.openUrl(
MainApplication.INSTANCE!!.lastActivity!!,
"https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"
"https://www.androidacy.com/membership-join/?utm_source=AMMM&utm_medium=app&utm_campaign=donate"
)
true
}
@ -164,7 +164,7 @@ class InfoFragment : PreferenceFragmentCompat() {
clipboard.setPrimaryClip(
ClipData.newPlainText(
toastText,
"https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"
"https://www.androidacy.com/membership-join/?utm_source=AMMM&utm_medium=app&utm_campaign=donate"
)
)
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show()

@ -17,7 +17,6 @@ import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainActivity
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
import com.fox2code.mmm.utils.sentry.SentryMain
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import timber.log.Timber
import kotlin.system.exitProcess
@ -53,7 +52,6 @@ class PrivacyFragment : PreferenceFragmentCompat() {
// Crash reporting
val crashReportingPreference =
findPreference<TwoStatePreference>("pref_crash_reporting")
if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference!!.isVisible = false
crashReportingPreference!!.isChecked = MainApplication.isCrashReportingEnabled
val initialValue: Any = MainApplication.isCrashReportingEnabled
crashReportingPreference.onPreferenceChangeListener =
@ -86,5 +84,16 @@ class PrivacyFragment : PreferenceFragmentCompat() {
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")
analyticsPreference!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
if (initialValue === newValue) return@OnPreferenceChangeListener true
crashReportingPreference.isEnabled = newValue as Boolean
if (!newValue) crashReportingPreference.isChecked = false
true
}
// now, disable pref_crash_reporting if analytics is off
crashReportingPreference.isEnabled = analyticsPreference.isChecked
}
}

@ -24,8 +24,6 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.preference.TwoStatePreference
import androidx.room.Room
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.foxcompat.view.FoxViewCompat
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainActivity
import com.fox2code.mmm.MainApplication
@ -177,7 +175,7 @@ class RepoFragment : PreferenceFragmentCompat() {
// User clicked OK button. Open GitHub releases page
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=FoxMMM&utm_medium=app&utm_campaign=FoxMMM")
Uri.parse("https://www.androidacy.com/downloads/?view=AMMM&utm_source=AMMM&utm_medium=app&utm_campaign=AMMM")
)
startActivity(browserIntent)
}.show()
@ -623,9 +621,17 @@ class RepoFragment : PreferenceFragmentCompat() {
override fun afterTextChanged(s: Editable) {}
})
positiveButton.isEnabled = false
val dp10 = FoxDisplay.dpToPixel(10f)
val dp20 = FoxDisplay.dpToPixel(20f)
FoxViewCompat.setMargin(input, dp20, dp10, dp20, dp10)
val dp10 = MainApplication.INSTANCE!!.lastActivity?.resources?.getDimensionPixelSize(
R.dimen.dp10
) ?: 0
val dp20 = MainApplication.INSTANCE!!.lastActivity?.resources?.getDimensionPixelSize(
R.dimen.dp20
) ?: 0
alertDialog.window!!.setSoftInputMode(20)
alertDialog.window!!.setLayout(
dp20,
dp10
)
true
}
}

@ -33,7 +33,6 @@ import com.fox2code.rosettax.LanguageActivity
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import com.mikepenz.aboutlibraries.LibsBuilder
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.sql.Timestamp
@ -89,7 +88,7 @@ class SettingsActivity : AppCompatActivity(), LanguageActivity,
.commit()
true
}
TrackHelper.track().screen(this).with(INSTANCE!!.tracker)
setContentView(R.layout.settings_activity)
setTitle(R.string.app_name_v2)
val ts = Timestamp(System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000)

@ -15,7 +15,6 @@ import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Supplier
import com.fox2code.mmm.Constants
import com.topjohnwu.superuser.internal.UiThreadHandler
@ -57,13 +56,6 @@ class ExternalHelper private constructor() {
fun openExternal(context: Context, uri: Uri?, repoId: String?): Boolean {
if (label == null) return false
val param =
ActivityOptionsCompat.makeCustomAnimation(
context,
rikka.core.R.anim.fade_in,
rikka.core.R.anim.fade_out
)
.toBundle()
var intent = Intent(FOX_MMM_OPEN_EXTERNAL, uri)
intent.flags = IntentHelper.FLAG_GRANT_URI_PERMISSION
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId)
@ -76,7 +68,7 @@ class ExternalHelper private constructor() {
if (multi) {
context.startActivity(intent)
} else {
context.startActivity(intent, param)
context.startActivity(intent, null)
}
return true
} catch (e: ActivityNotFoundException) {
@ -90,7 +82,7 @@ class ExternalHelper private constructor() {
}
intent.component = fallback
try {
context.startActivity(intent, param)
context.startActivity(intent, null)
return true
} catch (e: ActivityNotFoundException) {
Timber.e(e)

@ -159,7 +159,7 @@ class RuntimeUtils {
if (BuildConfig.DEBUG) 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.getSharedPreferences("mmm")!!
var firstLaunch = prefs.getString("last_shown_setup", null) != "v4"
var firstLaunch = prefs.getString("last_shown_setup", null) != "v5"
// 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)) {
@ -264,7 +264,7 @@ class RuntimeUtils {
snackbar.setAction(R.string.upgrade_now) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data =
Uri.parse("https://androidacy.com/membership-join/#utm_source=foxmmm&utm_medium=app&utm_campaign=upgrade_snackbar")
Uri.parse("https://androidacy.com/membership-join/#utm_source=AMMM&utm_medium=app&utm_campaign=upgrade_snackbar")
activity.startActivity(intent)
}
snackbar.setAnchorView(R.id.bottom_navigation)

@ -29,7 +29,7 @@ import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
import com.fox2code.mmm.utils.io.Files.Companion.makeBuffer
import com.google.net.cronet.okhttptransport.CronetInterceptor
import io.sentry.android.okhttp.SentryOkHttpInterceptor
import ly.count.android.sdk.Countly
import okhttp3.Cache
import okhttp3.Dns
import okhttp3.HttpUrl.*
@ -53,11 +53,13 @@ import org.chromium.net.CronetEngine
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.lang.Long.*
import java.net.InetAddress
import java.net.Proxy
import java.net.UnknownHostException
import java.nio.charset.StandardCharsets
import java.util.Objects
import java.util.UUID.randomUUID
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
@ -290,9 +292,9 @@ enum class Http {;
// User-Agent format was agreed on telegram
androidacyUA = if (hasWebView) {
WebSettings.getDefaultUserAgent(mainApplication)
.replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE
.replace("wv", "") + " AMMM/" + BuildConfig.VERSION_CODE
} else {
"Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + " FoxMmm/" + BuildConfig.VERSION_CODE
"Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + " AMMM/" + BuildConfig.VERSION_CODE
}
httpclientBuilder.addInterceptor(Interceptor { chain: Interceptor.Chain? ->
val request: Request.Builder = chain!!.request().newBuilder()
@ -340,9 +342,6 @@ enum class Http {;
chain.proceed(request.build())
})
// add sentry interceptor
httpclientBuilder.addInterceptor(SentryOkHttpInterceptor())
// Add cronet interceptor
// init cronet
try {
@ -457,6 +456,10 @@ enum class Http {;
if (url.isEmpty()) {
throw IOException("Empty URL")
}
val uniqid = randomUUID().toString()
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().startNetworkRequest(uniqid, url)
}
var response: Response?
response = try {
(if (allowCache) getHttpClientWithCache() else getHttpClient())!!.newCall(
@ -465,7 +468,7 @@ enum class Http {;
).get().build()
).execute()
} catch (e: IOException) {
Timber.e(e, "Failed to post %s", url)
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_")) {
MainApplication.INSTANCE!!.lastActivity!!.runOnUiThread {
@ -540,6 +543,11 @@ enum class Http {;
if (BuildConfig.DEBUG) Timber.d("doHttpGet: returning " + responseBody.contentLength() + " bytes")
}
}
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().endNetworkRequest(uniqid, url, response!!.code,
0, responseBody!!.contentLength().toInt()
)
}
return responseBody?.bytes() ?: ByteArray(0)
}
@ -552,6 +560,11 @@ enum class Http {;
@Throws(IOException::class)
private fun doHttpPostRaw(url: String, data: String, allowCache: Boolean): Any {
if (BuildConfig.DEBUG) Timber.d("POST %s", url)
val uniqid = randomUUID().toString()
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().startNetworkRequest(uniqid, url)
}
var response: Response?
try {
response =
@ -625,11 +638,20 @@ enum class Http {;
response = response.cacheResponse
if (response != null) responseBody = response.body
}
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().endNetworkRequest(uniqid, url, response!!.code,
data.toByteArray().size.toLong().toInt(), responseBody.contentLength().toInt()
)
}
return responseBody.bytes()
}
@Throws(IOException::class)
fun doHttpGet(url: String, progressListener: ProgressListener): ByteArray {
val uniqid = randomUUID().toString()
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().startNetworkRequest(uniqid, url)
}
val response: Response
try {
response =
@ -713,6 +735,11 @@ enum class Http {;
progressListener.onUpdate(
(downloaded / divider).toInt(), (target / divider).toInt(), true
)
if (MainApplication.analyticsAllowed() && MainApplication.isCrashReportingEnabled) {
Countly.sharedInstance().apm().endNetworkRequest(uniqid, url, response.code,
0, responseBody.contentLength().toInt()
)
}
return byteArrayOutputStream.toByteArray()
}

@ -1,32 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.utils.sentry
import io.sentry.Breadcrumb
import io.sentry.SentryLevel
import java.util.Objects
class SentryBreadcrumb {
val breadcrumb: Breadcrumb = Breadcrumb()
init {
breadcrumb.level = SentryLevel.INFO
}
fun setType(type: String?) {
breadcrumb.type = type
}
fun setData(key: String, value: Any?) {
@Suppress("NAME_SHADOWING") var value = value
if (value == null) value = "null"
Objects.requireNonNull(key)
breadcrumb.setData(key, value)
}
fun setCategory(category: String?) {
breadcrumb.category = category
}
}

@ -1,182 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.utils.sentry
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Process
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.CrashHandler
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.hideToken
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.isAndroidacyLink
import com.fox2code.mmm.utils.io.net.HttpException
import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.SentryOptions.BeforeBreadcrumbCallback
import io.sentry.SentryOptions.BeforeSendCallback
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.android.fragment.FragmentLifecycleIntegration
import io.sentry.android.timber.SentryTimberIntegration
import io.sentry.protocol.SentryId
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
object SentryMain {
const val IS_SENTRY_INSTALLED = true
private var isCrashing = false
private var isSentryEnabled = false
private var crashExceptionId: SentryId? = null
/**
* Initialize Sentry
* Sentry is used for crash reporting and performance monitoring.
*/
@JvmStatic
@SuppressLint("RestrictedApi", "UnspecifiedImmutableFlag", "ApplySharedPref")
fun initialize(mainApplication: MainApplication) {
Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable ->
isCrashing = true
MainApplication.clearCachedSharedPrefs()
TrackHelper.track().exception(throwable).with(MainApplication.INSTANCE!!.tracker)
// open crash handler and exit
val intent = Intent(mainApplication, 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", isSentryEnabled)
// add isCrashing to intent
intent.putExtra("isCrashing", true)
// add crashExceptionId to intent
if (crashExceptionId != null) {
intent.putExtra("lastEventId", crashExceptionId!!.toString())
} else {
intent.putExtra("lastEventId", "")
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Timber.e("Starting crash handler")
mainApplication.startActivity(intent)
Timber.e("Exiting")
Process.killProcess(Process.myPid())
}
// If first_launch pref is not false, refuse to initialize Sentry
val sharedPreferences = MainApplication.getSharedPreferences("mmm")!!
if (sharedPreferences.getString("last_shown_setup", null) != "v4") {
return
}
isSentryEnabled = sharedPreferences.getBoolean("pref_crash_reporting_enabled", false)
// set sentryEnabled on preference change of pref_crash_reporting_enabled
sharedPreferences.registerOnSharedPreferenceChangeListener { sharedPreferences1: SharedPreferences, s: String? ->
if (s !== null && s == "pref_crash_reporting_enabled") {
isSentryEnabled = sharedPreferences1.getBoolean(s, false)
}
}
SentryAndroid.init(mainApplication) { options: SentryAndroidOptions ->
// If crash reporting is disabled, stop here.
if (!MainApplication.isCrashReportingEnabled) {
isSentryEnabled = false // Set sentry state to disabled
options.dsn = ""
} else {
// get pref_crash_reporting_pii pref
val crashReportingPii = sharedPreferences.getBoolean("crashReportingPii", false)
isSentryEnabled = true // Set sentry state to enabled
options.addIntegration(
FragmentLifecycleIntegration(
mainApplication,
enableFragmentLifecycleBreadcrumbs = true,
enableAutoFragmentLifecycleTracing = true
)
)
// Enable automatic activity lifecycle breadcrumbs
options.isEnableActivityLifecycleBreadcrumbs = true
// Enable automatic fragment lifecycle breadcrumbs
options.addIntegration(SentryTimberIntegration())
options.isCollectAdditionalContext = true
options.isAttachThreads = true
options.isAttachStacktrace = true
options.isEnableNdk = true
options.addInAppInclude("com.fox2code.mmm")
options.addInAppInclude("com.fox2code.mmm.debug")
options.addInAppInclude("com.fox2code.mmm.fdroid")
options.addInAppExclude("com.fox2code.mmm.utils.sentry.SentryMain")
options.addInAppInclude("com.fox2code.mmm.utils")
// Respect user preference for sending PII. default is true on non fdroid builds, false on fdroid builds
options.isSendDefaultPii = crashReportingPii
options.enableAllAutoBreadcrumbs(true)
// in-app screenshots are only sent if the app crashes, and it only shows the last activity. so no, we won't see your, ahem, "private" stuff
options.isAttachScreenshot = true
// It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
options.isEnableAutoSessionTracking = true
// disable crash tracking - we handle that ourselves
options.isEnableUncaughtExceptionHandler = true
// Add a callback that will be used before the event is sent to Sentry.
// With this callback, you can modify the event or, when returning null, also discard the event.
options.environment = BuildConfig.BUILD_TYPE
options.beforeSend = BeforeSendCallback { event: SentryEvent?, _: Hint? ->
// in the rare event that crash reporting has been disabled since we started the app, we don't want to send the crash report
if (!isSentryEnabled) {
return@BeforeSendCallback null
}
crashExceptionId = event?.eventId
// if debug build, log everything, but for release only log errors
if (!BuildConfig.DEBUG) {
if (event?.level == SentryLevel.DEBUG || event?.level == SentryLevel.INFO || event?.level == SentryLevel.WARNING) {
return@BeforeSendCallback null
}
}
// remove all failed to fetch data messages
if (event?.message?.message?.contains("Failed to fetch") == true || event?.message?.message?.contains(
"Failed to load"
) == true
) {
return@BeforeSendCallback null
}
// for httpexception, do not send if error is 401, 403, 404, 429
// get exception from event
val exception = event?.throwable ?: return@BeforeSendCallback event
// check status code
if (exception is HttpException) {
if (exception.errorCode in intArrayOf(401, 403, 404, 429)) {
return@BeforeSendCallback null
}
}
// if exception is null, return event
event
}
// Filter breadcrumb content from crash report.
options.beforeBreadcrumb =
BeforeBreadcrumbCallback { breadcrumb: Breadcrumb, _: Hint? ->
if (!isSentryEnabled) {
return@BeforeBreadcrumbCallback null
}
val url = breadcrumb.getData("url") as String?
if ("cloudflare-dns.com" == Uri.parse(url).host) {
return@BeforeBreadcrumbCallback null
}
if (isAndroidacyLink(url)) {
url?.let { hideToken(it) }?.let { breadcrumb.setData("url", it) }
}
breadcrumb
}
}
}
}
fun addSentryBreadcrumb(sentryBreadcrumb: SentryBreadcrumb) {
if (MainApplication.isCrashReportingEnabled) {
Sentry.addBreadcrumb(sentryBreadcrumb.breadcrumb)
}
}
}

@ -2,20 +2,29 @@
~ Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- layout with crash_text header and crash_details body -->
<!-- first, app name -->
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="@string/app_name_v2"
android:textSize="24sp"
android:textStyle="bold" />
<!-- first, crash icon -->
<ImageView
<!-- second, crash icon -->
<com.google.android.material.imageview.ShapeableImageView
android:layout_width="101dp"
android:layout_height="93dp"
android:layout_gravity="center"
@ -39,7 +48,7 @@
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/crash_details_suggestion"
android:text="@string/crash_details_suggestion_v2"
android:textSize="14sp" />
<!-- copyable crash_details body with copy button in top right corner -->
@ -79,110 +88,36 @@
android:padding="4dp" />
</FrameLayout>
<!-- feedback form -->
<!-- feedback form placeholder. just tell the user we've receive a report if they have crash reporting on -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/feedback_placeholder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@string/feedback_placeholder"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_gravity="center|bottom"
android:orientation="horizontal">
<!-- feedback form header -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/feedback_text"
android:layout_width="match_parent"
<!-- restart button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/restart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/please_feedback"
android:textSize="18sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- feedback form name -->
<EditText
android:id="@+id/feedback_name"
android:enabled="false"
android:layout_width="320dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:hint="@string/feedback_name"
android:inputType="text"
android:autofillHints="name" />
<!-- feedback form email -->
<EditText
android:id="@+id/feedback_email"
android:layout_width="320dp"
android:layout_height="48dp"
android:enabled="false"
android:layout_margin="10dp"
android:hint="@string/feedback_email"
android:inputType="textEmailAddress"
android:autofillHints="emailAddress" />
<!-- feedback form message -->
<EditText
android:id="@+id/feedback_message"
android:enabled="false"
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:hint="@string/feedback_message"
android:inputType="textMultiLine"
android:importantForAutofill="no" />
android:text="@string/restart" />
</LinearLayout>
<!-- button group for submit feedback & restart / restart only -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- submit feedback button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/feedback_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:layout_margin="10dp"
android:text="@string/submit_feedback" />
<!-- restart button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/restart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@string/restart" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.button.MaterialButton
android:id="@+id/reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/reset_warning"
android:textSize="16sp" />
<!-- reset app button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@string/reset_app" />
</LinearLayout>
android:layout_margin="10dp"
android:text="@string/reset_app" />
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -11,7 +11,6 @@
android:layout_height="match_parent"
android:layout_margin="0dp"
android:padding="0dp"
app:fitsSystemWindowsInsets="start|end|bottom|top"
tools:context=".SetupActivity">
<ScrollView

@ -7,7 +7,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="left|right"
tools:context=".installer.InstallerActivity">
<HorizontalScrollView

@ -7,7 +7,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="left|right"
tools:context=".installer.InstallerActivity">
<androidx.recyclerview.widget.RecyclerView

@ -8,7 +8,6 @@
android:id="@+id/markdownBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="left|right"
tools:context=".markdown.MarkdownActivity">
<androidx.core.widget.NestedScrollView

@ -2,214 +2,203 @@
~ Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_view"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="3dp"
android:filterTouchesWhenObscured="true"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="1dp"
tools:ignore="RtlHardcoded,RtlSymmetry">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
style="@style/Widget.Material3.CardView.Elevated"
android:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="1dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="3dp"
android:filterTouchesWhenObscured="true"
android:gravity="center_vertical"
android:orientation="vertical"
tools:ignore="RtlHardcoded,RtlSymmetry">
<LinearLayout
android:id="@+id/main_card_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
android:padding="1dp"
app:layout_constraintTop_toTopOf="parent">
<!-- Module components -->
<LinearLayout
android:id="@+id/main_card_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="2dp"
app:layout_constraintTop_toTopOf="parent">
android:gravity="fill"
android:orientation="horizontal">
<!-- Module components -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="fill"
android:orientation="horizontal">
<com.google.android.material.chip.Chip
android:id="@+id/invalid_module_props"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:text="@string/low_quality_module"
android:textSize="12sp"
android:visibility="gone"
app:chipEndPadding="2dp"
app:chipIcon="@drawable/ic_baseline_error_24"
app:chipIconSize="15dp"
app:chipStartPadding="4dp" />
<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:maxLines="2"
android:paddingStart="4dp"
android:text="@string/loading"
android:textSize="19sp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:background="@android:color/transparent"
android:importantForAccessibility="no"
android:src="@drawable/ic_baseline_delete_forever_24"
android:textSize="16sp"
tools:ignore="RtlHardcoded" />
</LinearLayout>
<TextView
android:id="@+id/credit_text"
<com.google.android.material.chip.Chip
android:id="@+id/invalid_module_props"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:text="@string/low_quality_module"
android:textSize="12sp"
android:visibility="gone"
app:chipEndPadding="2dp"
app:chipIcon="@drawable/ic_baseline_error_24"
app:chipIconSize="15dp"
app:chipStartPadding="4dp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:maxLines="2"
android:paddingStart="4dp"
android:text="@string/loading"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
android:textSize="19sp" />
<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="2dp"
android:autoLink="all"
android:padding="4dp"
android:text="@string/loading"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/updated_text"
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginVertical="4dp"
android:paddingStart="4dp"
android:text="@string/loading"
android:textSize="12sp" />
android:layout_margin="2dp"
android:background="@null"
android:importantForAccessibility="no"
android:src="@drawable/ic_baseline_delete_forever_24"
android:textSize="16sp"
tools:ignore="RtlHardcoded,TouchTargetSizeCheck" />
</LinearLayout>
<TextView
android:id="@+id/module_layout_helper"
android:layout_width="match_parent"
<com.google.android.material.textview.MaterialTextView
android:id="@+id/credit_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/main_card_text">
</TextView>
<!-- Buttons -->
<HorizontalScrollView
android:id="@+id/module_options_holder"
android:paddingStart="4dp"
android:text="@string/loading"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:scrollbars="none"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/module_layout_helper">
android:layout_marginVertical="2dp"
android:autoLink="all"
android:padding="4dp"
android:text="@string/loading"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/updated_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginVertical="4dp"
android:paddingStart="4dp"
android:text="@string/loading"
android:textSize="12sp" />
</LinearLayout>
<!-- Buttons -->
<HorizontalScrollView
android:id="@+id/module_options_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="1dp"
android:scrollbars="none"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_card_text">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:animateLayoutChanges="true"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/button_action1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.ChipGroup
<com.google.android.material.chip.Chip
android:id="@+id/button_action2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:animateLayoutChanges="true"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/button_action1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:id="@+id/button_action6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:visibility="gone"
app:chipIcon="@drawable/ic_baseline_error_24" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

@ -6,8 +6,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:fitsSystemWindowsInsets="start|end|bottom|top">
android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"

@ -299,7 +299,7 @@
<string name="crash_reporting_pii_desc">السماح بإرسال معلومات إضافية في تقارير الأعطال ، والتي قد يحتوي بعضها على معلومات تعريف شخصية مثل عنوان IP ومعرفات الجهاز.</string>
<string name="setup_crash_reporting_pii_summary">قد يشمل ذلك معرفات الجهاز وعناوين IP. لن يتم استخدام أي بيانات لأي غرض آخر بخلاف تحليل الأعطال وتحسين الأداء.</string>
<string name="error_creating_cookie_database">خطأ في الوصول إلى WebView. قد تتأثر الوظيفة.</string>
<string name="analytics_desc">اسمح لنا بتتبع استخدام التطبيق وعمليات التثبيت. متوافق تمامًا مع إجمالي الناتج المحلي ويستخدم Matomo ، الذي مخزن عن طريق Androidacy.</string>
<string name="analytics_desc">اسمح لنا بتتبع استخدام التطبيق وعمليات التثبيت. متوافق تمامًا مع إجمالي الناتج المحلي ويستخدم Countly ، الذي مخزن عن طريق Androidacy.</string>
<string name="language_not_available">لم تتم ترجمة اللغة %s. أتود ترجمتها ؟</string>
<string name="blur_desc">إنشاء تأثير تمويه (Blur) خلف بعض الحوارات والعناصر. لاحظ أن التمويه (Blur) قد لا يعمل بشكل جيد على بعض الأجهزة وقد لا يعمل مع الجميع.</string>
<string name="error_encrypted_shared_preferences">حدث خطأ أثناء قراءة الخصائص المشتركة. الرجاء إعادة تعيين التطبيق.</string>

@ -304,7 +304,7 @@
<string name="empty_field">Je vyžadováná URL</string>
<string name="repo_already_added">Repozitář už existuje.</string>
<string name="showcase_mode_dialogue_message">K aktivaci režimu prezentace je vyžadován restart aplikace.</string>
<string name="analytics_desc">Umožněte nám sledovat používání a instalace aplikací. Plně kompatibilní s GDPR a používá Matomo, hostované společností Androidacy.</string>
<string name="analytics_desc">Umožněte nám sledovat používání a instalace aplikací. Plně kompatibilní s GDPR a používá Countly, hostované společností Androidacy.</string>
<string name="announcements">Novinky a aktualizace</string>
<string name="debug_cat">Ladění</string>
<string name="back">Zpátky</string>

@ -319,7 +319,7 @@
<string name="notification_update_wifi_pref">Απαίτηση wi-fi ή δικτύου χωρίς μετρήσεις για έλεγχο ενημερώσεων. Προτείνεται να είναι σε λειτουργία αν έχετε περιορισμένα δεδομένα κινητής τηλεφωνίας.</string>
<string name="keep_tapping_to_enter_hogwarts">Συνέχισε να πατάς, ώστε να γίνεις δεκτός στο Hogwarts!</string>
<string name="language_not_available">Η γλώσσα %s δεν έχει μεταφραστεί. Μπορείτε να μας βοηθήσετε στην μετάφρασή της;</string>
<string name="analytics_desc">Επιτρέψτε μας να παρακολουθούμε τη χρήση και τις εγκαταστάσεις εφαρμογών. Πλήρως συμβατό με το GDPR και χρησιμοποιεί το Matomo, που φιλοξενείται από το Androidacy.</string>
<string name="analytics_desc">Επιτρέψτε μας να παρακολουθούμε τη χρήση και τις εγκαταστάσεις εφαρμογών. Πλήρως συμβατό με το GDPR και χρησιμοποιεί το Countly, που φιλοξενείται από το Androidacy.</string>
<string name="background_update_check_excludes_version_summary">Προσδιορίστε μια έκδοση από ενημερώσεις modules που θα θέλατε να αγνοήσετε. Παρακαλείσθε να χρησιμοποιήσετε μόνο τον κωδικό έκδοσης. Μπορείτε να βρείτε αυτόν τον κωδικό κρατώντας πατημένη την έκδοση στην λίστα με τα modules. Χρησιμοποιήστε ^ στην αρχή για να αναφερθείτε στην καθορισμένη έκδοση και σε όλες τις νεότερες. Χρησιμοποιήστε $ στο τέλος για να αναφερθείτε σε όλες τις εκδόσεις πριν την καθορισμένη.</string>
<string name="crash_details_suggestion">Το stacktrace μπορεί να βρεθεί παρακάτω. Ωστόσο, <b>ανεπιφύλακτα</b> σας συνιστούμε να χρησιμοποιήσετε την παρακάτω φόρμα σχολίων για να υποβάλετε σχόλια. Με αυτόν τον τρόπο, αντί να αντιγράψετε χειροκίνητα το stacktrace, θα μας αποσταλεί αυτόματα. Επίσης, συμβάλλετε στην αποσυμφόρηση με αυτόν τον τρόπο και επιπλέον λεπτομέρειες αναφέρονται αυτόματα.</string>
<string name="reinstall">Επανεγκατάσταση</string>

@ -313,7 +313,7 @@
<string name="crash_reporting_pii_desc">Permite enviar información adicional en reportes de fallos, algunos de los cuales pueden contener información de identificación personal, como la dirección IP y los identificadores de dispositivo.</string>
<string name="setup_crash_reporting_pii_summary">Esto puede incluir identificadores de dispositivo y direcciones IP. No se utilizarán datos para ningún otro propósito que no sea analizar fallas y mejorar el rendimiento.</string>
<string name="showcase_mode_dialogue_message">Se requiere reiniciar la aplicación para activar el modo de exhibición.</string>
<string name="analytics_desc">Permítanos rastrear el uso y las instalaciones de la aplicación. Totalmente compatible con GDPR y utiliza Matomo, alojado por Androidacy.</string>
<string name="analytics_desc">Permítanos rastrear el uso y las instalaciones de la aplicación. Totalmente compatible con GDPR y utiliza Countly, alojado por Androidacy.</string>
<string name="reinstall">Reinstalar</string>
<string name="warning_pls_restart">Tenga en cuenta que es posible que algunas configuraciones no surtan efecto hasta que reinicie la aplicación.</string>
<string name="promo_code_copied">¡Use el código copiado para obtener la mitad de descuento en su primer mes!</string>

@ -292,7 +292,7 @@
<string name="empty_field">Se requiere una URL</string>
<string name="repo_already_added">Este repositorio ya existe.</string>
<string name="showcase_mode_dialogue_message">Se requiere reiniciar la aplicación para habilitar el modo de presentación.</string>
<string name="analytics_desc">Permítenos usar el seguimiento de uso e instalaciones. Totalmente compatible con el RGPD y utiliza Matomo, alojado por Androidacy.</string>
<string name="analytics_desc">Permítenos usar el seguimiento de uso e instalaciones. Totalmente compatible con el RGPD y utiliza Countly, alojado por Androidacy.</string>
<string name="announcements">Novedades y actualizaciones</string>
<string name="debug_cat">Depuración</string>
<string name="back">Atrás</string>

@ -309,7 +309,7 @@
<string name="empty_field">URL dibutuhkan</string>
<string name="repo_already_added">Repo sudah ada.</string>
<string name="showcase_mode_dialogue_message">Mulai ulang aplikasi dibutuhkan untuk mengaktifkan mode showcase.</string>
<string name="analytics_desc">Mengizinkan kami untuk mencatat penggunaan aplikasi dan pemasangan. Sepenuhnya mengikuti GDPR dan menggunakan Matomo, di hosting oleh Androidacy.</string>
<string name="analytics_desc">Mengizinkan kami untuk mencatat penggunaan aplikasi dan pemasangan. Sepenuhnya mengikuti GDPR dan menggunakan Countly, di hosting oleh Androidacy.</string>
<string name="debug_cat">Men-debug</string>
<string name="announcements">Berita dan pembaruan</string>
<string name="back">Kembali</string>

@ -310,7 +310,7 @@
<string name="error_creating_cookie_database">Errore nell\'accesso a WebView. La funzionalità potrebbe non essere disponibile.</string>
<string name="empty_field">L\'URL è richiesto</string>
<string name="repo_already_added">La repository esiste già.</string>
<string name="analytics_desc">Ci consente di tracciare l\'utilizzo della app e la quantità di installazioni. Conforme al GDPR, usa Matomo, un servizio offerto da Androidacy.</string>
<string name="analytics_desc">Ci consente di tracciare l\'utilizzo della app e la quantità di installazioni. Conforme al GDPR, usa Countly, un servizio offerto da Androidacy.</string>
<string name="debug_cat">Debug</string>
<string name="announcements">Notizie e aggiornamenti</string>
<string name="back">Indietro</string>

@ -299,7 +299,7 @@
<string name="empty_field">URL が必要です</string>
<string name="repo_already_added">リポジトリがすでに存在します。</string>
<string name="showcase_mode_dialogue_message">ショーケースモードを有効化するにはアプリを再起動してください。</string>
<string name="analytics_desc">アプリの使用状況とインストール済みモジュールの追跡を許可します。GDPR に完全に準拠し、Androidacy によって運営されている Matomo を使用します。</string>
<string name="analytics_desc">アプリの使用状況とインストール済みモジュールの追跡を許可します。GDPR に完全に準拠し、Androidacy によって運営されている Countly を使用します。</string>
<string name="debug_cat">デバッグ</string>
<string name="announcements">ニュースとアップデート</string>
<string name="back">戻る</string>

@ -295,7 +295,7 @@
<string name="empty_field">URL is vereist</string>
<string name="repo_already_added">Repo bestaat al.</string>
<string name="showcase_mode_dialogue_message">Een herstart van de app is vereist om de showcase-modus in te schakelen.</string>
<string name="analytics_desc">Sta ons toe app-gebruik en installaties bij te houden. Volledig GDPR-compatibel en maakt gebruik van Matomo, gehost door Androidacy.</string>
<string name="analytics_desc">Sta ons toe app-gebruik en installaties bij te houden. Volledig GDPR-compatibel en maakt gebruik van Countly, gehost door Androidacy.</string>
<string name="announcements">Nieuws en updates</string>
<string name="debug_cat">Debugging</string>
<string name="back">Ga terug</string>

@ -307,7 +307,7 @@
<string name="blur_desc">Tworzy efekt rozmycia za niektórymi oknami dialogowymi i elementami. Może nie działać dobrze na niektórych urządzeniach.</string>
<string name="error_encrypted_shared_preferences">Wystąpił błąd podczas odczytywania preferencji współdzielonych. Proszę zresetować aplikację.</string>
<string name="showcase_mode_dialogue_message">Aby włączyć tryb blokady, wymagany jest restart aplikacji.</string>
<string name="analytics_desc">Pozwól nam śledzić wykorzystanie aplikacji i instalacje. W pełni zgodne z GDPR i używa Matomo, hostowanego przez Androidacy.</string>
<string name="analytics_desc">Pozwól nam śledzić wykorzystanie aplikacji i instalacje. W pełni zgodne z GDPR i używa Countly, hostowanego przez Androidacy.</string>
<string name="announcements">Wiadomości i aktualizacje</string>
<string name="back">Wróć</string>
<string name="debug_cat">Debugowanie</string>

@ -311,7 +311,7 @@
<string name="setup_background_update_check_require_wifi_summary">Exigir wifi ou uma conexão ilimitada para buscar por atualizações</string>
<string name="clear_cache_dialogue_message">Isso irá limpar o cache do aplicativo. Suas preferências serão salvas, mas o aplicativo poderá demorar para realizar algumas operações temporariamente.</string>
<string name="showcase_mode_dialogue_message">Uma reinicialização é necessária para ativar o modo showcase.</string>
<string name="analytics_desc">Nos permite monitorar o uso e instalação do app. Totalmente conforme o GDPR, utilizando Matomo, hospedado pelo Androidacy.</string>
<string name="analytics_desc">Nos permite monitorar o uso e instalação do app. Totalmente conforme o GDPR, utilizando Countly, hospedado pelo Androidacy.</string>
<string name="debug_cat">Depuração</string>
<string name="announcements">Novidades e atualizações</string>
<string name="back">Voltar</string>

@ -300,7 +300,7 @@
<string name="androidacy_test_mode_warning">Você está configurando o aplicativo para usar um servidor de testes do Androidacy. Isso pode resultar em instabilidade no aplicativo e falha ao carregar o repositório online. NÃO reporte falhas se você tiver esta opção habilitada. O aplicativo será reinicializado para recarregar os repositórios.</string>
<string name="fox2code_thanks_desc">Fox2Code é o desenvolvedor original do aplicativo. Sem ele, isso nunca seria possível.</string>
<string name="showcase_mode_dialogue_message">Uma reinicialização é necessária para ativar o modo showcase.</string>
<string name="analytics_desc">Nos permite monitorar o uso e instalação do app. Totalmente conforme o GDPR, utilizando Matomo, hospedado pelo Androidacy.</string>
<string name="analytics_desc">Nos permite monitorar o uso e instalação do app. Totalmente conforme o GDPR, utilizando Countly, hospedado pelo Androidacy.</string>
<string name="debug_cat">Depuração</string>
<string name="announcements">Novidades e atualizações</string>
<string name="back">Voltar</string>

@ -302,7 +302,7 @@
<string name="blur_desc">Создаёт эффект размытия позади некоторых диалоговых окон и элементов. Обратите внимание, что размытие может на некоторых устройствах работать плохо и не во всех местах.</string>
<string name="error_encrypted_shared_preferences">Произошла ошибка при чтении общих настроек. Пожалуйста, перезагрузите приложение.</string>
<string name="showcase_mode_dialogue_message">Для включения режима демонстрации требуется перезапуск приложения.</string>
<string name="analytics_desc">Разрешите нам отслеживать использование и установки приложения. Полностью соответсвует GDPR и использует Matomo, размещенный на Androidacy.</string>
<string name="analytics_desc">Разрешите нам отслеживать использование и установки приложения. Полностью соответсвует GDPR и использует Countly, размещенный на Androidacy.</string>
<string name="debug_cat">Отладка</string>
<string name="announcements">Новости и обновления</string>
<string name="back">Назад</string>

@ -302,7 +302,7 @@
<string name="debug_cat">Відладка</string>
<string name="announcements">Новини та оновлення</string>
<string name="back">Назад</string>
<string name="analytics_desc">Дозвольте нам відстежувати використання та встановлення додатку. Повністю відповідає GDPR і використовує Matomo, розміщений на Androidacy.</string>
<string name="analytics_desc">Дозвольте нам відстежувати використання та встановлення додатку. Повністю відповідає GDPR і використовує Countly, розміщений на Androidacy.</string>
<string name="donate_fox">Донат на Fox2Code</string>
<string name="donate_androidacy">Дотан на Androidacy</string>
<string name="donate_androidacy_sum">Придбайте преміальну підписку на Androidacy, щоб підтримувати додаток і репозиторій.</string>

@ -22,7 +22,7 @@
<style name="Theme.MagiskModuleManager.Monet.Dark" parent="Theme.Material3.DynamicColors.Dark">
<item name="android:statusBarColor">@color/status_bar_color</item>
<item name="colorBackgroundFloating">@color/system_neutral1_800</item>
<item name="colorBackgroundFloating">@color/system_neutral1_700</item>
<item name="android:windowBackground">@color/system_neutral1_900</item>
<item name="chipStyle">@style/Widget.Material3.Chip.Assist.Elevated</item>
<item name="windowActionBar">false</item>
@ -36,7 +36,7 @@
<!-- Black monet theme, which is just dark monet theme with black background -->
<style name="Theme.MagiskModuleManager.Monet.Black" parent="Theme.MagiskModuleManager.Monet.Dark">
<item name="colorBackgroundFloating">@color/system_neutral2_900</item>
<item name="colorBackgroundFloating">@color/system_neutral1_900</item>
<item name="boxBackgroundColor">@color/black</item>
<item name="android:windowBackground">@color/black</item>
<item name="backgroundColor">@color/black</item>

@ -284,7 +284,7 @@
<string name="empty_field">URL là bắt buộc</string>
<string name="repo_already_added">Kho lưu trữ đã tồn tại.</string>
<string name="showcase_mode_dialogue_message">Cần khởi động lại ứng dụng để bật chế độ giới thiệu.</string>
<string name="analytics_desc">Cho phép chúng tôi theo dõi việc sử dụng và cài đặt ứng dụng. Hoàn toàn tuân thủ GDPR và sử dụng Matomo, được lưu trữ bởi Androidacy.</string>
<string name="analytics_desc">Cho phép chúng tôi theo dõi việc sử dụng và cài đặt ứng dụng. Hoàn toàn tuân thủ GDPR và sử dụng Countly, được lưu trữ bởi Androidacy.</string>
<string name="debug_cat">Gỡ lỗi</string>
<string name="announcements">Tin tức và cập nhật</string>
<string name="back">Quay lại</string>

@ -307,7 +307,7 @@
<string name="setup_crash_reporting_pii_summary">這可能會包含IP地址以及終端識別碼。收到的資料將不會用作分析錯誤以及提升服務品質以外的任何用途。</string>
<string name="empty_field">URL不能為空</string>
<string name="repo_already_added">倉庫已存在。</string>
<string name="analytics_desc">允許我們追踪應用的使用以及安裝。完全支援並符合GDPR且使用Androidacy運營的Matomo</string>
<string name="analytics_desc">允許我們追踪應用的使用以及安裝。完全支援並符合GDPR且使用Androidacy運營的Countly</string>
<string name="debug_cat">偵錯</string>
<string name="announcements">消息與更新</string>
<string name="back">返回</string>

@ -313,7 +313,7 @@
<string name="language_not_available">%s 尚未被翻译。 帮忙翻译一下\?</string>
<string name="blur_desc">在某些对话框和元素后面创建模糊效果。 请注意,模糊可能在某些设备上表现不佳并且可能不适用于所有人。</string>
<string name="error_encrypted_shared_preferences">读取共享首选项时出错。 请重置应用程序。</string>
<string name="analytics_desc">允许我们跟踪应用程序的使用和安装。完全符合 GDPR 并使用由 Androidacy 托管的 Matomo</string>
<string name="analytics_desc">允许我们跟踪应用程序的使用和安装。完全符合 GDPR 并使用由 Androidacy 托管的 Countly</string>
<string name="debug_cat">调试</string>
<string name="announcements">消息和更新</string>
<string name="back">返回</string>

@ -5,4 +5,7 @@
<resources>
<dimen name="card_corner_radius">8dp</dimen>
<dimen name="markdown_border_content">8dp</dimen>
<dimen name="dim5dp">5dp</dimen>
<dimen name="dp10">10dp</dimen>
<dimen name="dp20">20dp</dimen>
</resources>

@ -327,7 +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="analytics_desc">Allow us to track app usage and installs. Fully GDPR compliant and uses Matomo, hosted by Androidacy.</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>
<string name="back">Go back</string>
@ -398,4 +398,10 @@
<string name="setup_agree_eula_toast">Please agree to the terms first</string>
<string name="setup_androidacy_repo_recommendation">We recommend you to enable just the Androidacy repo. This ensures you will receive an optimized and more secure experience.</string>
<string name="install_terminal_support_link">Support: %s</string>
<string name="feedback_placeholder">We\'re sorry for this error. If you enabled crash reporting, we already received a report and are working on the issue. Feel free to restart the app when you\'re ready! You may also reset the app if you keep seeing this screen.</string>
<string name="crash_details_suggestion_v2">Below is the stacktrace. However, automatic reporting will automatically send this to us if enabled in settings. Hit the copy icon in the corner to copy it to your clipboard.</string>
<string name="androidacy_thanks_desc_v2">This app is maintained, updated, and supported by Androidacy, and names, logos, and trademarks are copyright Androidacy.</string>
<string name="androidacy_thanks_v2">An Androidacy app</string>
<string name="fox2code_thanks_v2">v0.x by Fox2Code</string>
<string name="fox2code_thanks_desc_v2">Versions before 0.6.8 were developed by Fox2Code and versions up to 1.1 were contributed to by him. We thank him for his work.</string>
</resources>

@ -1,30 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_contributors">
<!-- Small lil thanks to Androidacy -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:icon="@drawable/ic_baseline_android_24"
app:key="pref_androidacy_thanks"
app:singleLineTitle="false"
app:summary="@string/androidacy_thanks_desc"
app:title="@string/androidacy_thanks" />
app:summary="@string/androidacy_thanks_desc_v2"
app:title="@string/androidacy_thanks_v2" />
<!-- Small lil thanks to Fox2Code -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:key="pref_fox2code_thanks"
app:singleLineTitle="false"
app:summary="@string/fox2code_thanks_desc"
app:title="@string/fox2code_thanks" />
app:summary="@string/fox2code_thanks_desc_v2"
app:title="@string/fox2code_thanks_v2" />
<!-- OKay, so we'll thank all the other contributors too -->
<com.fox2code.mmm.settings.LongClickablePreference
android:textAppearance="?android:attr/textAppearanceSmall"
app:iconSpaceReserved="false"
app:key="pref_contributors"
app:singleLineTitle="false"
app:title="@string/contributors" />
app:summary="@string/contributors" />
<!-- And the translators -->
</PreferenceCategory>

@ -14,22 +14,11 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.8.3")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath("org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin:2.7.2")
}
}
subprojects {
plugins.withType<com.android.build.gradle.api.AndroidBasePlugin> {
apply(plugin = "org.gradle.android.cache-fix")
}
}
tasks.register("clean", Delete::class) {
delete(layout.buildDirectory)
}

Loading…
Cancel
Save