(misc) optimizations and fixes

Signed-off-by: androidacy-user <opensource@androidacy.com>
master
androidacy-user 1 year ago
parent d55a75bcec
commit 216d4ea4ad

@ -2,7 +2,7 @@ import io.sentry.android.gradle.extensions.InstrumentationFeature
plugins {
// Gradle doesn't allow conditionally enabling/disabling plugins
id "io.sentry.android.gradle" version "3.4.2"
id "io.sentry.android.gradle"
id 'com.android.application'
id 'com.mikepenz.aboutlibraries.plugin'
id 'org.jetbrains.kotlin.android'
@ -129,8 +129,14 @@ android {
buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_ANALYTICS", "true" // unused for now
buildConfigField "boolean", "DEFAULT_ENABLE_ANALYTICS_PII", "true" // unused for now
buildConfigField "boolean", "DEFAULT_ENABLE_ANALYTICS", "true"
Properties propertiesL = new Properties()
if (project.rootProject.file('local.properties').exists()) {
// grab matomo.url
buildConfigField "String", "ANALYTICS_ENDPOINT", '"' + propertiesL.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"
if (hasSentryConfig) {
Properties properties = new Properties()
@ -186,7 +192,15 @@ android {
// Disable crash reporting for F-Droid flavor by default
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "false"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "false"
buildConfigField "boolean", "DEFAULT_ENABLE_ANALYTICS", "false"
Properties propertiesL = new Properties()
if (project.rootProject.file('local.properties').exists()) {
// grab matomo.url
buildConfigField "String", "ANALYTICS_ENDPOINT", '"' + propertiesL.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"
if (hasSentryConfig) {
@ -223,7 +237,7 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
disable 'MissingTranslation'
}
@ -372,6 +386,9 @@ dependencies {
implementation 'commons-io:commons-io:20030203.000550'
implementation 'org.apache.commons:commons-compress:1.23.0'
// analytics
implementation 'com.github.matomo-org:matomo-sdk-android:4.1.4'
}
if (hasSentryConfig) {

@ -2,7 +2,6 @@ package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.ClipboardManager;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
@ -15,16 +14,10 @@ import com.fox2code.foxcompat.app.FoxApplication;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textview.MaterialTextView;
import org.chromium.net.CronetEngine;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import io.sentry.Sentry;
import io.sentry.UserFeedback;
import timber.log.Timber;
public class CrashHandler extends FoxActivity {
@ -65,7 +58,6 @@ public class CrashHandler extends FoxActivity {
stacktrace = stacktrace.replace(",", "\n ");
crashDetails.setText(getString(R.string.crash_full_stacktrace, stacktrace));
}
SharedPreferences preferences = MainApplication.getPreferences("sentry");
String lastEventId = getIntent().getStringExtra("lastEventId");
Timber.d("CrashHandler.onCreate: lastEventId=%s, crashReportingEnabled=%s", lastEventId, crashReportingEnabled);
if (lastEventId == null && crashReportingEnabled) {
@ -75,11 +67,6 @@ public class CrashHandler extends FoxActivity {
} else {
// if lastEventId is not null, show the feedback button
findViewById(R.id.feedback).setVisibility(View.VISIBLE);
// set the name and email fields to the saved values
EditText name = findViewById(R.id.feedback_name);
EditText email = findViewById(R.id.feedback_email);
name.setText(preferences.getString("name", ""));
email.setText(preferences.getString("email", ""));
}
// disable feedback if sentry is disabled
//noinspection ConstantConditions
@ -98,67 +85,33 @@ public class CrashHandler extends FoxActivity {
// if email or name is empty, use "Anonymous"
final String[] nameString = {name.getText().toString().equals("") ? "Anonymous" : name.getText().toString()};
final String[] emailString = {email.getText().toString().equals("") ? "Anonymous" : email.getText().toString()};
// Prevent strict mode violation
// create sentry userFeedback request
try {
URL.setURLStreamHandlerFactory(new CronetEngine.Builder(this).build().createURLStreamHandlerFactory());
} catch (Error ignored) {
// Ignore
}
// get sentryException passed in intent
Throwable sentryException = (Throwable) getIntent().getSerializableExtra("sentryException");
new Thread(() -> {
try {
HttpURLConnection connection = (HttpURLConnection) new URL("https" + "://sentry.androidacy.com/api/0/projects/sentry/foxmmm/user-feedback/").openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + BuildConfig.SENTRY_TOKEN);
UserFeedback userFeedback = new UserFeedback(Sentry.captureException(sentryException));
// Setups the JSON body
if (nameString[0].equals("")) nameString[0] = "Anonymous";
if (emailString[0].equals("")) emailString[0] = "Anonymous";
JSONObject body = new JSONObject();
body.put("event_id", lastEventId);
body.put("name", nameString[0]);
body.put("email", emailString[0]);
body.put("comments", description.getText().toString());
// Send the request
connection.setDoOutput(true);
OutputStream outputStream = connection.getOutputStream();
outputStream.write(body.toString().getBytes());
outputStream.flush();
outputStream.close();
// get response body
byte[] response;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
response = connection.getInputStream().readAllBytes();
// convert response to string
String responseBody = new String(response);
// log the response body
Timber.d("Response Body: %s", responseBody);
}
// close and disconnect the connection
connection.getInputStream().close();
connection.disconnect();
// For debug builds, log the response code and response body
Timber.d("Response Code: %s", connection.getResponseCode());
// Check if the request was successful
if (connection.getResponseCode() == 200) {
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show());
} else {
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show());
}
} catch (JSONException | IOException ignored) {
userFeedback.setName(nameString[0]);
userFeedback.setEmail(emailString[0]);
userFeedback.setComments(description.getText().toString());
Sentry.captureUserFeedback(userFeedback);
Timber.i("Submitted user feedback: name %s email %s comment %s", nameString[0], emailString[0], description.getText().toString());
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show());
// Close the activity
finish();
// start the main activity
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
} catch (Exception e) {
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();
// Close the activity
finish();
// start the main activity
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
// get restart button
findViewById(R.id.restart).setOnClickListener(v -> {
// Save the user's name and email
preferences.edit().putString("name", name.getText().toString()).putString("email", email.getText().toString()).apply();
// Restart the app
finish();
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));

@ -1,6 +1,7 @@
package com.fox2code.mmm;
import static com.fox2code.mmm.MainApplication.isOfficial;
import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE;
import android.Manifest;
import android.animation.Animator;
@ -48,16 +49,20 @@ import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ReposList;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
import org.chromium.net.CronetEngine;
import org.matomo.sdk.extra.TrackHelper;
import java.net.URL;
import java.util.Objects;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper {
@ -113,6 +118,20 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
}
BackgroundUpdateChecker.onMainActivityCreate(this);
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
// track enabled repos
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build();
Realm realm = Realm.getInstance(realmConfig);
StringBuilder enabledRepos = new StringBuilder();
realm.executeTransaction(r -> {
for (ReposList r2 : r.where(ReposList.class).equalTo("enabled", true).findAll()) {
enabledRepos.append(r2.getUrl()).append(":").append(r2.getName()).append(",");
}
});
if (enabledRepos.length() > 0) {
enabledRepos.setLength(enabledRepos.length() - 1);
}
TrackHelper.track().event("enabled_repos", enabledRepos.toString()).with(MainApplication.getINSTANCE().getTracker());
// log all shared preferences that are present
if (!isOfficial) {
Timber.w("You may be running an untrusted build.");
@ -180,10 +199,12 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
bottomNavigationView.setOnItemSelectedListener(item -> {
if (item.getItemId() == R.id.settings_menu_item) {
TrackHelper.track().event("view_list", "settings").with(MainApplication.getINSTANCE().getTracker());
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
} else if (item.getItemId() == R.id.online_menu_item) {
TrackHelper.track().event("view_list", "online_modules").with(MainApplication.getINSTANCE().getTracker());
// set module_list_online as visible and module_list as gone. fade in/out
this.moduleListOnline.setAlpha(0F);
this.moduleListOnline.setVisibility(View.VISIBLE);
@ -197,6 +218,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
// clear search view
this.searchView.setQuery("", false);
} else if (item.getItemId() == R.id.installed_menu_item) {
TrackHelper.track().event("view_list", "installed_modules").with(MainApplication.getINSTANCE().getTracker());
// set module_list_online as gone and module_list as visible. fade in/out
this.moduleList.setAlpha(0F);
this.moduleList.setVisibility(View.VISIBLE);
@ -223,7 +245,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
rootContainer.setLayoutParams(params);
rootContainer.setY(0F);
});
// reset update module and update module Count in mainapplication
// reset update module and update module count in main application
MainApplication.getINSTANCE().resetUpdateModule();
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
@ -326,7 +348,11 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (max != 0) {
int current = 0;
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
// if it has updateJson and FLAG_MM_REMOTE_MODULE is not set on flags, check for json update
// this is a dirty hack until we better store if it's a remote module
// the reasoning is that remote repos are considered "validated" while local modules are not
// for instance, a potential attacker could hijack a perfectly legitimate module and inject an updateJson with a malicious update - thereby bypassing any checks repos may have, without anyone noticing until it's too late
if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
try {
localModuleInfo.checkModuleUpdate();
@ -528,7 +554,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (max != 0) {
int current = 0;
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
try {
localModuleInfo.checkModuleUpdate();
@ -559,6 +585,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
public boolean onQueryTextSubmit(final String query) {
this.searchView.clearFocus();
if (this.initMode) return false;
TrackHelper.track().event("search", query).with(MainApplication.getINSTANCE().getTracker());
if (this.moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query);
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
@ -574,6 +601,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
@Override
public boolean onQueryTextChange(String query) {
if (this.initMode) return false;
TrackHelper.track().event("search_type", query).with(MainApplication.getINSTANCE().getTracker());
if (this.moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query);
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();

@ -38,6 +38,11 @@ import com.fox2code.rosettax.LanguageSwitcher;
import com.google.common.hash.Hashing;
import com.topjohnwu.superuser.Shell;
import org.matomo.sdk.Matomo;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.TrackerBuilder;
import org.matomo.sdk.extra.TrackHelper;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -90,8 +95,9 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
@SuppressLint("RestrictedApi")
// Use FoxProcess wrapper helper.
private static final boolean wrapped = !FoxProcessExt.isRootLoader();
private static String SHOWCASE_MODE_TRUE = null;
private static final ArrayList<String> callers = new ArrayList<>();
public static boolean isOfficial = false;
private static String SHOWCASE_MODE_TRUE = null;
private static long secret;
private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().getLocales().get(0);
private static SimpleDateFormat timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale);
@ -100,8 +106,6 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
private static MainApplication INSTANCE;
private static boolean firstBoot;
private static HashMap<Object, Object> mSharedPrefs;
private static final ArrayList<String> callers = new ArrayList<>();
public boolean modulesHaveUpdates = false;
static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR).setTimeout(10).setInitializers(InstallerInitializer.class));
@ -111,14 +115,16 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
} while (secret == 0);
}
public boolean modulesHaveUpdates = false;
public int updateModuleCount = 0;
public List<String> updateModules = new ArrayList<>();
public List<String> updateModules = new ArrayList<>();
public boolean isMatomoAllowed;
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
private byte[] existingKey;
private Tracker tracker;
public MainApplication() {
if (INSTANCE != null && INSTANCE != this)
@ -356,6 +362,16 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
return !this.isLightTheme();
}
@SuppressWarnings("UnusedReturnValue")
public synchronized Tracker getTracker() {
if (tracker == null) {
tracker = TrackerBuilder.createDefault(BuildConfig.ANALYTICS_ENDPOINT, 1).build(Matomo.getInstance(this));
tracker.setDispatchTimeout(10);
tracker.setDispatchInterval(1000);
}
return tracker;
}
@Override
public void onCreate() {
supportedLocales.add("ar");
@ -410,6 +426,31 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
Timber.d("Initializing Realm");
Realm.init(this);
Timber.d("Initialized Realm");
// analytics
Timber.d("Initializing matomo");
isMatomoAllowed = isMatomoAllowed();
// add preference listener to set isMatomoAllowed
getPreferences("mmm").registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> {
if (key.equals("pref_analytics_enabled")) {
isMatomoAllowed = sharedPreferences.getBoolean(key, false);
tracker.setOptOut(isMatomoAllowed);
Timber.d("Matomo is allowed change: %s", isMatomoAllowed);
}
});
getTracker();
if (!isMatomoAllowed) {
Timber.d("Matomo is not allowed");
tracker.setOptOut(true);
} else {
tracker.setOptOut(false);
}
if (getPreferences("matomo").getBoolean("install_tracked", false)) {
TrackHelper.track().download().with(MainApplication.getINSTANCE().getTracker());
Timber.d("Sent install event to matomo");
getPreferences("matomo").edit().putBoolean("install_tracked", true).apply();
} else {
Timber.d("Matomo already has install");
}
// Determine if this is an official build based on the signature
try {
// Get the signature of the key used to sign the app
@ -461,6 +502,10 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
}
}
private boolean isMatomoAllowed() {
return getPreferences("mmm").getBoolean("pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS);
}
@SuppressWarnings("unused")
private Intent getIntent() {
return this.getPackageManager().getLaunchIntentForPackage(this.getPackageName());

@ -49,12 +49,6 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setTitle(R.string.setup_title);
// set action bar
/**ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// back button is close button
actionBar.hide();
}*/
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0);
createFiles();
disableUpdateActivityForFdroidFlavor();
@ -75,6 +69,8 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
// pref_crash_reporting_pii
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING_PII);
// pref_analytics_enabled
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).setChecked(BuildConfig.DEFAULT_ENABLE_ANALYTICS);
// assert that both switches match the build config on debug builds
if (BuildConfig.DEBUG) {
assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked() == BuildConfig.ENABLE_AUTO_UPDATER;
@ -175,6 +171,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked());
// Set the crash reporting PII pref
editor.putBoolean("pref_crash_reporting_pii", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).isChecked());
editor.putBoolean("pref_analytics_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).isChecked());
Timber.d("Saving preferences");
// Set the repos in the ReposList realm db
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
@ -375,6 +372,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/cronet"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/repos/magisk_alt_repo"));
} catch (IOException e) {
Timber.e(e);
}

@ -6,12 +6,10 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebView;
import androidx.core.content.FileProvider;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.app.FoxApplication;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.LinearProgressIndicator;
@ -20,6 +18,7 @@ import com.google.android.material.textview.MaterialTextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.matomo.sdk.extra.TrackHelper;
import java.io.File;
import java.io.FileOutputStream;
@ -35,6 +34,9 @@ public class UpdateActivity extends FoxActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (MainApplication.getINSTANCE().isMatomoAllowed) {
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
}
setContentView(R.layout.activity_update);
// Get the progress bar and make it indeterminate for now
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);

@ -37,6 +37,8 @@ import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import org.matomo.sdk.extra.TrackHelper;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
@ -72,6 +74,7 @@ public final class AndroidacyActivity extends FoxActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
this.moduleFile = new File(this.getCacheDir(), "module.zip");
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
Intent intent = this.getIntent();
Uri uri;
if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) {

@ -48,12 +48,16 @@ public class BackgroundUpdateChecker extends Worker {
public static final String NOTIFICATION_CHANNEL_ID = "background_update";
public static final int NOTIFICATION_ID = 1;
public static final String NOTFIICATION_GROUP = "updates";
static final Object lock = new Object(); // Avoid concurrency issuespublic static final String NOTIFICATION_CHANNEL_ID = "background_update";
public static final String NOTIFICATION_CHANNEL_ID_APP = "background_update_app";
static final Object lock = new Object(); // Avoid concurrency issuespublic static final String NOTIFICATION_CHANNEL_ID = "background_update";
private static final int NOTIFICATION_ID_ONGOING = 2;
private static final String NOTIFICATION_CHANNEL_ID_ONGOING = "mmm_background_update";
private static final int NOTIFICATION_ID_APP = 3;
public BackgroundUpdateChecker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@SuppressLint("RestrictedApi")
private static void postNotificationForAppUpdate(Context context) {
// create the notification channel if not already created
@ -82,25 +86,21 @@ public class BackgroundUpdateChecker extends Worker {
}
}
public BackgroundUpdateChecker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
static void doCheck(Context context) {
// first, check if the user has enabled background update checking
if (!MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check", false)) {
return;
}
//if (MainApplication.getINSTANCE().isInForeground()) {
if (MainApplication.getINSTANCE().isInForeground()) {
// don't check if app is in foreground, this is a background check
// return;
//}
return;
}
// next, check if user requires wifi
if (MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check_wifi", true)) {
// check if wifi is connected
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network networkInfo = connectivityManager.getActiveNetwork();
if (networkInfo == null || !connectivityManager.getNetworkCapabilities(networkInfo).hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
if (networkInfo == null || !connectivityManager.getNetworkCapabilities(networkInfo).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
Timber.w("Background update check: wifi not connected but required");
return;
}
@ -110,7 +110,7 @@ public class BackgroundUpdateChecker extends Worker {
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_MIN).setName(context.getString(R.string.notification_channel_category_background_update)).setDescription(context.getString(R.string.notification_channel_category_background_update_description)).setGroup(NOTFIICATION_GROUP).build());
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_ONGOING);
builder.setSmallIcon(R.drawable.ic_baseline_update_24);
builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
@ -224,6 +224,11 @@ public class BackgroundUpdateChecker extends Worker {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context);
notificationManagerCompat.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH).setShowBadge(true).setName(context.getString(R.string.notification_update_pref)).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(NOTFIICATION_GROUP).build());
notificationManagerCompat.cancel(BackgroundUpdateChecker.NOTIFICATION_ID);
// now for the ongoing notification
notificationManagerCompat.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_MIN).setShowBadge(true).setName(context.getString(R.string.notification_update_pref)).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(NOTFIICATION_GROUP).build());
notificationManagerCompat.cancel(BackgroundUpdateChecker.NOTIFICATION_ID_ONGOING);
// schedule periodic check for updates every 6 hours (6 * 60 * 60 = 21600)
Timber.d("Scheduling periodic background check");
WorkManager.getInstance(context).enqueueUniquePeriodicWork("background_checker", ExistingPeriodicWorkPolicy.UPDATE, new PeriodicWorkRequest.Builder(BackgroundUpdateChecker.class, 6, TimeUnit.HOURS).setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).build()).build());
}

@ -47,6 +47,7 @@ import com.topjohnwu.superuser.io.SuFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader;
import java.io.File;
@ -79,6 +80,7 @@ public class InstallerActivity extends FoxActivity {
if (!this.moduleCache.exists() && !this.moduleCache.mkdirs())
Timber.e("Failed to mkdir module cache dir!");
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true);
setActionBarBackground(null);
this.setOnBackPressedCallback(a -> {

@ -25,7 +25,7 @@ public class LocalModuleInfo extends ModuleInfo {
}
public void checkModuleUpdate() {
if (this.updateJson != null) {
if (this.updateJson != null && (this.flags & FLAG_MM_REMOTE_MODULE) == 0) {
try {
JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet(
this.updateJson, true), StandardCharsets.UTF_8));

@ -22,6 +22,7 @@ public class ModuleInfo {
public static final int FLAG_METADATA_INVALID = 0x80000000;
public static final int FLAG_CUSTOM_INTERNAL = 0x40000000;
public static final int FLAG_MM_REMOTE_MODULE = 0x20000000;
private static final int FLAG_FENCE = 0x10000000; // Should never be set
// Magisk standard

@ -35,6 +35,7 @@ public final class ModuleManager extends SyncManager {
private static final int FLAGS_KEEP_INIT = FLAG_MM_UNPROCESSED | ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private static final ModuleManager INSTANCE = new ModuleManager();
private static final int FLAG_MM_REMOTE_MODULE = ModuleInfo.FLAG_MM_REMOTE_MODULE;
private final HashMap<String, LocalModuleInfo> moduleInfos;
private final SharedPreferences bootPrefs;
private int updatableModuleCount = 0;
@ -88,7 +89,6 @@ public final class ModuleManager extends SyncManager {
// get all dirs under the realms/repos/ dir under app's data dir
File cacheRoot = new File(MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/").toURI());
ModuleListCache moduleListCache;
boolean foundCache = false;
for (File dir : Objects.requireNonNull(cacheRoot.listFiles())) {
if (dir.isDirectory()) {
// if the dir name matches the module name, use it as the cache dir
@ -99,18 +99,18 @@ public final class ModuleManager extends SyncManager {
Timber.d("Looking for cache for %s out of %d", module, realm.where(ModuleListCache.class).count());
moduleListCache = realm.where(ModuleListCache.class).equalTo("codename", module).findFirst();
if (moduleListCache != null) {
foundCache = true;
Timber.d("Found cache for %s", module);
// get module info from cache
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
}
moduleInfo.name = moduleListCache.getName();
moduleInfo.description = moduleListCache.getDescription() + " (cached)";
moduleInfo.author = moduleListCache.getAuthor();
moduleInfo.safe = moduleListCache.isSafe();
moduleInfo.support = moduleListCache.getSupport();
moduleInfo.donate = moduleListCache.getDonate();
moduleInfo.name = !Objects.equals(moduleListCache.getName(), "") ? moduleListCache.getName() : module;
moduleInfo.description = !Objects.equals(moduleListCache.getDescription(), "") ? moduleListCache.getDescription() : null;
moduleInfo.author = !Objects.equals(moduleListCache.getAuthor(), "") ? moduleListCache.getAuthor() : null;
moduleInfo.safe = Objects.equals(moduleListCache.isSafe(), true);
moduleInfo.support = !Objects.equals(moduleListCache.getSupport(), "") ? moduleListCache.getSupport() : null;
moduleInfo.donate = !Objects.equals(moduleListCache.getDonate(), "") ? moduleListCache.getDonate() : null;
moduleInfo.flags |= FLAG_MM_REMOTE_MODULE;
moduleInfos.put(module, moduleInfo);
realm.close();
break;
@ -185,7 +185,7 @@ public final class ModuleManager extends SyncManager {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
}
if (moduleInfo.updateJson != null) {
if (moduleInfo.updateJson != null && (moduleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
this.updatableModuleCount++;
} else {
moduleInfo.updateVersion = null;

@ -27,6 +27,8 @@ import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.matomo.sdk.extra.TrackHelper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
@ -67,6 +69,7 @@ public class MarkdownActivity extends FoxActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true);
Intent intent = this.getIntent();
if (!MainApplication.checkSecret(intent)) {

@ -25,9 +25,12 @@ import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.matomo.sdk.extra.TrackHelper;
import io.noties.markwon.Markwon;
import timber.log.Timber;
@SuppressWarnings("ReplaceNullCheck")
@SuppressLint("UseCompatLoadingForDrawables")
public enum ActionButtonType {
INFO() {
@ -39,6 +42,13 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker());
String notesUrl = moduleHolder.repoModule.notesUrl;
if (AndroidacyUtil.isAndroidacyLink(notesUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig());
@ -70,6 +80,14 @@ public enum ActionButtonType {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
if (moduleInfo == null)
return;
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("view_update_install", name).with(MainApplication.getINSTANCE().getTracker());
String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null)
return;
@ -132,6 +150,13 @@ public enum ActionButtonType {
doActionLong(button, moduleHolder);
return;
}
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("uninstall_module", name).with(MainApplication.getINSTANCE().getTracker());
Timber.i(Integer.toHexString(moduleHolder.moduleInfo.flags));
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Timber.e("Failed to switch uninstalled state!");
@ -174,6 +199,14 @@ public enum ActionButtonType {
String config = moduleHolder.getMainModuleConfig();
if (config == null)
return;
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker());
if (AndroidacyUtil.isAndroidacyLink(config)) {
IntentHelper.openUrlAndroidacy(button.getContext(), config, true);
} else {
@ -190,6 +223,14 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
}
}, DONATE() {
@ -202,6 +243,13 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
}
}, WARNING() {
@ -213,6 +261,13 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("warning_module", name).with(MainApplication.getINSTANCE().getTracker());
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show();
}
@ -226,6 +281,13 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("safe_module", name).with(MainApplication.getINSTANCE().getTracker());
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.safe_module).setMessage(R.string.safe_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show();
}

@ -80,6 +80,7 @@ import com.mikepenz.aboutlibraries.LibsBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.apache.commons.io.FileUtils;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader;
import java.io.File;
@ -112,6 +113,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
private final NavigationBarView.OnItemSelectedListener onItemSelectedListener = item -> {
int itemId = item.getItemId();
if (itemId == R.id.back) {
TrackHelper.track().event("view_list", "main_modules").with(MainApplication.getINSTANCE().getTracker());
startActivity(new Intent(this, MainActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
@ -161,6 +163,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
protected void onCreate(Bundle savedInstanceState) {
devModeStep = 0;
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
setContentView(R.layout.settings_activity);
setTitle(R.string.app_name);
//hideActionBar();
@ -936,6 +939,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
ReposList reposList1 = realm2.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst();
if (reposList1 != null) {
reposList1.setEnabled(Boolean.parseBoolean(String.valueOf(newValue)));
} else {
Timber.e("Alt Repo not found in realm db");
}
});
return true;

@ -68,6 +68,7 @@ public enum IntentHelper {
}
public static void openUrl(Context context, String url, boolean forceBrowser) {
Timber.d("Opening url: %s, forced browser %b", url, forceBrowser);
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
myIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -76,12 +77,14 @@ public enum IntentHelper {
}
startActivity(context, myIntent, false);
} catch (ActivityNotFoundException e) {
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show();
}
}
public static void openCustomTab(Context context, String url) {
Timber.d("Opening url: %s in custom tab", url);
try {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -90,6 +93,7 @@ public enum IntentHelper {
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivityEx(context, tabIntent, viewIntent);
} catch (ActivityNotFoundException e) {
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show();
}

@ -10,6 +10,8 @@ import com.fox2code.mmm.CrashHandler;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import org.matomo.sdk.extra.TrackHelper;
import java.util.Objects;
import io.sentry.Sentry;
@ -20,6 +22,7 @@ import timber.log.Timber;
public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true;
public static boolean isCrashing = false;
private static boolean sentryEnabled = false;
/**
@ -29,6 +32,8 @@ public class SentryMain {
@SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"})
public static void initialize(final MainApplication mainApplication) {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
isCrashing = true;
TrackHelper.track().exception(throwable).with(MainApplication.getINSTANCE().getTracker());
SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("sentry", Context.MODE_PRIVATE).edit();
editor.putString("lastExitReason", "crash");
editor.putLong("lastExitTime", System.currentTimeMillis());
@ -44,19 +49,28 @@ public class SentryMain {
intent.putExtra("stacktrace", throwable.getStackTrace());
// put lastEventId in intent (get from preferences)
intent.putExtra("lastEventId", String.valueOf(Sentry.getLastEventId()));
// serialize Sentry.captureException and pass it to the crash handler
intent.putExtra("sentryException", throwable);
// pass crashReportingEnabled to crash handler
intent.putExtra("crashReportingEnabled", isSentryEnabled());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Timber.e("Starting crash handler");
mainApplication.startActivity(intent);
Timber.e("Exiting");
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
});
// If first_launch pref is not false, refuse to initialize Sentry
SharedPreferences sharedPreferences = MainApplication.getPreferences("sentry");
if (!Objects.equals(MainApplication.getPreferences("mmm").getString("last_shown_setup", null), "v1")) {
SharedPreferences sharedPreferences = MainApplication.getPreferences("mmm");
if (!Objects.equals(sharedPreferences.getString("last_shown_setup", null), "v1")) {
return;
}
sentryEnabled = sharedPreferences.getBoolean("pref_crash_reporting_enabled", false);
// set sentryEnabled on preference change of pref_crash_reporting_enabled
sharedPreferences.registerOnSharedPreferenceChangeListener((sharedPreferences1, s) -> {
if (s.equals("pref_crash_reporting_enabled")) {
sentryEnabled = sharedPreferences1.getBoolean(s, false);
}
});
SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here.
if (!MainApplication.isCrashReportingEnabled()) {
@ -87,28 +101,25 @@ public class SentryMain {
options.setAttachScreenshot(true);
// It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
options.setEnableAutoSessionTracking(true);
// disable crash tracking - we handle that ourselves
options.setEnableUncaughtExceptionHandler(false);
// 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.setBeforeSend((event, 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 (!MainApplication.isCrashReportingEnabled()) {
if (!sentryEnabled) {
return null;
}
if (isCrashing) {
return null;
}
// Save lastEventId to private shared preferences
SharedPreferences sentryPrefs = MainApplication.getPreferences("sentry");
String lastEventId = Objects.requireNonNull(event.getEventId()).toString();
SharedPreferences.Editor editor = sentryPrefs.edit();
editor.putString("lastEventId", lastEventId);
editor.apply();
return event;
});
// Filter breadcrumb content from crash report.
options.setBeforeBreadcrumb((breadcrumb, hint) -> {
String url = (String) breadcrumb.getData("url");
if (url == null || url.isEmpty())
return breadcrumb;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost()))
return null;
if (url == null || url.isEmpty()) return breadcrumb;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost())) return null;
if (AndroidacyUtil.isAndroidacyLink(url)) {
breadcrumb.setData("url", AndroidacyUtil.hideToken(url));
}

@ -200,6 +200,28 @@
android:text="@string/setup_crash_reporting_pii_summary"
android:textAppearance="@android:style/TextAppearance.Material.Small" />
<!-- Placeholder for future settings -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_app_analytics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"
android:key="pref_app_analytics"
android:text="@string/setup_app_analytics"
android:textSize="18sp"
android:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:text="@string/analytics_desc"
android:drawableStart="@drawable/ic_baseline_info_24"
android:drawablePadding="8dp" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -250,18 +272,6 @@
android:textAppearance="@android:style/TextAppearance.Material.Small"
app:icon="@drawable/ic_baseline_info_24" />
<!-- Placeholder for future settings -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/setup_app_analytics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:checked="false"
android:key="pref_app_analytics"
android:text="@string/setup_app_analytics"
android:textAppearance="@android:style/TextAppearance.Material.Small"
android:visibility="gone" />
<!-- licenses, disclaimers, and EULA -->
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"

@ -403,4 +403,5 @@
<string name="showcase_mode_dialogue_message">An app restart is required to enable showcase mode.</string>
<string name="other_section">Other</string>
<string name="eula_agree">By clicking "finish", you are agreeing 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/)</string>
<string name="analytics_desc">Allow us to track app usage and installs. Fully GDPR compliant and uses Matomo, hosted by Androidacy.</string>
</resources>

@ -192,6 +192,14 @@
app:singleLineTitle="false"
app:summary="@string/crash_reporting_pii_desc"
app:title="@string/crash_reporting_pii" />
<!-- analytics enabled -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_info_24"
app:singleLineTitle="false"
app:key="pref_analytics_enabled"
app:summary="@string/analytics_desc"
app:title="@string/setup_app_analytics" />
<!-- Purposely crash the app -->
<Preference
app:icon="@drawable/ic_baseline_bug_report_24"

@ -27,6 +27,7 @@ buildscript {
// in the individual module build.gradle files
//noinspection GradleDependency
classpath "io.realm:realm-gradle-plugin:10.13.3-transformer-api"
classpath 'io.sentry:sentry-android-gradle-plugin:3.4.2'
}
}

Loading…
Cancel
Save