apply best practices

fix a lot of best practices and respect user choice for needing wifi for bg updates

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/284/head
androidacy-user 1 year ago
parent a277a7e18e
commit 4fc7a94f78

@ -10,21 +10,40 @@
<intent>
<action android:name="com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL" />
</intent>
</queries> <!-- Wifi is not the only way to get an internet connection -->
</queries>
<!-- Wifi is not the only way to get an internet connection -->
<uses-feature
android:name="android.hardware.wifi"
android:required="false" /> <!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- WebView offline webpage support -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Check if there is modules updates on boot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- Open config apps for applications -->
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Post background notifications -->
android:required="false" />
<!-- uses webview -->
<uses-feature
android:name="android.software.webview"
android:required="false" />
<!-- uses opengl 1.2 -->
<uses-feature
android:name="android.hardware.opengles.aep"
android:required="false" />
<uses-feature
android:glEsVersion="0x00020000" android:required="false" />
<!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- WebView offline webpage support -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Check if there is modules updates on boot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Open config apps for applications -->
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- Open and read zips -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Post background notifications -->
<uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Install updates -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/full_backup_content"
@ -41,17 +60,14 @@
tools:targetApi="tiramisu">
<activity
android:name=".UpdateActivity"
android:hardwareAccelerated="true"
android:exported="false" />
<activity
android:name=".CrashHandler"
android:exported="false"
android:hardwareAccelerated="true"
android:process=":crash" />
<activity
android:name=".SetupActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:label="@string/title_activity_setup"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
@ -66,7 +82,6 @@
<activity
android:name=".settings.SettingsActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity">
<intent-filter>
@ -76,7 +91,6 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:label="@string/app_name_short"
android:launchMode="singleTask">
<intent-filter>
@ -88,7 +102,6 @@
<activity
android:name=".installer.InstallerActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity"
android:screenOrientation="portrait"
@ -103,7 +116,6 @@
<activity
android:name=".utils.ZipFileOpener"
android:exported="true"
android:hardwareAccelerated="true"
android:theme="@style/Theme.MagiskModuleManager">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -118,13 +130,11 @@
<activity
android:name=".markdown.MarkdownActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager" />
<activity
android:name=".androidacy.AndroidacyActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager">
@ -165,22 +175,28 @@
android:value="false" />
<meta-data
android:name="io.sentry.dsn"
android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" /> <!-- Sane value, but feel free to lower it -->
android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" />
<!-- Sane value, but feel free to lower it -->
<meta-data
android:name="io.sentry.traces.sample-rate"
android:value="0.5" /> <!-- Doesn't actually monitor anything, just used to get the activities the user went through -->
android:value="0.25" />
<!-- Doesn't actually monitor anything, just used to get the activities the user went through -->
<meta-data
android:name="io.sentry.traces.user-interaction.enable"
android:value="true" /> <!-- Just a screenshot of ONLY the current activity at the time of the crash -->
android:value="true" />
<!-- Just a screenshot of ONLY the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" /> <!-- Just the current activity at the time of the crash -->
android:value="true" />
<!-- Just the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-stacktrace"
android:value="true" /> <!-- Don't send PII, this is actually default but let's be explicit -->
android:value="true" />
<!-- Don't send PII, this is actually default but let's be explicit -->
<meta-data
android:name="io.sentry.sendDefaultPii"
android:value="false" />
<!-- Performance profiling -->
<meta-data
android:name="io.sentry.traces.profiling.sample-rate"
android:value="0.25" />

@ -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) {

@ -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();

@ -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;

@ -60,7 +60,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
public static final HashSet<String> 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

@ -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<ReposList> 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();
}
}
});
}

@ -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);

@ -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) {

@ -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;
}

@ -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<String, RepoModule> repoModules = RepoManager.getINSTANCE().getModules();
// hasmap of updateable modules names
HashMap<String, String> 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<String, String> 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: <module name> <module version> <module name> <module version> ...
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<String, String> 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

@ -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();

@ -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);
}

@ -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);

@ -299,7 +299,15 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
// 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);
}
}
}

@ -323,7 +323,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
} else if (type == ModuleHolder.Type.INSTALLED && moduleHolder.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
this.invalidPropsChip.setOnClickListener(_view -> {
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;

@ -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);

@ -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=<some 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) {

@ -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();

@ -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<String, String> 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<LocalModuleInfo> 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<String> 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<String> 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<String> 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<String> 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<ReposList> 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) {

@ -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;
}

@ -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);
}
}

@ -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) {

@ -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!");
}

@ -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();

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M24,9C20.9,5.9 16.7,4 12,4C7.3,4 3.1,5.9 0,9L12,21v0l0,0L24,9zM2.9,9.1C5.5,7.1 8.7,6 12,6s6.5,1.1 9.1,3.1l-1.4,1.4C17.5,8.9 14.9,8 12,8s-5.5,0.9 -7.7,2.5L2.9,9.1z"/>
</vector>

@ -140,10 +140,10 @@
<string name="androidacy_test_mode_desc">Use staging Androidacy endpoint instead of release endpoint. (Will restart app)</string>
<!-- Background Notification translation -->
<string name="notification_update_title">Found %i module updates</string>
<string name="notification_update_title">Found %1$d module updates</string>
<string name="notification_update_title_easter_egg">Sniffed %i module updates</string>
<string name="notification_update_subtitle">Click to open the app</string>
<string name="notification_update_pref">Background modules update check</string>
<string name="notification_update_pref">Automatic modules update check</string>
<string name="notification_update_desc">May increase battery usage</string>
<string name="notification_update_debug_pref">Test Notification</string>
@ -340,6 +340,19 @@
<string name="error_download_update">An error occurred downloading the update information.</string>
<string name="error_no_asset">ERROR: Failed to parse update information</string>
<string name="downloading_update">Downloading update… %1$d%%</string>
<string name="installing_update">Installing update…</string><string name="no_file_found">ERROR: Could not find update package.</string><string name="check_for_updates">Check for app updates</string><string name="update_debug_download_pref">Test update download mechanism</string><string name="changelog_none">No changes yet!</string><string name="update_cancel_button">Cancel update</string><string name="invalid_repo_url">The URL you entered for the repo is invalid</string>
<string name="add_repo_message">Repos must be served over HTTPS, and must follow the spec outlined in the <a href="https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/docs/DEVELOPERS.md#custom-repo-format">docs</a>.</string>
<string name="installing_update">Installing update…</string>
<string name="no_file_found">ERROR: Could not find update package.</string>
<string name="check_for_updates">Check for app updates</string>
<string name="update_debug_download_pref">Test update download mechanism</string>
<string name="changelog_none">No changes yet!</string>
<string name="update_cancel_button">Cancel update</string>
<string name="invalid_repo_url">The URL you entered for the repo is invalid</string>
<string name="add_repo_message">Repos must be served over HTTPS, and must follow the spec outlined in the documentation.</string>
<string name="notification_update_summary">The following modules can be updated:</string>
<string name="notification_update_module_template">%1$s to version %2$s</string>
<string name="notification_channel_background_update">Checking for updates...</string>
<string name="notification_channel_background_update_description">FoxMMM is checking for updates in the background.</string><string name="notification_channel_category_background_update">Background update status</string>
<string name="notification_channel_category_background_update_description">Shows a notification while checking for updates so the system doesn\'t kill it</string>
<string name="notification_update_wifi_desc">Only check on WiFi</string>
<string name="notification_update_wifi_pref">Require wi-fi or an unmetered network for update checks. Recommended to leave on if you have limited mobile data.</string><string name="background_update_check_excludes_no_modules">No modules installed on device</string><string name="auto_updates_notifs">Notifies when module updates are found</string><string name="notification_group_updates">Updates</string><string name="low_quality_module_desc">This module has metadata that is either invalid or considered a marker for a low-quality module. Uninstallation is recommended.</string>
</resources>

@ -97,6 +97,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_0"
app:isPreferenceVisible="false"
app:title="@string/loading">
<!-- Custom repos can't be enabled/disabled. Instead, they must be deleted. Show a disabled
switch to indicate that. -->
@ -136,6 +137,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_1"
app:isPreferenceVisible="false"
app:title="@string/loading">
<SwitchPreferenceCompat
app:defaultValue="true"
@ -173,6 +175,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_2"
app:isPreferenceVisible="false"
app:title="@string/loading">
<SwitchPreferenceCompat
app:defaultValue="true"
@ -210,6 +213,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_3"
app:isPreferenceVisible="false"
app:title="@string/loading">
<SwitchPreferenceCompat
app:defaultValue="true"
@ -247,6 +251,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_4"
app:isPreferenceVisible="false"
app:title="@string/loading">
<SwitchPreferenceCompat
app:defaultValue="true"

@ -43,6 +43,15 @@
app:summary="@string/notification_update_desc"
app:title="@string/notification_update_pref" />
<!-- require wifi -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/baseline_network_wifi_24"
app:key="pref_background_update_check_wifi"
app:singleLineTitle="false"
app:summary="@string/notification_update_wifi_pref"
app:title="@string/notification_update_wifi_desc" />
<!-- Ignore updates for preference. Used to ignore updates for specific modules -->
<Preference
app:icon="@drawable/baseline_block_24"

Loading…
Cancel
Save