From 4fc7a94f78f97d35bc2e08650226dd2645d9d1a8 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Fri, 3 Feb 2023 15:44:46 -0500 Subject: [PATCH] apply best practices fix a lot of best practices and respect user choice for needing wifi for bg updates Signed-off-by: androidacy-user --- app/src/main/AndroidManifest.xml | 58 +++++--- .../com/fox2code/mmm/AppUpdateManager.java | 6 +- .../java/com/fox2code/mmm/CrashHandler.java | 18 +-- .../java/com/fox2code/mmm/MainActivity.java | 4 +- .../com/fox2code/mmm/MainApplication.java | 7 +- .../java/com/fox2code/mmm/SetupActivity.java | 23 +-- .../java/com/fox2code/mmm/UpdateActivity.java | 40 ++++-- .../mmm/androidacy/AndroidacyActivity.java | 17 ++- .../mmm/androidacy/AndroidacyRepoData.java | 43 +++--- .../background/BackgroundUpdateChecker.java | 104 ++++++++++++-- .../mmm/installer/InstallerActivity.java | 34 +++-- .../mmm/installer/InstallerInitializer.java | 2 +- .../mmm/markdown/MarkdownActivity.java | 9 +- .../com/fox2code/mmm/module/ModuleHolder.java | 10 +- .../mmm/module/ModuleViewAdapter.java | 2 +- .../java/com/fox2code/mmm/repo/RepoData.java | 2 +- .../com/fox2code/mmm/repo/RepoManager.java | 21 ++- .../com/fox2code/mmm/repo/RepoUpdater.java | 4 +- .../mmm/settings/SettingsActivity.java | 135 ++++++++++++------ .../com/fox2code/mmm/utils/IntentHelper.java | 8 +- .../mmm/utils/io/AddCookiesInterceptor.java | 4 + .../com/fox2code/mmm/utils/io/Hashes.java | 21 +-- .../java/com/fox2code/mmm/utils/io/Http.java | 2 +- .../mmm/utils/realm/ModuleListCache.java | 3 +- .../res/drawable/baseline_network_wifi_24.xml | 5 + app/src/main/res/values/strings.xml | 21 ++- app/src/main/res/xml/repo_preferences.xml | 5 + app/src/main/res/xml/root_preferences.xml | 9 ++ 28 files changed, 415 insertions(+), 202 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_network_wifi_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12021d1..7dd079c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,21 +10,40 @@ - + + - - - - - + android:required="false" /> + + + + + + + + + + + + + + + + + @@ -66,7 +82,6 @@ @@ -76,7 +91,6 @@ @@ -88,7 +102,6 @@ @@ -118,13 +130,11 @@ @@ -165,22 +175,28 @@ android:value="false" /> + android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" /> + + android:value="0.25" /> + + android:value="true" /> + + android:value="true" /> + + android:value="true" /> + + diff --git a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java index 246fa13..981f299 100644 --- a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java +++ b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java @@ -44,8 +44,7 @@ public class AppUpdateManager { try { this.parseCompatibilityFlags(new FileInputStream(this.compatFile)); } catch ( - IOException e) { - e.printStackTrace(); + IOException ignored) { } } } @@ -132,7 +131,7 @@ public class AppUpdateManager { Files.write(compatFile, new byte[0]); } catch ( IOException e) { - e.printStackTrace(); + Timber.e(e); } // There once lived an implementation that used a GitHub API to get the compatibility flags. It was removed because it was too slow and the API was rate limited. Timber.w("Remote compatibility data flags are not implemented."); @@ -208,6 +207,7 @@ public class AppUpdateManager { } compatDataId.put(line.substring(0, i), value); } + bufferedReader.close(); } public int getCompatibilityFlags(String moduleId) { diff --git a/app/src/main/java/com/fox2code/mmm/CrashHandler.java b/app/src/main/java/com/fox2code/mmm/CrashHandler.java index ad4c9c4..aa06ff7 100644 --- a/app/src/main/java/com/fox2code/mmm/CrashHandler.java +++ b/app/src/main/java/com/fox2code/mmm/CrashHandler.java @@ -16,6 +16,7 @@ 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; @@ -30,10 +31,6 @@ public class CrashHandler extends FoxActivity { Timber.i("CrashHandler.onCreate(%s)", savedInstanceState); // log intent with extras Timber.d("CrashHandler.onCreate: intent=%s", getIntent()); - // get exception, stacktrace, and lastEventId from intent and log them - Timber.d("CrashHandler.onCreate: exception=%s", getIntent().getSerializableExtra("exception")); - Timber.d("CrashHandler.onCreate: stacktrace=%s", getIntent().getSerializableExtra("stacktrace")); - Timber.d("CrashHandler.onCreate: lastEventId=%s", getIntent().getStringExtra("lastEventId")); super.onCreate(savedInstanceState); setContentView(R.layout.activity_crash_handler); // set crash_details MaterialTextView to the exception passed in the intent or unknown if null @@ -95,8 +92,13 @@ public class CrashHandler extends FoxActivity { body.put("comments", description.getText().toString()); // Send the request connection.setDoOutput(true); - connection.getOutputStream().write(body.toString().getBytes()); - connection.connect(); + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(body.toString().getBytes()); + outputStream.flush(); + outputStream.close(); + // close and disconnect the connection + connection.getInputStream().close(); + connection.disconnect(); // For debug builds, log the response code and response body if (BuildConfig.DEBUG) { Timber.d("Response Code: %s", connection.getResponseCode()); @@ -107,8 +109,6 @@ public class CrashHandler extends FoxActivity { } else { runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show()); } - // close and disconnect the connection - connection.disconnect(); } catch ( JSONException | IOException ignored) { @@ -184,7 +184,7 @@ public class CrashHandler extends FoxActivity { Thread.sleep(1000); } catch ( InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } runOnUiThread(() -> view.setBackgroundResource(R.drawable.baseline_copy_all_24)); }).start(); diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 71bec1c..1f0c02f 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -110,7 +110,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe } urlFactoryInstalled = true; } catch ( - Throwable t) { + Exception t) { Timber.e("Failed to install CronetURLStreamHandlerFactory - other"); } } @@ -386,6 +386,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe Thread.sleep(100); } catch ( InterruptedException ignored) { + Thread.currentThread().interrupt(); } } if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND) @@ -681,6 +682,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe } } catch ( InterruptedException e) { + Thread.currentThread().interrupt(); return true; } return doSetupRestarting; diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 49d2447..a184b19 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -60,7 +60,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con public static final HashSet supportedLocales = new HashSet<>(); private static final String timeFormatString = "dd MMM yyyy"; // Example: 13 july 2001 private static final Shell.Builder shellBuilder; - private static final long secret; + private static long secret; @SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper. private static final boolean wrapped = !FoxProcessExt.isRootLoader(); @@ -75,7 +75,10 @@ public class MainApplication extends FoxApplication implements androidx.work.Con static { Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR).setTimeout(10).setInitializers(InstallerInitializer.class)); - secret = new Random().nextLong(); + Random random = new Random(); + do { + secret = random.nextLong(); + } while (secret == 0); } @StyleRes diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.java b/app/src/main/java/com/fox2code/mmm/SetupActivity.java index 1031ca3..0c99040 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.java +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.java @@ -41,7 +41,6 @@ import java.util.Objects; import io.realm.Realm; import io.realm.RealmConfiguration; -import io.realm.RealmResults; import timber.log.Timber; public class SetupActivity extends FoxActivity implements LanguageActivity { @@ -161,8 +160,10 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { break; } // restart the activity because switching to transparent pisses the rendering engine off - Intent intent = getIntent(); + Intent intent = new Intent(this, SetupActivity.class); finish(); + // ensure intent originates from the same package + intent.setPackage(getPackageName()); startActivity(intent); }, 100); }); @@ -218,7 +219,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { Thread.sleep(500); } catch ( InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } // Log the changes if debug if (BuildConfig.DEBUG) { @@ -275,7 +276,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { // refresh app language runOnUiThread(() -> { // refresh activity - Intent intent = getIntent(); + Intent intent = new Intent(this, SetupActivity.class); finish(); startActivity(intent); }); @@ -325,20 +326,6 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { realm1.insertOrUpdate(magisk_alt_repo); } realm1.commitTransaction(); - realm1.close(); - if (BuildConfig.DEBUG) { - Timber.d("Realm databases created"); - Realm realm3 = Realm.getInstance(config2); - RealmResults reposLists = realm3.where(ReposList.class).findAll(); - assert reposLists != null; - Timber.d("ReposList.realm"); - for (ReposList reposList : reposLists) { - Timber.d("Record: %s", reposList.getId()); - // log the data - Timber.d("Name: %s, Donate: %s, Support: %s, Submit Module: %s, Website: %s, Enabled: %s, Last Update: %s", reposList.getName(), reposList.getDonate(), reposList.getSupport(), reposList.getSubmitModule(), reposList.getWebsite(), reposList.isEnabled(), reposList.getLastUpdate()); - } - realm3.close(); - } } }); } diff --git a/app/src/main/java/com/fox2code/mmm/UpdateActivity.java b/app/src/main/java/com/fox2code/mmm/UpdateActivity.java index 25fb09a..0df7383 100644 --- a/app/src/main/java/com/fox2code/mmm/UpdateActivity.java +++ b/app/src/main/java/com/fox2code/mmm/UpdateActivity.java @@ -82,7 +82,6 @@ public class UpdateActivity extends FoxActivity { downloadUpdate(); } catch ( JSONException e) { - e.printStackTrace(); runOnUiThread(() -> { // set status text to error statusTextView.setText(R.string.error_download_update); @@ -92,9 +91,9 @@ public class UpdateActivity extends FoxActivity { }); } } else if (action == ACTIONS.INSTALL) { - // ensure path was passed and points to a file within our cache directory - String path = getIntent().getStringExtra("path"); - if (path == null) { + // ensure path was passed and points to a file within our cache directory. replace .. and url encoded characters + String path = getIntent().getStringExtra("path").trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", ""); + if (path.isEmpty()) { runOnUiThread(() -> { // set status text to error statusTextView.setText(R.string.no_file_found); @@ -104,7 +103,21 @@ public class UpdateActivity extends FoxActivity { }); return; } + // check and sanitize file path + // path must be in our cache directory + if (!path.startsWith(getCacheDir().getAbsolutePath())) { + throw new SecurityException("Path is not in cache directory: " + path); + } File file = new File(path); + File parentFile = file.getParentFile(); + try { + if (parentFile == null || !parentFile.getCanonicalPath().startsWith(getCacheDir().getCanonicalPath())) { + throw new SecurityException("Path is not in cache directory: " + path); + } + } catch ( + IOException e) { + throw new SecurityException("Path is not in cache directory: " + path); + } if (!file.exists()) { runOnUiThread(() -> { // set status text to error @@ -248,7 +261,6 @@ public class UpdateActivity extends FoxActivity { })); } catch ( Exception e) { - e.printStackTrace(); runOnUiThread(() -> { progressIndicator.setIndeterminate(false); progressIndicator.setProgressCompat(100, false); @@ -288,19 +300,27 @@ public class UpdateActivity extends FoxActivity { }); // save the update to the cache File updateFile = null; + FileOutputStream fileOutputStream = null; try { updateFile = new File(getCacheDir(), "update.apk"); - FileOutputStream fileOutputStream = new FileOutputStream(updateFile); + fileOutputStream = new FileOutputStream(updateFile); fileOutputStream.write(update); - fileOutputStream.close(); } catch ( IOException e) { - e.printStackTrace(); runOnUiThread(() -> { progressIndicator.setIndeterminate(false); progressIndicator.setProgressCompat(100, false); statusTextView.setText(R.string.error_download_update); }); + } finally { + if (Objects.nonNull(updateFile)) { + Objects.requireNonNull(updateFile).deleteOnExit(); + } + try { + Objects.requireNonNull(fileOutputStream).close(); + } catch ( + IOException ignored) { + } } // install the update installUpdate(updateFile); @@ -321,10 +341,10 @@ public class UpdateActivity extends FoxActivity { progressIndicator.setProgressCompat(100, false); }); // request install permissions - Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + Intent intent = new Intent(Intent.ACTION_VIEW); Context context = getApplicationContext(); Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".file-provider", updateFile); - intent.setData(uri); + intent.setDataAndTypeAndNormalize(uri, "application/vnd.android.package-archive"); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); 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 ec94de6..dc6e885 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -110,8 +110,7 @@ public final class AndroidacyActivity extends FoxActivity { try { device_id = AndroidacyRepoData.generateDeviceId(); } catch ( - NoSuchAlgorithmException e) { - e.printStackTrace(); + NoSuchAlgorithmException ignored) { } url = url + "&device_id=" + device_id; } @@ -173,7 +172,11 @@ public final class AndroidacyActivity extends FoxActivity { if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) { if (downloadMode || backOnResume) return true; - Timber.i("Exiting WebView %s", AndroidacyUtil.hideToken(request.getUrl().toString())); + // sanitize url + String url = request.getUrl().toString(); + //noinspection UnnecessaryCallToStringValueOf + url = String.valueOf(AndroidacyUtil.hideToken(url)); + Timber.i("Exiting WebView %s", url); IntentHelper.openUri(view.getContext(), request.getUrl().toString()); return true; } @@ -369,10 +372,12 @@ public final class AndroidacyActivity extends FoxActivity { private boolean megaIntercept(String pageUrl, String fileUrl) { if (pageUrl == null || fileUrl == null) return false; - if (this.isFileUrl(fileUrl)) { - Timber.i("megaIntercept(%s", AndroidacyUtil.hideToken(AndroidacyUtil.hideToken(fileUrl) )); - } else + // ensure neither pageUrl nor fileUrl are going to cause a crash + if (pageUrl.contains(" ") || fileUrl.contains(" ")) return false; + if (!this.isFileUrl(fileUrl)) { + return false; + } final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; String moduleId = AndroidacyUtil.getModuleId(fileUrl); if (moduleId == null) { diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index cb4780e..34449f9 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -4,6 +4,8 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.widget.Toast; import androidx.annotation.NonNull; @@ -41,8 +43,6 @@ import timber.log.Timber; @SuppressWarnings("KotlinInternalInJava") public final class AndroidacyRepoData extends RepoData { - public String[][] userInfo = new String[][]{{"role", null}, {"permissions", null}}; - public static String token = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).getString("pref_androidacy_api_token", null); static { @@ -54,9 +54,10 @@ public final class AndroidacyRepoData extends RepoData { @SuppressWarnings("unused") public final String ClientID = BuildConfig.ANDROIDACY_CLIENT_ID; + public final SharedPreferences cachedPreferences = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0); private final boolean testMode; private final String host; - public final SharedPreferences cachedPreferences = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0); + public String[][] userInfo = new String[][]{{"role", null}, {"permissions", null}}; public String memberLevel; // Avoid spamming requests to Androidacy private long androidacyBlockade = 0; @@ -139,22 +140,13 @@ public final class AndroidacyRepoData extends RepoData { String deviceId = generateDeviceId(); try { byte[] resp = Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token + "&device_id=" + deviceId, false); + // response is JSON JSONObject jsonObject = new JSONObject(new String(resp)); memberLevel = jsonObject.getString("role"); JSONArray memberPermissions = jsonObject.getJSONArray("permissions"); // set role and permissions on userInfo property userInfo = new String[][]{{"role", memberLevel}, {"permissions", String.valueOf(memberPermissions)}}; - String status = jsonObject.getString("status"); - if (status.equals("success")) { - return true; - } else { - Timber.w("Invalid token, resetting..."); - // Remove saved preference - SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit(); - editor.remove("pref_androidacy_api_token"); - editor.apply(); - return false; - } + return true; } catch ( HttpException e) { if (e.getErrorCode() == 401) { @@ -169,7 +161,13 @@ public final class AndroidacyRepoData extends RepoData { } catch ( JSONException e) { // response is not JSON - throw new IOException(e); + Timber.w("Invalid token, resetting..."); + Timber.w(e); + // Remove saved preference + SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit(); + editor.remove("pref_androidacy_api_token"); + editor.apply(); + return false; } } @@ -242,25 +240,30 @@ public final class AndroidacyRepoData extends RepoData { if (token == null) { try { Timber.i("Requesting new token..."); - // POST json request to https://produc/tion-api.androidacy.com/auth/register - token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "{\"device_id\":\"" + deviceId + "\"}", false)); + // POST json request to https://production-api.androidacy.com/auth/register + token = new String(Http.doHttpPost("https://" + this.host + "/auth/register?client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, "{\"device_id\":\"" + deviceId + "\"}", false)); // Parse token try { JSONObject jsonObject = new JSONObject(token); + Timber.d("Token: %s", token); token = jsonObject.getString("token"); memberLevel = jsonObject.getString("role"); } catch ( JSONException e) { Timber.e(e, "Failed to parse token"); // Show a toast - Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_parse_token, Toast.LENGTH_LONG).show(); + Looper mainLooper = Looper.getMainLooper(); + Handler handler = new Handler(mainLooper); + handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_parse_token, Toast.LENGTH_LONG).show()); return false; } // Ensure token is valid if (!isValidToken(token)) { Timber.e("Failed to validate token"); // Show a toast - Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_validate_token, Toast.LENGTH_LONG).show(); + Looper mainLooper = Looper.getMainLooper(); + Handler handler = new Handler(mainLooper); + handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_validate_token, Toast.LENGTH_LONG).show()); return false; } // Save token to shared preference @@ -277,8 +280,6 @@ public final class AndroidacyRepoData extends RepoData { return false; } } - //noinspection SillyAssignment // who are you calling silly? - token = token; return true; } 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 94ab973..fe56762 100644 --- a/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java +++ b/app/src/main/java/com/fox2code/mmm/background/BackgroundUpdateChecker.java @@ -1,10 +1,16 @@ package com.fox2code.mmm.background; import android.Manifest; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Build; import androidx.annotation.NonNull; import androidx.core.app.NotificationChannelCompat; @@ -13,7 +19,6 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import androidx.work.Worker; @@ -29,26 +34,66 @@ import com.fox2code.mmm.repo.RepoModule; import com.fox2code.mmm.utils.io.PropUtils; import java.util.HashMap; -import java.util.Random; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import timber.log.Timber; + public class BackgroundUpdateChecker extends Worker { public static final String NOTIFICATION_CHANNEL_ID = "background_update"; + public static final String NOTIFICATION_CHANNEL_ID_ONGOING = "background_update_status"; public static final int NOTIFICATION_ID = 1; + public static final int NOTIFICATION_ID_ONGOING = 2; + public static final String NOTFIICATION_GROUP = "updates"; static final Object lock = new Object(); // Avoid concurrency issues - private static boolean easterEggActive = false; 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.getSharedPreferences().getBoolean("pref_background_update_check", false)) { + return; + } + // next, check if user requires wifi + if (MainApplication.getSharedPreferences().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)) { + Timber.w("Background update check: wifi not connected but required"); + return; + } + } + // post checking notification if notofiications are enabled + if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + if (!MainApplication.getINSTANCE().isInForeground()) { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_LOW).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); + builder.setSmallIcon(R.drawable.ic_baseline_update_24); + builder.setPriority(NotificationCompat.PRIORITY_LOW); + builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION); + builder.setShowWhen(false); + builder.setOnlyAlertOnce(true); + builder.setOngoing(true); + builder.setAutoCancel(false); + builder.setGroup("update"); + builder.setContentTitle(context.getString(R.string.notification_channel_background_update)); + builder.setContentText(context.getString(R.string.notification_channel_background_update_description)); + notificationManager.notify(NOTIFICATION_ID_ONGOING, builder.build()); + } + } Thread.currentThread().setPriority(2); ModuleManager.getINSTANCE().scanAsync(); RepoManager.getINSTANCE().update(null); ModuleManager.getINSTANCE().runAfterScan(() -> { int moduleUpdateCount = 0; HashMap repoModules = RepoManager.getINSTANCE().getModules(); + // hasmap of updateable modules names + HashMap updateableModules = new HashMap<>(); for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { if ("twrp-keep".equals(localModuleInfo.id)) continue; @@ -56,26 +101,55 @@ public class BackgroundUpdateChecker extends Worker { try { if (MainApplication.getSharedPreferences().getStringSet("pref_background_update_check_excludes", null).contains(localModuleInfo.id)) continue; - } catch (Exception ignored) { + } catch ( + Exception ignored) { } RepoModule repoModule = repoModules.get(localModuleInfo.id); localModuleInfo.checkModuleUpdate(); if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode && !PropUtils.isNullString(localModuleInfo.updateVersion)) { moduleUpdateCount++; + updateableModules.put(localModuleInfo.name, localModuleInfo.version); } else if (repoModule != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode && !PropUtils.isNullString(repoModule.moduleInfo.version)) { moduleUpdateCount++; + updateableModules.put(localModuleInfo.name, localModuleInfo.version); } } if (moduleUpdateCount != 0) { - postNotification(context, moduleUpdateCount, false); + postNotification(context, updateableModules, moduleUpdateCount, false); } }); + // remove checking notification + if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(NOTIFICATION_ID_ONGOING); + } } - public static void postNotification(Context context, int updateCount, boolean test) { - if (!easterEggActive) - easterEggActive = new Random().nextInt(100) <= updateCount; - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).setContentTitle(context.getString(easterEggActive ? R.string.notification_update_title_easter_egg : R.string.notification_update_title).replace("%i", String.valueOf(updateCount))).setContentText(context.getString(R.string.notification_update_subtitle)).setSmallIcon(R.drawable.ic_baseline_extension_24).setPriority(NotificationCompat.PRIORITY_HIGH).setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK), PendingIntent.FLAG_IMMUTABLE)).setAutoCancel(true); + public static void postNotification(Context context, HashMap updateable, int updateCount, boolean test) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); + builder.setSmallIcon(R.drawable.baseline_system_update_24); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION); + builder.setShowWhen(false); + builder.setOnlyAlertOnce(true); + builder.setOngoing(false); + builder.setAutoCancel(true); + builder.setGroup(NOTFIICATION_GROUP); + // open app on click + Intent intent = new Intent(context, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + builder.setContentIntent(android.app.PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)); + // set summary to Found X updates: ... + StringBuilder summary = new StringBuilder(); + summary.append(context.getString(R.string.notification_update_summary)); + // use notification_update_module_template string to set name and version + for (Map.Entry entry : updateable.entrySet()) { + summary.append("\n").append(context.getString(R.string.notification_update_module_template, entry.getKey(), entry.getValue())); + } + builder.setContentTitle(context.getString(R.string.notification_update_title, updateCount)); + builder.setContentText(summary); + // set long text to summary so it doesn't get cut off + builder.setStyle(new NotificationCompat.BigTextStyle().bigText(summary)); if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; } @@ -89,16 +163,20 @@ public class BackgroundUpdateChecker extends Worker { // Refuse to run if first_launch pref is not false if (MainApplication.getSharedPreferences().getBoolean("first_time_setup_done", true)) return; + // create notification channel group + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence groupName = context.getString(R.string.notification_group_updates); + NotificationManager mNotificationManager = (NotificationManager) ContextCompat.getSystemService(context, NotificationManager.class); + Objects.requireNonNull(mNotificationManager).createNotificationChannelGroup(new NotificationChannelGroup(NOTFIICATION_GROUP, groupName)); + } 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)).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); - BackgroundUpdateChecker.easterEggActive = false; - WorkManager.getInstance(context).enqueueUniquePeriodicWork("background_checker", ExistingPeriodicWorkPolicy.REPLACE, new PeriodicWorkRequest.Builder(BackgroundUpdateChecker.class, 6, TimeUnit.HOURS).setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).setRequiredNetworkType(NetworkType.UNMETERED).build()).build()); + WorkManager.getInstance(context).enqueueUniquePeriodicWork("background_checker", ExistingPeriodicWorkPolicy.REPLACE, new PeriodicWorkRequest.Builder(BackgroundUpdateChecker.class, 6, TimeUnit.HOURS).setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).build()).build()); } public static void onMainActivityResume(Context context) { NotificationManagerCompat.from(context).cancel(BackgroundUpdateChecker.NOTIFICATION_ID); - BackgroundUpdateChecker.easterEggActive = false; } @NonNull 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 3288bdb..f5de328 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -83,7 +83,7 @@ public class InstallerActivity extends FoxActivity { return false; }); final Intent intent = this.getIntent(); - final String target; + String target; final String name; final String checksum; final boolean noExtensions; @@ -96,7 +96,12 @@ public class InstallerActivity extends FoxActivity { this.forceBackPressed(); return; } - target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH); + // ensure the intent is from our app, and is either a url or within our directory. replace all instances of .. and url encoded .. + target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH).trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", ""); + if (target.isEmpty() || !target.startsWith(MainApplication.getINSTANCE().getDataDir().getAbsolutePath()) && !target.startsWith("https://")) { + this.forceBackPressed(); + return; + } name = intent.getStringExtra(Constants.EXTRA_INSTALL_NAME); checksum = intent.getStringExtra(Constants.EXTRA_INSTALL_CHECKSUM); noExtensions = intent.getBooleanExtra(// Allow intent to disable extensions @@ -110,7 +115,6 @@ public class InstallerActivity extends FoxActivity { this.forceBackPressed(); return; } - Timber.i("Install link: %s", target); // Note: Sentry only send this info on crash. if (MainApplication.isCrashReportingEnabled()) { SentryBreadcrumb breadcrumb = new SentryBreadcrumb(); @@ -154,19 +158,29 @@ public class InstallerActivity extends FoxActivity { WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); this.progressIndicator.setVisibility(View.VISIBLE); if (urlMode) this.installerTerminal.addLine("- Downloading " + name); + String finalTarget = target; new Thread(() -> { + // ensure module cache is is in our cache dir + if (urlMode && !moduleCache.getAbsolutePath().startsWith(MainApplication.getINSTANCE().getCacheDir().getAbsolutePath())) + throw new SecurityException("Module cache is not in cache dir!"); File moduleCache = this.toDelete = urlMode ? - new File(this.moduleCache, "module.zip") : new File(target); + new File(this.moduleCache, "module.zip") : new File(finalTarget); + try { + if (!moduleCache.getCanonicalPath().startsWith(MainApplication.getINSTANCE().getCacheDir().getAbsolutePath())) + throw new SecurityException("Module cache is not in cache dir!"); + } catch ( + IOException ignored) { + } if (urlMode && moduleCache.exists() && !moduleCache.delete() && !new SuFile(moduleCache.getAbsolutePath()).delete()) Timber.e("Failed to delete module cache"); String errMessage = "Failed to download module zip"; // Set this to the error message if it's a HTTP error byte[] rawModule; - boolean androidacyBlame = false; // In case Androidacy mess-up again... yeah screw you too jk jk + boolean androidacyBlame = false; try { - Timber.i("%s%s", (urlMode ? "Downloading: " : "Loading: "), target); - rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> { + Timber.i("%s%s", (urlMode ? "Downloading: " : "Loading: "), finalTarget); + rawModule = urlMode ? Http.doHttpGet(finalTarget, (progress, max, done) -> { if (max <= 0 && this.progressIndicator.isIndeterminate()) return; this.runOnUiThread(() -> { @@ -180,9 +194,10 @@ public class InstallerActivity extends FoxActivity { this.progressIndicator.setIndeterminate(true); }); if (this.canceled) return; - androidacyBlame = urlMode && AndroidacyUtil.isAndroidacyFileUrl(target); + androidacyBlame = urlMode && AndroidacyUtil.isAndroidacyFileUrl(finalTarget); if (checksum != null && !checksum.isEmpty()) { - Timber.i("Checking for checksum: %s", checksum); + //noinspection UnnecessaryCallToStringValueOf + Timber.i("Checking for checksum: %s", String.valueOf(checksum)); this.runOnUiThread(() -> this.installerTerminal.addLine("- Checking file integrity")); if (!Hashes.checkSumMatch(rawModule, checksum)) { this.setInstallStateFinished(false, @@ -245,7 +260,6 @@ public class InstallerActivity extends FoxActivity { } else { errMessage = "Failed to patch module zip"; this.runOnUiThread(() -> this.installerTerminal.addLine("- Patching " + name)); - Timber.i("Patching: %s", moduleCache.getName()); try (OutputStream outputStream = new FileOutputStream(moduleCache)) { Files.patchModuleSimple(rawModule, outputStream); outputStream.flush(); diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java index a67b38e..e0372ad 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java @@ -98,7 +98,7 @@ public class InstallerInitializer extends Shell.Initializer { } catch (NoShellException e) { error = ERROR_NO_SU; Timber.w(e); - } catch (Throwable e) { + } catch (Exception e) { error = ERROR_OTHER; Timber.e(e); } 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 7fef234..5b01817 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java @@ -119,7 +119,14 @@ public class MarkdownActivity extends FoxActivity { configPkg + "\" missing for markdown view"); } } - Timber.i("Url for markdown %s", url); + // validate the url won't crash the app + if (url == null || url.isEmpty() || url.contains("..")) { + Timber.e("Invalid url %s", String.valueOf(url)); + this.forceBackPressed(); + return; + } + //noinspection UnnecessaryCallToStringValueOf + Timber.i("Url for markdown %s", String.valueOf(url)); setContentView(R.layout.markdown_view); final ViewGroup markdownBackground = findViewById(R.id.markdownBackground); final TextView textView = findViewById(R.id.markdownView); diff --git a/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java b/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java index 3dc7b2d..b54e3cd 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java +++ b/app/src/main/java/com/fox2code/mmm/module/ModuleHolder.java @@ -299,7 +299,15 @@ public final class ModuleHolder implements Comparable { // Note: This method should only be called if both element have the same type @Override public int compare(ModuleHolder o1, ModuleHolder o2) { - return 0; + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return -1; + } else if (o2 == null) { + return 1; + } else { + return o1.moduleId.compareTo(o2.moduleId); + } } } diff --git a/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java b/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java index 3e57380..b117dff 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java +++ b/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java @@ -323,7 +323,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(_view.getContext()); - builder.setTitle(R.string.low_quality_module).setMessage("Actual description for Low-quality module").setCancelable(true).setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show(); + builder.setTitle(R.string.low_quality_module).setMessage(R.string.low_quality_module_desc).setCancelable(true).setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show(); }); // Backup restore // foregroundAttr = R.attr.colorOnError; diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index 0abc1ec..3255578 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -93,7 +93,7 @@ public class RepoData extends XRepo { supportedProperties.put("installed", ""); supportedProperties.put("installedVersionCode", ""); } catch (JSONException e) { - e.printStackTrace(); + Timber.e(e, "Error while setting up supportedProperties"); } this.url = url; this.id = RepoManager.internalIdOfUrl(url); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java index b284328..a5b5b59 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -37,6 +37,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import timber.log.Timber; @@ -231,11 +232,16 @@ public final class RepoManager extends SyncManager { // Check if we have internet connection // Attempt to contact connectivitycheck.gstatic.com/generate_204 // If we can't, we don't have internet connection + HttpURLConnection urlConnection = null; try { Timber.d("Checking internet connection..."); // this url is actually hosted by Cloudflare and is not dependent on Androidacy servers being up - HttpURLConnection urlConnection = (HttpURLConnection) new URL("https://production-api.androidacy.com/cdn-cgi/trace").openConnection(); - Timber.d("Opened connection to %s", urlConnection.getURL()); + urlConnection = (HttpURLConnection) new URL("https://production-api.androidacy.com/cdn-cgi/trace").openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 10; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Mobile Safari/537.36"); + urlConnection.setRequestProperty("Accept", "*/*"); + urlConnection.setRequestProperty("Accept-Language", "en-US,en;q=0.5"); + Timber.d("Opened connection to %s", String.valueOf(urlConnection.getURL())); urlConnection.setInstanceFollowRedirects(false); urlConnection.setReadTimeout(1000); urlConnection.setUseCaches(false); @@ -243,17 +249,22 @@ public final class RepoManager extends SyncManager { // should return a 200 and the content should contain "visit_scheme=https" and ip= Timber.d("Response code: %s", urlConnection.getResponseCode()); // get the response body - String responseBody = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).lines().collect(Collectors.joining("\n")); - Timber.d("Response body: %s", responseBody); + BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String responseBody = reader.lines().collect(Collectors.joining("\n")); + reader.close(); // check if the response body contains the expected content if (urlConnection.getResponseCode() == 200 && responseBody.contains("visit_scheme=https") && responseBody.contains("ip=")) { this.hasInternet = true; } else { Timber.e("Failed to check internet connection"); } + // close output stream + Timber.d("Closed connection to %s", String.valueOf(urlConnection.getURL())); } catch ( IOException e) { Timber.e(e); + } finally { + Objects.requireNonNull(urlConnection).disconnect(); } for (int i = 0; i < repoDatas.length; i++) { if (BuildConfig.DEBUG) @@ -300,7 +311,7 @@ public final class RepoManager extends SyncManager { Timber.e(e); } updatedModules++; - updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules)); + updateListener.update(STEP1 + (STEP2 / (moduleToUpdate != 0 ? moduleToUpdate : 1) * updatedModules)); } for (RepoModule repoModule : repoUpdaters[i].toApply()) { if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) { diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java index 47cdd8d..26da2e0 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -281,14 +281,12 @@ public class RepoUpdater { realm.commitTransaction(); } catch ( Exception e) { - e.printStackTrace(); Timber.w("Failed to get module info from module " + module + " in repo " + this.repoData.id + " with error " + e.getMessage()); } } realm.close(); } catch ( - Exception e) { - e.printStackTrace(); + Exception ignored) { } this.indexRaw = null; RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); 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 2b7018b..16818ba 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -87,6 +87,7 @@ import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.security.NoSuchAlgorithmException; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Objects; @@ -116,16 +117,14 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { int totalCpuFreq = 0; int freqResolved = 0; for (int i = 0; i < cpuCount; i++) { - try { - RandomAccessFile reader = new RandomAccessFile(String.format(Locale.ENGLISH, "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i), "r"); + try (RandomAccessFile reader = new RandomAccessFile(String.format(Locale.ENGLISH, "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i), "r")) { String line = reader.readLine(); if (line != null) { totalCpuFreq += parseInt(line) / 1000; freqResolved++; } - reader.close(); } catch ( - Throwable ignore) { + Exception ignore) { } } int maxCpuFreq = freqResolved == 0 ? -1 : (int) Math.ceil(totalCpuFreq / (float) freqResolved); @@ -410,7 +409,23 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { debugNotification.setEnabled(MainApplication.isBackgroundUpdateCheckEnabled()); debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped() && MainApplication.isBackgroundUpdateCheckEnabled()); debugNotification.setOnPreferenceClickListener(preference -> { - BackgroundUpdateChecker.postNotification(this.requireContext(), new Random().nextInt(4) + 2, true); + // fake updateable modules hashmap + HashMap updateableModules = new HashMap<>(); + // count of modules to fake must match the count in the random number generator + Random random = new Random(); + int count; + do { + count = random.nextInt(4) + 2; + } while (count == 2); + for (int i = 0; i < count; i++) { + int fakeVersion; + do { + fakeVersion = random.nextInt(10); + } while (fakeVersion == 0); + Timber.d("Fake version: %s, count: %s", fakeVersion, i); + updateableModules.put("FakeModule " + i, "1.0." + fakeVersion); + } + BackgroundUpdateChecker.postNotification(this.requireContext(), updateableModules, count, true); return true; }); Preference backgroundUpdateCheck = findPreference("pref_background_update_check"); @@ -452,41 +467,47 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // updateCheckExcludes saves to pref_background_update_check_excludes as a stringset. On clicking, it should open a dialog with a list of all installed modules updateCheckExcludes.setOnPreferenceClickListener(preference -> { Collection localModuleInfos = ModuleManager.getINSTANCE().getModules().values(); - String[] moduleNames = new String[localModuleInfos.size()]; - boolean[] checkedItems = new boolean[localModuleInfos.size()]; - int i = 0; - for (LocalModuleInfo localModuleInfo : localModuleInfos) { - moduleNames[i] = localModuleInfo.name; - SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); - // get the stringset pref_background_update_check_excludes - Set stringSet = sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>()); - // Stringset uses id, we show name - checkedItems[i] = stringSet.contains(localModuleInfo.id); - Timber.d("name: %s, checked: %s", moduleNames[i], checkedItems[i]); - i++; - } - new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMultiChoiceItems(moduleNames, checkedItems, (dialog, which, isChecked) -> { - // get the stringset pref_background_update_check_excludes - SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); - Set stringSet = new HashSet<>(sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>())); - // get id from name - String id; - if (localModuleInfos.stream().anyMatch(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which]))) { - //noinspection OptionalGetWithoutIsPresent - id = localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which])).findFirst().get().id; - } else { - id = ""; + // make sure we have modules + boolean[] checkedItems; + if (!localModuleInfos.isEmpty()) { + String[] moduleNames = new String[localModuleInfos.size()]; + checkedItems = new boolean[localModuleInfos.size()]; + int i = 0; + for (LocalModuleInfo localModuleInfo : localModuleInfos) { + moduleNames[i] = localModuleInfo.name; + SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); + // get the stringset pref_background_update_check_excludes + Set stringSet = sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>()); + // Stringset uses id, we show name + checkedItems[i] = stringSet.contains(localModuleInfo.id); + Timber.d("name: %s, checked: %s", moduleNames[i], checkedItems[i]); + i++; } - if (!id.isEmpty()) { - if (isChecked) { - stringSet.add(id); + new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMultiChoiceItems(moduleNames, checkedItems, (dialog, which, isChecked) -> { + // get the stringset pref_background_update_check_excludes + SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); + Set stringSet = new HashSet<>(sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>())); + // get id from name + String id; + if (localModuleInfos.stream().anyMatch(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which]))) { + id = localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which])).findFirst().orElse(null).id; } else { - stringSet.remove(id); + id = ""; } - } - sharedPreferences.edit().putStringSet("pref_background_update_check_excludes", stringSet).apply(); - }).setPositiveButton(R.string.ok, (dialog, which) -> { - }).show(); + if (!id.isEmpty()) { + if (isChecked) { + stringSet.add(id); + } else { + stringSet.remove(id); + } + } + sharedPreferences.edit().putStringSet("pref_background_update_check_excludes", stringSet).apply(); + }).setPositiveButton(R.string.ok, (dialog, which) -> { + }).show(); + } else { + new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMessage(R.string.background_update_check_excludes_no_modules).setPositiveButton(R.string.ok, (dialog, which) -> { + }).show(); + } return true; }); final LibsBuilder libsBuilder = new LibsBuilder().withShowLoadingProgress(false).withLicenseShown(true).withAboutMinimalDesign(false); @@ -602,10 +623,11 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { saveLogs.setOnPreferenceClickListener(p -> { // Save logs to external storage File logsFile = new File(requireContext().getExternalFilesDir(null), "logs.txt"); + FileOutputStream fileOutputStream = null; try { //noinspection ResultOfMethodCallIgnored logsFile.createNewFile(); - FileOutputStream fileOutputStream = new FileOutputStream(logsFile); + fileOutputStream = new FileOutputStream(logsFile); // first, write some info about the device fileOutputStream.write(("FoxMagiskModuleManager version: " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")\n").getBytes()); fileOutputStream.write(("Android version: " + Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")\n").getBytes()); @@ -619,12 +641,18 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { while ((line = bufferedReader.readLine()) != null) { fileOutputStream.write((line + "\n").getBytes()); } - fileOutputStream.close(); } catch ( IOException e) { e.printStackTrace(); Toast.makeText(requireContext(), R.string.error_saving_logs, Toast.LENGTH_SHORT).show(); return true; + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException ignored) { + } + } } // Share logs Intent shareIntent = new Intent(); @@ -690,7 +718,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { try { initialApplication = FoxProcessExt.getInitialApplication(); } catch ( - Throwable ignored) { + Exception ignored) { } String realPackageName; if (initialApplication != null) { @@ -728,6 +756,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } } + @SuppressWarnings("ConstantConditions") public static class RepoFragment extends PreferenceFragmentCompat { /** @@ -1088,6 +1117,10 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } }); builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); + builder.setNeutralButton("Docs", (dialog, which) -> { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/docs/DEVELOPERS.md#custom-repo-format")); + startActivity(intent); + }); AlertDialog alertDialog = builder.show(); //make message clickable ((TextView) Objects.requireNonNull(alertDialog.findViewById(android.R.id.message))).setMovementMethod(LinkMovementMethod.getInstance()); @@ -1123,18 +1156,32 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } private void setRepoData(final RepoData repoData, String preferenceName) { + Timber.d("Setting preference " + preferenceName + " to " + repoData.toString()); ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE); Preference preference = findPreference(preferenceName); if (preference == null) return; if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { - Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); - if (repoData == null || repoData.isForceHide()) { + if (repoData != null) { + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + Realm realm = Realm.getInstance(realmConfiguration); + RealmResults repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.id).findAll(); + Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); + if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) { + Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden"); + hideRepoData(preferenceName); + return; + } else { + //noinspection ConstantConditions + Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), repoDataRealmResults.toString()); + preference.setTitle(repoData.getName()); + preference.setVisible(true); + } + } else { + Timber.d("Hiding preference " + preferenceName + " because it's data is null"); hideRepoData(preferenceName); return; } - preference.setTitle(repoData.getName()); - preference.setVisible(true); } preference = findPreference(preferenceName + "_enabled"); if (preference != null) { 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 1dfee4e..ab75cbf 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -78,7 +78,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "No application can handle this request.\n" + " Please install a web-browser", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -93,7 +92,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "No application can handle this request.\n" + " Please install a web-browser", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -123,7 +121,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "No application can handle this request." + " Please install a web-browser", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -165,7 +162,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "Failed to launch module config activity", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -186,7 +182,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "Failed to launch markdown activity", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -215,7 +210,6 @@ public enum IntentHelper { } catch (ActivityNotFoundException e) { Toast.makeText(context, "Failed to launch markdown activity", Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } } @@ -322,7 +316,7 @@ public enum IntentHelper { OnFileReceivedCallback callback) { File destinationFolder; if (destination == null || (destinationFolder = destination.getParentFile()) == null || - (!destinationFolder.isDirectory() && !destinationFolder.mkdirs())) { + (!destinationFolder.mkdirs() && !destinationFolder.isDirectory())) { callback.onReceived(destination, null, RESPONSE_ERROR); return; } diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/AddCookiesInterceptor.java b/app/src/main/java/com/fox2code/mmm/utils/io/AddCookiesInterceptor.java index d7ae8e7..3dab63f 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/AddCookiesInterceptor.java +++ b/app/src/main/java/com/fox2code/mmm/utils/io/AddCookiesInterceptor.java @@ -69,12 +69,16 @@ public class AddCookiesInterceptor implements Interceptor { //noinspection UnnecessaryContinue continue; } else { + // yeet any newlines from the cookie + cookie = cookie.replaceAll("[\\r\\n]", ""); builder.addHeader("Cookie", cookie); } } catch ( Exception ignored) { } } else { + // yeet any newlines from the cookie + cookie = cookie.replaceAll("[\\r\\n]", ""); builder.addHeader("Cookie", cookie); } } diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/Hashes.java b/app/src/main/java/com/fox2code/mmm/utils/io/Hashes.java index b1bec18..5da5af6 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/Hashes.java +++ b/app/src/main/java/com/fox2code/mmm/utils/io/Hashes.java @@ -19,30 +19,15 @@ public enum Hashes { hexChars[j * 2] = HEX_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } - return new String(hexChars); + return String.valueOf(hexChars); } public static String hashMd5(byte[] input) { - Timber.w("hashMd5: This method is insecure, use hashSha256 instead"); - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - - return bytesToHex(md.digest(input)); - } catch ( - NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + throw new SecurityException("MD5 is not secure"); } public static String hashSha1(byte[] input) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - - return bytesToHex(md.digest(input)); - } catch ( - NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + throw new SecurityException("SHA-1 is not secure"); } public static String hashSha256(byte[] input) { diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/Http.java b/app/src/main/java/com/fox2code/mmm/utils/io/Http.java index 585b7e0..95786f3 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/io/Http.java @@ -90,7 +90,7 @@ public enum Http { cookieManager.setAcceptCookie(true); cookieManager.flush(); // Make sure the instance work } catch ( - Throwable t) { + Exception t) { cookieManager = null; Timber.e(t, "No WebView support!"); } diff --git a/app/src/main/java/com/fox2code/mmm/utils/realm/ModuleListCache.java b/app/src/main/java/com/fox2code/mmm/utils/realm/ModuleListCache.java index 980a39a..714f163 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/realm/ModuleListCache.java +++ b/app/src/main/java/com/fox2code/mmm/utils/realm/ModuleListCache.java @@ -8,6 +8,7 @@ import io.realm.RealmObject; import io.realm.RealmResults; import io.realm.annotations.PrimaryKey; import io.realm.annotations.Required; +import timber.log.Timber; @SuppressWarnings("unused") public class ModuleListCache extends RealmObject { @@ -69,7 +70,7 @@ public class ModuleListCache extends RealmObject { jsonObject.put(module.getId(), module.toJson()); } catch ( JSONException e) { - e.printStackTrace(); + Timber.e(e); } } realm.close(); diff --git a/app/src/main/res/drawable/baseline_network_wifi_24.xml b/app/src/main/res/drawable/baseline_network_wifi_24.xml new file mode 100644 index 0000000..8941313 --- /dev/null +++ b/app/src/main/res/drawable/baseline_network_wifi_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ee9ceb..cc460cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,10 +140,10 @@ Use staging Androidacy endpoint instead of release endpoint. (Will restart app) - Found %i module updates + Found %1$d module updates Sniffed %i module updates Click to open the app - Background modules update check + Automatic modules update check May increase battery usage Test Notification @@ -340,6 +340,19 @@ An error occurred downloading the update information. ERROR: Failed to parse update information Downloading update… %1$d%% - Installing update…ERROR: Could not find update package.Check for app updatesTest update download mechanismNo changes yet!Cancel updateThe URL you entered for the repo is invalid - Repos must be served over HTTPS, and must follow the spec outlined in the docs. + Installing update… + ERROR: Could not find update package. + Check for app updates + Test update download mechanism + No changes yet! + Cancel update + The URL you entered for the repo is invalid + Repos must be served over HTTPS, and must follow the spec outlined in the documentation. + The following modules can be updated: + %1$s to version %2$s + Checking for updates... + FoxMMM is checking for updates in the background.Background update status + Shows a notification while checking for updates so the system doesn\'t kill it + Only check on WiFi + Require wi-fi or an unmetered network for update checks. Recommended to leave on if you have limited mobile data.No modules installed on deviceNotifies when module updates are foundUpdatesThis module has metadata that is either invalid or considered a marker for a low-quality module. Uninstallation is recommended. diff --git a/app/src/main/res/xml/repo_preferences.xml b/app/src/main/res/xml/repo_preferences.xml index 590cfc5..bedf844 100644 --- a/app/src/main/res/xml/repo_preferences.xml +++ b/app/src/main/res/xml/repo_preferences.xml @@ -97,6 +97,7 @@ @@ -136,6 +137,7 @@ + + +