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

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

@ -1,6 +1,7 @@
package com.fox2code.mmm; package com.fox2code.mmm;
import static com.fox2code.mmm.MainApplication.isOfficial; import static com.fox2code.mmm.MainApplication.isOfficial;
import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE;
import android.Manifest; import android.Manifest;
import android.animation.Animator; import android.animation.Animator;
@ -48,16 +49,20 @@ import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity; import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.ExternalHelper; import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.io.net.Http; 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.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import org.chromium.net.CronetEngine; import org.chromium.net.CronetEngine;
import org.matomo.sdk.extra.TrackHelper;
import java.net.URL; import java.net.URL;
import java.util.Objects; import java.util.Objects;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber; import timber.log.Timber;
public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper { 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); BackgroundUpdateChecker.onMainActivityCreate(this);
super.onCreate(savedInstanceState); 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 // log all shared preferences that are present
if (!isOfficial) { if (!isOfficial) {
Timber.w("You may be running an untrusted build."); 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 bottomNavigationView = findViewById(R.id.bottom_navigation);
bottomNavigationView.setOnItemSelectedListener(item -> { bottomNavigationView.setOnItemSelectedListener(item -> {
if (item.getItemId() == R.id.settings_menu_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)); startActivity(new Intent(MainActivity.this, SettingsActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish(); finish();
} else if (item.getItemId() == R.id.online_menu_item) { } 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 // set module_list_online as visible and module_list as gone. fade in/out
this.moduleListOnline.setAlpha(0F); this.moduleListOnline.setAlpha(0F);
this.moduleListOnline.setVisibility(View.VISIBLE); this.moduleListOnline.setVisibility(View.VISIBLE);
@ -197,6 +218,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
// clear search view // clear search view
this.searchView.setQuery("", false); this.searchView.setQuery("", false);
} else if (item.getItemId() == R.id.installed_menu_item) { } 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 // set module_list_online as gone and module_list as visible. fade in/out
this.moduleList.setAlpha(0F); this.moduleList.setAlpha(0F);
this.moduleList.setVisibility(View.VISIBLE); this.moduleList.setVisibility(View.VISIBLE);
@ -223,7 +245,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
rootContainer.setLayoutParams(params); rootContainer.setLayoutParams(params);
rootContainer.setY(0F); 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(); MainApplication.getINSTANCE().resetUpdateModule();
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() { InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override @Override
@ -326,7 +348,11 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (max != 0) { if (max != 0) {
int current = 0; int current = 0;
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { 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); if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
try { try {
localModuleInfo.checkModuleUpdate(); localModuleInfo.checkModuleUpdate();
@ -528,7 +554,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (max != 0) { if (max != 0) {
int current = 0; int current = 0;
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { 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); if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
try { try {
localModuleInfo.checkModuleUpdate(); localModuleInfo.checkModuleUpdate();
@ -559,6 +585,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
public boolean onQueryTextSubmit(final String query) { public boolean onQueryTextSubmit(final String query) {
this.searchView.clearFocus(); this.searchView.clearFocus();
if (this.initMode) return false; if (this.initMode) return false;
TrackHelper.track().event("search", query).with(MainApplication.getINSTANCE().getTracker());
if (this.moduleViewListBuilder.setQueryChange(query)) { if (this.moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query); Timber.i("Query submit: %s on offline list", query);
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
@ -574,6 +601,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
@Override @Override
public boolean onQueryTextChange(String query) { public boolean onQueryTextChange(String query) {
if (this.initMode) return false; if (this.initMode) return false;
TrackHelper.track().event("search_type", query).with(MainApplication.getINSTANCE().getTracker());
if (this.moduleViewListBuilder.setQueryChange(query)) { if (this.moduleViewListBuilder.setQueryChange(query)) {
Timber.i("Query submit: %s on offline list", query); Timber.i("Query submit: %s on offline list", query);
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); 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.google.common.hash.Hashing;
import com.topjohnwu.superuser.Shell; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -90,8 +95,9 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
// Use FoxProcess wrapper helper. // Use FoxProcess wrapper helper.
private static final boolean wrapped = !FoxProcessExt.isRootLoader(); 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; public static boolean isOfficial = false;
private static String SHOWCASE_MODE_TRUE = null;
private static long secret; private static long secret;
private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().getLocales().get(0); private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().getLocales().get(0);
private static SimpleDateFormat timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale); 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 MainApplication INSTANCE;
private static boolean firstBoot; private static boolean firstBoot;
private static HashMap<Object, Object> mSharedPrefs; private static HashMap<Object, Object> mSharedPrefs;
private static final ArrayList<String> callers = new ArrayList<>();
public boolean modulesHaveUpdates = false;
static { static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR).setTimeout(10).setInitializers(InstallerInitializer.class)); 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); } while (secret == 0);
} }
public boolean modulesHaveUpdates = false;
public int updateModuleCount = 0; public int updateModuleCount = 0;
public List<String> updateModules = new ArrayList<>(); public List<String> updateModules = new ArrayList<>();
public boolean isMatomoAllowed;
@StyleRes @StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager; private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext; private FoxThemeWrapper markwonThemeContext;
private Markwon markwon; private Markwon markwon;
private byte[] existingKey; private byte[] existingKey;
private Tracker tracker;
public MainApplication() { public MainApplication() {
if (INSTANCE != null && INSTANCE != this) if (INSTANCE != null && INSTANCE != this)
@ -356,6 +362,16 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
return !this.isLightTheme(); 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 @Override
public void onCreate() { public void onCreate() {
supportedLocales.add("ar"); supportedLocales.add("ar");
@ -410,6 +426,31 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
Timber.d("Initializing Realm"); Timber.d("Initializing Realm");
Realm.init(this); Realm.init(this);
Timber.d("Initialized Realm"); 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 // Determine if this is an official build based on the signature
try { try {
// Get the signature of the key used to sign the app // 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") @SuppressWarnings("unused")
private Intent getIntent() { private Intent getIntent() {
return this.getPackageManager().getLaunchIntentForPackage(this.getPackageName()); return this.getPackageManager().getLaunchIntentForPackage(this.getPackageName());

@ -49,12 +49,6 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
this.setTitle(R.string.setup_title); 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); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0);
createFiles(); createFiles();
disableUpdateActivityForFdroidFlavor(); 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); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
// pref_crash_reporting_pii // pref_crash_reporting_pii
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setChecked(BuildConfig.DEFAULT_ENABLE_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 // assert that both switches match the build config on debug builds
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked() == BuildConfig.ENABLE_AUTO_UPDATER; 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()); editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked());
// Set the crash reporting PII pref // 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_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"); Timber.d("Saving preferences");
// Set the repos in the ReposList realm db // 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(); 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/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/wasm"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js")); 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) { } catch (IOException e) {
Timber.e(e); Timber.e(e);
} }

@ -6,12 +6,10 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.webkit.WebView;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import com.fox2code.foxcompat.app.FoxActivity; import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.app.FoxApplication;
import com.fox2code.mmm.utils.io.net.Http; import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.LinearProgressIndicator; 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.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.matomo.sdk.extra.TrackHelper;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -35,6 +34,9 @@ public class UpdateActivity extends FoxActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (MainApplication.getINSTANCE().isMatomoAllowed) {
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
}
setContentView(R.layout.activity_update); setContentView(R.layout.activity_update);
// Get the progress bar and make it indeterminate for now // Get the progress bar and make it indeterminate for now
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress); 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.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.progressindicator.LinearProgressIndicator;
import org.matomo.sdk.extra.TrackHelper;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -72,6 +74,7 @@ public final class AndroidacyActivity extends FoxActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
this.moduleFile = new File(this.getCacheDir(), "module.zip"); this.moduleFile = new File(this.getCacheDir(), "module.zip");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
Intent intent = this.getIntent(); Intent intent = this.getIntent();
Uri uri; Uri uri;
if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) { 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 String NOTIFICATION_CHANNEL_ID = "background_update";
public static final int NOTIFICATION_ID = 1; public static final int NOTIFICATION_ID = 1;
public static final String NOTFIICATION_GROUP = "updates"; 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"; 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 int NOTIFICATION_ID_ONGOING = 2;
private static final String NOTIFICATION_CHANNEL_ID_ONGOING = "mmm_background_update"; private static final String NOTIFICATION_CHANNEL_ID_ONGOING = "mmm_background_update";
private static final int NOTIFICATION_ID_APP = 3; private static final int NOTIFICATION_ID_APP = 3;
public BackgroundUpdateChecker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private static void postNotificationForAppUpdate(Context context) { private static void postNotificationForAppUpdate(Context context) {
// create the notification channel if not already created // 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) { static void doCheck(Context context) {
// first, check if the user has enabled background update checking // first, check if the user has enabled background update checking
if (!MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check", false)) { if (!MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check", false)) {
return; return;
} }
//if (MainApplication.getINSTANCE().isInForeground()) { if (MainApplication.getINSTANCE().isInForeground()) {
// don't check if app is in foreground, this is a background check // don't check if app is in foreground, this is a background check
// return; return;
//} }
// next, check if user requires wifi // next, check if user requires wifi
if (MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check_wifi", true)) { if (MainApplication.getPreferences("mmm").getBoolean("pref_background_update_check_wifi", true)) {
// check if wifi is connected // check if wifi is connected
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network networkInfo = connectivityManager.getActiveNetwork(); 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"); Timber.w("Background update check: wifi not connected but required");
return; return;
} }
@ -110,7 +110,7 @@ public class BackgroundUpdateChecker extends Worker {
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); 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()); 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.setSmallIcon(R.drawable.ic_baseline_update_24);
builder.setPriority(NotificationCompat.PRIORITY_MIN); builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setCategory(NotificationCompat.CATEGORY_SERVICE); builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
@ -224,6 +224,11 @@ public class BackgroundUpdateChecker extends Worker {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context); 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.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); 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()); 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.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
@ -79,6 +80,7 @@ public class InstallerActivity extends FoxActivity {
if (!this.moduleCache.exists() && !this.moduleCache.mkdirs()) if (!this.moduleCache.exists() && !this.moduleCache.mkdirs())
Timber.e("Failed to mkdir module cache dir!"); Timber.e("Failed to mkdir module cache dir!");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true); this.setDisplayHomeAsUpEnabled(true);
setActionBarBackground(null); setActionBarBackground(null);
this.setOnBackPressedCallback(a -> { this.setOnBackPressedCallback(a -> {

@ -25,7 +25,7 @@ public class LocalModuleInfo extends ModuleInfo {
} }
public void checkModuleUpdate() { public void checkModuleUpdate() {
if (this.updateJson != null) { if (this.updateJson != null && (this.flags & FLAG_MM_REMOTE_MODULE) == 0) {
try { try {
JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet( JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet(
this.updateJson, true), StandardCharsets.UTF_8)); 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_METADATA_INVALID = 0x80000000;
public static final int FLAG_CUSTOM_INTERNAL = 0x40000000; 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 private static final int FLAG_FENCE = 0x10000000; // Should never be set
// Magisk standard // 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_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 int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private static final ModuleManager INSTANCE = new ModuleManager(); 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 HashMap<String, LocalModuleInfo> moduleInfos;
private final SharedPreferences bootPrefs; private final SharedPreferences bootPrefs;
private int updatableModuleCount = 0; 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 // get all dirs under the realms/repos/ dir under app's data dir
File cacheRoot = new File(MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/").toURI()); File cacheRoot = new File(MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/").toURI());
ModuleListCache moduleListCache; ModuleListCache moduleListCache;
boolean foundCache = false;
for (File dir : Objects.requireNonNull(cacheRoot.listFiles())) { for (File dir : Objects.requireNonNull(cacheRoot.listFiles())) {
if (dir.isDirectory()) { if (dir.isDirectory()) {
// if the dir name matches the module name, use it as the cache dir // 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()); 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(); moduleListCache = realm.where(ModuleListCache.class).equalTo("codename", module).findFirst();
if (moduleListCache != null) { if (moduleListCache != null) {
foundCache = true;
Timber.d("Found cache for %s", module); Timber.d("Found cache for %s", module);
// get module info from cache // get module info from cache
if (moduleInfo == null) { if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module); moduleInfo = new LocalModuleInfo(module);
} }
moduleInfo.name = moduleListCache.getName(); moduleInfo.name = !Objects.equals(moduleListCache.getName(), "") ? moduleListCache.getName() : module;
moduleInfo.description = moduleListCache.getDescription() + " (cached)"; moduleInfo.description = !Objects.equals(moduleListCache.getDescription(), "") ? moduleListCache.getDescription() : null;
moduleInfo.author = moduleListCache.getAuthor(); moduleInfo.author = !Objects.equals(moduleListCache.getAuthor(), "") ? moduleListCache.getAuthor() : null;
moduleInfo.safe = moduleListCache.isSafe(); moduleInfo.safe = Objects.equals(moduleListCache.isSafe(), true);
moduleInfo.support = moduleListCache.getSupport(); moduleInfo.support = !Objects.equals(moduleListCache.getSupport(), "") ? moduleListCache.getSupport() : null;
moduleInfo.donate = moduleListCache.getDonate(); moduleInfo.donate = !Objects.equals(moduleListCache.getDonate(), "") ? moduleListCache.getDonate() : null;
moduleInfo.flags |= FLAG_MM_REMOTE_MODULE;
moduleInfos.put(module, moduleInfo); moduleInfos.put(module, moduleInfo);
realm.close(); realm.close();
break; break;
@ -185,7 +185,7 @@ public final class ModuleManager extends SyncManager {
moduleInfoIterator.remove(); moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced continue; // Don't process fallbacks if unreferenced
} }
if (moduleInfo.updateJson != null) { if (moduleInfo.updateJson != null && (moduleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
this.updatableModuleCount++; this.updatableModuleCount++;
} else { } else {
moduleInfo.updateVersion = null; moduleInfo.updateVersion = null;

@ -27,6 +27,8 @@ import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler; import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.matomo.sdk.extra.TrackHelper;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
@ -67,6 +69,7 @@ public class MarkdownActivity extends FoxActivity {
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true); this.setDisplayHomeAsUpEnabled(true);
Intent intent = this.getIntent(); Intent intent = this.getIntent();
if (!MainApplication.checkSecret(intent)) { 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.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.matomo.sdk.extra.TrackHelper;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import timber.log.Timber; import timber.log.Timber;
@SuppressWarnings("ReplaceNullCheck")
@SuppressLint("UseCompatLoadingForDrawables") @SuppressLint("UseCompatLoadingForDrawables")
public enum ActionButtonType { public enum ActionButtonType {
INFO() { INFO() {
@ -39,6 +42,13 @@ public enum ActionButtonType {
@Override @Override
public void doAction(Chip button, ModuleHolder moduleHolder) { 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; String notesUrl = moduleHolder.repoModule.notesUrl;
if (AndroidacyUtil.isAndroidacyLink(notesUrl)) { if (AndroidacyUtil.isAndroidacyLink(notesUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig()); IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig());
@ -70,6 +80,14 @@ public enum ActionButtonType {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
if (moduleInfo == null) if (moduleInfo == null)
return; 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(); String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) if (updateZipUrl == null)
return; return;
@ -132,6 +150,13 @@ public enum ActionButtonType {
doActionLong(button, moduleHolder); doActionLong(button, moduleHolder);
return; 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)); Timber.i(Integer.toHexString(moduleHolder.moduleInfo.flags));
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) { if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Timber.e("Failed to switch uninstalled state!"); Timber.e("Failed to switch uninstalled state!");
@ -174,6 +199,14 @@ public enum ActionButtonType {
String config = moduleHolder.getMainModuleConfig(); String config = moduleHolder.getMainModuleConfig();
if (config == null) if (config == null)
return; 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)) { if (AndroidacyUtil.isAndroidacyLink(config)) {
IntentHelper.openUrlAndroidacy(button.getContext(), config, true); IntentHelper.openUrlAndroidacy(button.getContext(), config, true);
} else { } else {
@ -190,6 +223,14 @@ public enum ActionButtonType {
@Override @Override
public void doAction(Chip button, ModuleHolder moduleHolder) { 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); IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
} }
}, DONATE() { }, DONATE() {
@ -202,6 +243,13 @@ public enum ActionButtonType {
@Override @Override
public void doAction(Chip button, ModuleHolder moduleHolder) { 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); IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
} }
}, WARNING() { }, WARNING() {
@ -213,6 +261,13 @@ public enum ActionButtonType {
@Override @Override
public void doAction(Chip button, ModuleHolder moduleHolder) { 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) -> { new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show(); }).create().show();
} }
@ -226,6 +281,13 @@ public enum ActionButtonType {
@Override @Override
public void doAction(Chip button, ModuleHolder moduleHolder) { 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) -> { new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.safe_module).setMessage(R.string.safe_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show(); }).create().show();
} }

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

@ -68,6 +68,7 @@ public enum IntentHelper {
} }
public static void openUrl(Context context, String url, boolean forceBrowser) { public static void openUrl(Context context, String url, boolean forceBrowser) {
Timber.d("Opening url: %s, forced browser %b", url, forceBrowser);
try { try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
myIntent.setFlags(FLAG_GRANT_URI_PERMISSION); myIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -76,12 +77,14 @@ public enum IntentHelper {
} }
startActivity(context, myIntent, false); startActivity(context, myIntent, false);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString( Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show(); R.string.no_browser), Toast.LENGTH_LONG).show();
} }
} }
public static void openCustomTab(Context context, String url) { public static void openCustomTab(Context context, String url) {
Timber.d("Opening url: %s in custom tab", url);
try { try {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION); viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -90,6 +93,7 @@ public enum IntentHelper {
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE); tabIntent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivityEx(context, tabIntent, viewIntent); startActivityEx(context, tabIntent, viewIntent);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString( Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show(); 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.MainApplication;
import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.androidacy.AndroidacyUtil;
import org.matomo.sdk.extra.TrackHelper;
import java.util.Objects; import java.util.Objects;
import io.sentry.Sentry; import io.sentry.Sentry;
@ -20,6 +22,7 @@ import timber.log.Timber;
public class SentryMain { public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true; public static final boolean IS_SENTRY_INSTALLED = true;
public static boolean isCrashing = false;
private static boolean sentryEnabled = false; private static boolean sentryEnabled = false;
/** /**
@ -29,6 +32,8 @@ public class SentryMain {
@SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"}) @SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"})
public static void initialize(final MainApplication mainApplication) { public static void initialize(final MainApplication mainApplication) {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { 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(); SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("sentry", Context.MODE_PRIVATE).edit();
editor.putString("lastExitReason", "crash"); editor.putString("lastExitReason", "crash");
editor.putLong("lastExitTime", System.currentTimeMillis()); editor.putLong("lastExitTime", System.currentTimeMillis());
@ -44,19 +49,28 @@ public class SentryMain {
intent.putExtra("stacktrace", throwable.getStackTrace()); intent.putExtra("stacktrace", throwable.getStackTrace());
// put lastEventId in intent (get from preferences) // put lastEventId in intent (get from preferences)
intent.putExtra("lastEventId", String.valueOf(Sentry.getLastEventId())); 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.putExtra("crashReportingEnabled", isSentryEnabled());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Timber.e("Starting crash handler"); Timber.e("Starting crash handler");
mainApplication.startActivity(intent); mainApplication.startActivity(intent);
Timber.e("Exiting"); Timber.e("Exiting");
android.os.Process.killProcess(android.os.Process.myPid()); android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}); });
// If first_launch pref is not false, refuse to initialize Sentry // If first_launch pref is not false, refuse to initialize Sentry
SharedPreferences sharedPreferences = MainApplication.getPreferences("sentry"); SharedPreferences sharedPreferences = MainApplication.getPreferences("mmm");
if (!Objects.equals(MainApplication.getPreferences("mmm").getString("last_shown_setup", null), "v1")) { if (!Objects.equals(sharedPreferences.getString("last_shown_setup", null), "v1")) {
return; 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 -> { SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here. // If crash reporting is disabled, stop here.
if (!MainApplication.isCrashReportingEnabled()) { if (!MainApplication.isCrashReportingEnabled()) {
@ -87,28 +101,25 @@ public class SentryMain {
options.setAttachScreenshot(true); options.setAttachScreenshot(true);
// It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling. // It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
options.setEnableAutoSessionTracking(true); 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. // 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. // With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> { 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 // 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; 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; return event;
}); });
// Filter breadcrumb content from crash report. // Filter breadcrumb content from crash report.
options.setBeforeBreadcrumb((breadcrumb, hint) -> { options.setBeforeBreadcrumb((breadcrumb, hint) -> {
String url = (String) breadcrumb.getData("url"); String url = (String) breadcrumb.getData("url");
if (url == null || url.isEmpty()) if (url == null || url.isEmpty()) return breadcrumb;
return breadcrumb; if ("cloudflare-dns.com".equals(Uri.parse(url).getHost())) return null;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost()))
return null;
if (AndroidacyUtil.isAndroidacyLink(url)) { if (AndroidacyUtil.isAndroidacyLink(url)) {
breadcrumb.setData("url", AndroidacyUtil.hideToken(url)); breadcrumb.setData("url", AndroidacyUtil.hideToken(url));
} }

@ -200,6 +200,28 @@
android:text="@string/setup_crash_reporting_pii_summary" android:text="@string/setup_crash_reporting_pii_summary"
android:textAppearance="@android:style/TextAppearance.Material.Small" /> 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 <com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -250,18 +272,6 @@
android:textAppearance="@android:style/TextAppearance.Material.Small" android:textAppearance="@android:style/TextAppearance.Material.Small"
app:icon="@drawable/ic_baseline_info_24" /> 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 --> <!-- licenses, disclaimers, and EULA -->
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent" 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="showcase_mode_dialogue_message">An app restart is required to enable showcase mode.</string>
<string name="other_section">Other</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="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> </resources>

@ -192,6 +192,14 @@
app:singleLineTitle="false" app:singleLineTitle="false"
app:summary="@string/crash_reporting_pii_desc" app:summary="@string/crash_reporting_pii_desc"
app:title="@string/crash_reporting_pii" /> 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 --> <!-- Purposely crash the app -->
<Preference <Preference
app:icon="@drawable/ic_baseline_bug_report_24" app:icon="@drawable/ic_baseline_bug_report_24"

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

Loading…
Cancel
Save