diff --git a/app/build.gradle b/app/build.gradle index 2e5b34c..719ef57 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) { diff --git a/app/src/main/java/com/fox2code/mmm/CrashHandler.java b/app/src/main/java/com/fox2code/mmm/CrashHandler.java index a02ce3f..90d077c 100644 --- a/app/src/main/java/com/fox2code/mmm/CrashHandler.java +++ b/app/src/main/java/com/fox2code/mmm/CrashHandler.java @@ -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())); diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index f36c1c3..c3ff15c 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -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(); diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index c3bcb44..871e255 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -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 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 mSharedPrefs; - private static final ArrayList 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 updateModules = new ArrayList<>(); - + public List 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()); diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.java b/app/src/main/java/com/fox2code/mmm/SetupActivity.java index 0eda156..aaf564f 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.java +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.java @@ -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); } diff --git a/app/src/main/java/com/fox2code/mmm/UpdateActivity.java b/app/src/main/java/com/fox2code/mmm/UpdateActivity.java index 4cd91cf..4ee64c5 100644 --- a/app/src/main/java/com/fox2code/mmm/UpdateActivity.java +++ b/app/src/main/java/com/fox2code/mmm/UpdateActivity.java @@ -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); diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java index 5854a9f..8d3fe5d 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -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) { diff --git a/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java b/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java index b97711c..cace4a0 100644 --- a/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java +++ b/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java @@ -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()); } diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index c78f1c2..f8ff993 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -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 -> { diff --git a/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java b/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java index 8e816f7..77762b8 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java +++ b/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java @@ -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)); diff --git a/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java b/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java index 97674c7..4a5fa5d 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java @@ -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 diff --git a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java index f823bdd..2f75693 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java @@ -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 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; diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java index add29e3..6ec1519 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java @@ -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)) { diff --git a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java index 3dd77d0..74968b4 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java @@ -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(); } diff --git a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java index 7c15b09..4f7ffd4 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -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; diff --git a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java index 9de2bb9..518601f 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -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(); } diff --git a/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java b/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java index 798c6f4..ce6a9cb 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java +++ b/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java @@ -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)); } diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index fac4f88..8b2f384 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -200,6 +200,28 @@ android:text="@string/setup_crash_reporting_pii_summary" android:textAppearance="@android:style/TextAppearance.Material.Small" /> + + + + + + + - - - An app restart is required to enable showcase mode. Other 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/) + Allow us to track app usage and installs. Fully GDPR compliant and uses Matomo, hosted by Androidacy. diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 17e48cb..d1859bc 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -192,6 +192,14 @@ app:singleLineTitle="false" app:summary="@string/crash_reporting_pii_desc" app:title="@string/crash_reporting_pii" /> + +