You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
539 lines
20 KiB
Java
539 lines
20 KiB
Java
package com.fox2code.mmm.androidacy;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Color;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.util.TypedValue;
|
|
import android.view.View;
|
|
import android.webkit.JavascriptInterface;
|
|
import android.widget.Button;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.Keep;
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.core.content.ContextCompat;
|
|
|
|
import com.fox2code.foxcompat.view.FoxDisplay;
|
|
import com.fox2code.mmm.BuildConfig;
|
|
import com.fox2code.mmm.MainApplication;
|
|
import com.fox2code.mmm.R;
|
|
import com.fox2code.mmm.installer.InstallerInitializer;
|
|
import com.fox2code.mmm.manager.LocalModuleInfo;
|
|
import com.fox2code.mmm.manager.ModuleInfo;
|
|
import com.fox2code.mmm.manager.ModuleManager;
|
|
import com.fox2code.mmm.repo.RepoModule;
|
|
import com.fox2code.mmm.utils.ExternalHelper;
|
|
import com.fox2code.mmm.utils.IntentHelper;
|
|
import com.fox2code.mmm.utils.io.Files;
|
|
import com.fox2code.mmm.utils.io.Hashes;
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import timber.log.Timber;
|
|
|
|
@SuppressWarnings("SameReturnValue")
|
|
@Keep
|
|
public class AndroidacyWebAPI {
|
|
public static final int COMPAT_UNSUPPORTED = 0;
|
|
public static final int COMPAT_DOWNLOAD = 1;
|
|
private static final int MAX_COMPAT_MODE = 1;
|
|
private final AndroidacyActivity activity;
|
|
private final boolean allowInstall;
|
|
boolean consumedAction;
|
|
boolean downloadMode;
|
|
int effectiveCompatMode;
|
|
int notifiedCompatMode;
|
|
private boolean allowHideNote = true;
|
|
|
|
public AndroidacyWebAPI(AndroidacyActivity activity, boolean allowInstall) {
|
|
this.activity = activity;
|
|
this.allowInstall = allowInstall;
|
|
}
|
|
|
|
void forceQuitRaw(String error) {
|
|
Toast.makeText(this.activity, error, Toast.LENGTH_LONG).show();
|
|
this.activity.runOnUiThread(this.activity::forceBackPressed);
|
|
this.activity.backOnResume = true; // Set backOnResume just in case
|
|
this.downloadMode = false;
|
|
}
|
|
|
|
void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle, String checksum, boolean canInstall) {
|
|
if (BuildConfig.DEBUG)
|
|
Timber.d("ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl) + ", moduleId: " + moduleId + ", installTitle: " + installTitle + ", checksum: " + checksum + ", canInstall: " + canInstall);
|
|
this.downloadMode = false;
|
|
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
|
|
String title, description;
|
|
boolean mmtReborn = false;
|
|
if (repoModule != null) {
|
|
title = repoModule.moduleInfo.name;
|
|
description = repoModule.moduleInfo.description;
|
|
mmtReborn = repoModule.moduleInfo.mmtReborn;
|
|
if (description == null || description.length() == 0) {
|
|
description = this.activity.getString(R.string.no_desc_found);
|
|
}
|
|
} else {
|
|
// URL Decode installTitle
|
|
title = installTitle;
|
|
String checkSumType = Hashes.checkSumName(checksum);
|
|
if (checkSumType == null) {
|
|
description = "Checksum: " + ((checksum == null || checksum.isEmpty()) ? "null" : checksum);
|
|
} else {
|
|
description = checkSumType + ": " + checksum;
|
|
}
|
|
}
|
|
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this.activity);
|
|
builder.setTitle(title).setMessage(description).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24);
|
|
builder.setNegativeButton(R.string.download_module, (x, y) -> {
|
|
this.downloadMode = true;
|
|
IntentHelper.openCustomTab(this.activity, moduleUrl);
|
|
});
|
|
if (canInstall) {
|
|
boolean hasUpdate = false;
|
|
String config = null;
|
|
if (repoModule != null) {
|
|
config = repoModule.moduleInfo.config;
|
|
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(repoModule.id);
|
|
hasUpdate = localModuleInfo != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode;
|
|
}
|
|
final String fModuleUrl = moduleUrl, fTitle = title, fConfig = config, fChecksum = checksum;
|
|
final boolean fMMTReborn = mmtReborn;
|
|
builder.setPositiveButton(hasUpdate ? R.string.update_module : R.string.install_module, (x, y) -> IntentHelper.openInstaller(this.activity, fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn));
|
|
}
|
|
builder.setOnCancelListener(dialogInterface -> {
|
|
if (!this.activity.backOnResume)
|
|
this.consumedAction = false;
|
|
});
|
|
ExternalHelper.INSTANCE.injectButton(builder, () -> {
|
|
this.downloadMode = true;
|
|
try {
|
|
return this.activity.downloadFileAsync(moduleUrl);
|
|
} catch (
|
|
IOException e) {
|
|
Timber.e(e, "Failed to download module");
|
|
AndroidacyWebAPI.this.activity.runOnUiThread(() -> Toast.makeText(AndroidacyWebAPI.this.activity, R.string.failed_download, Toast.LENGTH_SHORT).show());
|
|
return null;
|
|
}
|
|
}, "androidacy_repo");
|
|
final int dim5dp = FoxDisplay.dpToPixel(5);
|
|
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp);
|
|
this.activity.runOnUiThread(() -> {
|
|
AlertDialog alertDialog = builder.show();
|
|
for (int i = -3; i < 0; i++) {
|
|
Button alertButton = alertDialog.getButton(i);
|
|
if (alertButton != null && alertButton.getPaddingStart() > dim5dp) {
|
|
alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void notifyCompatModeRaw(int value) {
|
|
if (this.consumedAction)
|
|
return;
|
|
if (BuildConfig.DEBUG)
|
|
Timber.d("Androidacy Compat mode: %s", value);
|
|
this.notifiedCompatMode = value;
|
|
if (value < 0) {
|
|
value = 0;
|
|
} else if (value > MAX_COMPAT_MODE) {
|
|
value = MAX_COMPAT_MODE;
|
|
}
|
|
this.effectiveCompatMode = value;
|
|
}
|
|
|
|
@JavascriptInterface
|
|
public void forceQuit(String error) {
|
|
// Allow forceQuit and cancel in downloadMode
|
|
if (this.consumedAction && !this.downloadMode)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.forceQuitRaw(error);
|
|
}
|
|
|
|
@JavascriptInterface
|
|
public void cancel() {
|
|
// Allow forceQuit and cancel in downloadMode
|
|
if (this.consumedAction && !this.downloadMode)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.activity.runOnUiThread(this.activity::forceBackPressed);
|
|
}
|
|
|
|
/**
|
|
* Open an url always in an external page or browser.
|
|
*/
|
|
@JavascriptInterface
|
|
public void openUrl(String url) {
|
|
if (this.consumedAction)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.downloadMode = false;
|
|
if (BuildConfig.DEBUG)
|
|
Timber.d("Received openUrl request: %s", url);
|
|
if (Uri.parse(url).getScheme().equals("https")) {
|
|
IntentHelper.openUrl(this.activity, url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open an url in a custom tab if possible.
|
|
*/
|
|
@JavascriptInterface
|
|
public void openCustomTab(String url) {
|
|
if (this.consumedAction)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.downloadMode = false;
|
|
if (BuildConfig.DEBUG)
|
|
Timber.d("Received openCustomTab request: %s", url);
|
|
if (Uri.parse(url).getScheme().equals("https")) {
|
|
IntentHelper.openCustomTab(this.activity, url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return if current theme is a light theme.
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean isLightTheme() {
|
|
return MainApplication.getINSTANCE().isLightTheme();
|
|
}
|
|
|
|
/**
|
|
* Check if the manager has received root access
|
|
* (Note: hasRoot only return true on Magisk rooted phones)
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean hasRoot() {
|
|
return InstallerInitializer.peekMagiskPath() != null;
|
|
}
|
|
|
|
/**
|
|
* Check if the install API can be used
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean canInstall() {
|
|
// With lockdown mode enabled or lack of root, install should not have any effect
|
|
return this.allowInstall && this.hasRoot() && !MainApplication.isShowcaseMode();
|
|
}
|
|
|
|
/**
|
|
* install a module via url, with the file checked with the md5 checksum value.
|
|
*/
|
|
@JavascriptInterface
|
|
public void install(String moduleUrl, String installTitle, String checksum) {
|
|
// If compat mode is 0, this means Androidacy didn't implemented a download mode yet
|
|
if (this.consumedAction || (this.effectiveCompatMode >= 1 && !this.canInstall())) {
|
|
return;
|
|
}
|
|
this.consumedAction = true;
|
|
this.downloadMode = false;
|
|
if (BuildConfig.DEBUG)
|
|
Timber.d("Received install request: " + moduleUrl + " " + installTitle + " " + checksum);
|
|
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
|
|
this.forceQuitRaw("Non Androidacy module link used on Androidacy");
|
|
return;
|
|
}
|
|
checksum = Hashes.checkSumFormat(checksum);
|
|
if (checksum == null || checksum.isEmpty()) {
|
|
Timber.w("Androidacy WebView didn't provided a checksum!");
|
|
} else if (!Hashes.checkSumValid(checksum)) {
|
|
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
|
|
return;
|
|
}
|
|
// moduleId is the module parameter in the url
|
|
String moduleId = AndroidacyUtil.getModuleId(moduleUrl);
|
|
// Let's handle download mode ourself if not implemented
|
|
if (this.effectiveCompatMode < 1) {
|
|
if (!this.canInstall()) {
|
|
this.downloadMode = true;
|
|
this.activity.runOnUiThread(() -> this.activity.webView.loadUrl(moduleUrl));
|
|
} else {
|
|
this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
|
|
}
|
|
} else {
|
|
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
|
|
String config = null;
|
|
boolean mmtReborn = false;
|
|
if (repoModule != null && repoModule.moduleInfo.name.length() >= 3) {
|
|
installTitle = repoModule.moduleInfo.name; // Set title to module name
|
|
config = repoModule.moduleInfo.config;
|
|
mmtReborn = repoModule.moduleInfo.mmtReborn;
|
|
}
|
|
this.activity.backOnResume = true;
|
|
IntentHelper.openInstaller(this.activity, moduleUrl, installTitle, config, checksum, mmtReborn);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* install a module via url, with the file checked with the md5 checksum value.
|
|
*/
|
|
@JavascriptInterface
|
|
public void openNativeModuleDialog(String moduleUrl, String moduleId, String checksum) {
|
|
if (this.consumedAction)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.downloadMode = false;
|
|
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
|
|
this.forceQuitRaw("Non Androidacy module link used on Androidacy");
|
|
return;
|
|
}
|
|
checksum = Hashes.checkSumFormat(checksum);
|
|
if (checksum == null || checksum.isEmpty()) {
|
|
Timber.w("Androidacy WebView didn't provided a checksum!");
|
|
} else if (!Hashes.checkSumValid(checksum)) {
|
|
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
|
|
return;
|
|
}
|
|
// Get moduleTitle from url
|
|
String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl);
|
|
this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall());
|
|
}
|
|
|
|
/**
|
|
* Tell if the moduleId is installed on the device
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean isModuleInstalled(String moduleId) {
|
|
return ModuleManager.getINSTANCE().getModules().get(moduleId) != null;
|
|
}
|
|
|
|
/**
|
|
* Tell if the moduleId is updating and waiting a reboot to update
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean isModuleUpdating(String moduleId) {
|
|
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
|
|
return localModuleInfo != null && localModuleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING);
|
|
}
|
|
|
|
/**
|
|
* Return the module version name or null if not installed.
|
|
*/
|
|
@JavascriptInterface
|
|
public String getModuleVersion(String moduleId) {
|
|
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
|
|
return localModuleInfo != null ? localModuleInfo.version : null;
|
|
}
|
|
|
|
/**
|
|
* Return the module version code or -1 if not installed.
|
|
*/
|
|
@JavascriptInterface
|
|
public long getModuleVersionCode(String moduleId) {
|
|
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
|
|
return localModuleInfo != null ? localModuleInfo.versionCode : -1L;
|
|
}
|
|
|
|
/**
|
|
* Hide action bar if visible, the action bar is only visible by default on notes.
|
|
*/
|
|
@JavascriptInterface
|
|
public void hideActionBar() {
|
|
if (this.consumedAction)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.activity.runOnUiThread(() -> {
|
|
this.activity.hideActionBar();
|
|
this.consumedAction = false;
|
|
if (this.allowHideNote) {
|
|
this.allowHideNote = false;
|
|
this.activity.webViewNote.setVisibility(View.GONE);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show action bar if not visible, the action bar is only visible by default on notes.
|
|
* Optional title param to set action bar title.
|
|
*/
|
|
@JavascriptInterface
|
|
public void showActionBar(final String title) {
|
|
if (this.consumedAction)
|
|
return;
|
|
this.consumedAction = true;
|
|
this.activity.runOnUiThread(() -> {
|
|
this.activity.showActionBar();
|
|
if (title != null && !title.isEmpty()) {
|
|
this.activity.setTitle(title);
|
|
}
|
|
this.consumedAction = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return true if the module is an Andoridacy module.
|
|
*/
|
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
@JavascriptInterface
|
|
public boolean isAndroidacyModule(String moduleId) {
|
|
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
|
|
return localModuleInfo != null && ("Androidacy".equals(localModuleInfo.author) || AndroidacyUtil.isAndroidacyLink(localModuleInfo.config));
|
|
}
|
|
|
|
/**
|
|
* get a module file, return an empty string if not
|
|
* an Androidacy module or if file doesn't exists.
|
|
*/
|
|
@JavascriptInterface
|
|
public String getAndroidacyModuleFile(String moduleId, String moduleFile) {
|
|
if (moduleFile == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
|
|
return "";
|
|
File moduleFolder = new File("/data/adb/modules/" + moduleId);
|
|
File absModuleFile = new File(moduleFolder, moduleFile).getAbsoluteFile();
|
|
if (!absModuleFile.getPath().startsWith(moduleFolder.getPath()))
|
|
return "";
|
|
try {
|
|
return new String(Files.readSU(absModuleFile.getAbsoluteFile()), StandardCharsets.UTF_8);
|
|
} catch (
|
|
IOException e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an ".androidacy" file with {@param content} as content
|
|
* Return true if action succeeded
|
|
*/
|
|
@JavascriptInterface
|
|
public boolean setAndroidacyModuleMeta(String moduleId, String content) {
|
|
if (content == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
|
|
return false;
|
|
File androidacyMetaFile = new File("/data/adb/modules/" + moduleId + "/.androidacy");
|
|
try {
|
|
Files.writeSU(androidacyMetaFile, content.getBytes(StandardCharsets.UTF_8));
|
|
return true;
|
|
} catch (
|
|
IOException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return current app version code
|
|
*/
|
|
@JavascriptInterface
|
|
public int getAppVersionCode() {
|
|
return BuildConfig.VERSION_CODE;
|
|
}
|
|
|
|
/**
|
|
* Return current app version name
|
|
*/
|
|
@JavascriptInterface
|
|
public String getAppVersionName() {
|
|
return BuildConfig.VERSION_NAME;
|
|
}
|
|
|
|
/**
|
|
* Return current magisk version code or 0 if not applicable
|
|
*/
|
|
@JavascriptInterface
|
|
public int getMagiskVersionCode() {
|
|
return InstallerInitializer.peekMagiskPath() == null ? 0 : InstallerInitializer.peekMagiskVersion();
|
|
}
|
|
|
|
/**
|
|
* Return current android sdk-int version code, see:
|
|
* <a href="https://source.android.com/setup/start/build-numbers">right here</a>
|
|
*/
|
|
@JavascriptInterface
|
|
public int getAndroidVersionCode() {
|
|
return Build.VERSION.SDK_INT;
|
|
}
|
|
|
|
/**
|
|
* Return current navigation bar height or 0 if not visible
|
|
*/
|
|
@JavascriptInterface
|
|
public int getNavigationBarHeight() {
|
|
return this.activity.getNavigationBarHeight();
|
|
}
|
|
|
|
/**
|
|
* Allow Androidacy backend to notify compat mode
|
|
* return current effective compat mode
|
|
*/
|
|
@JavascriptInterface
|
|
public int getEffectiveCompatMode() {
|
|
return this.effectiveCompatMode;
|
|
}
|
|
|
|
/**
|
|
* Return current theme accent color
|
|
*/
|
|
@JavascriptInterface
|
|
public int getAccentColor() {
|
|
Resources.Theme theme = this.activity.getTheme();
|
|
TypedValue typedValue = new TypedValue();
|
|
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
|
|
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
|
|
return typedValue.data;
|
|
}
|
|
theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true);
|
|
return typedValue.data;
|
|
}
|
|
|
|
/**
|
|
* Return current theme foreground color
|
|
*/
|
|
@JavascriptInterface
|
|
public int getForegroundColor() {
|
|
return this.activity.isLightTheme() ? Color.BLACK : Color.WHITE;
|
|
}
|
|
|
|
/**
|
|
* Return current theme background color
|
|
*/
|
|
@JavascriptInterface
|
|
public int getBackgroundColor() {
|
|
Resources.Theme theme = this.activity.getTheme();
|
|
TypedValue typedValue = new TypedValue();
|
|
theme.resolveAttribute(R.attr.backgroundColor, typedValue, true);
|
|
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
|
|
return typedValue.data;
|
|
}
|
|
theme.resolveAttribute(android.R.attr.background, typedValue, true);
|
|
return typedValue.data;
|
|
}
|
|
|
|
/**
|
|
* Return current hex string of monet theme
|
|
*/
|
|
@JavascriptInterface
|
|
public String getMonetColor(String id) {
|
|
@SuppressLint("DiscouragedApi") int nameResourceID = this.activity.getResources().getIdentifier("@android:color/" + id, "color", this.activity.getApplicationInfo().packageName);
|
|
if (nameResourceID == 0) {
|
|
throw new IllegalArgumentException("No resource string found with name " + id);
|
|
} else {
|
|
int color = ContextCompat.getColor(this.activity, nameResourceID);
|
|
int red = Color.red(color);
|
|
int blue = Color.blue(color);
|
|
int green = Color.green(color);
|
|
return String.format("#%02x%02x%02x", red, green, blue);
|
|
}
|
|
}
|
|
|
|
@JavascriptInterface
|
|
public void setAndroidacyToken(String token) {
|
|
AndroidacyRepoData.getInstance().setToken(token);
|
|
}
|
|
|
|
// Androidacy feature level declaration method
|
|
|
|
@JavascriptInterface
|
|
public void notifyCompatUnsupported() {
|
|
this.notifyCompatModeRaw(COMPAT_UNSUPPORTED);
|
|
}
|
|
|
|
@JavascriptInterface
|
|
public void notifyCompatDownloadButton() {
|
|
this.notifyCompatModeRaw(COMPAT_DOWNLOAD);
|
|
}
|
|
}
|