fixed bugs
added some more for later 😈
Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/27/head
parent
a196ca32d3
commit
9424c16b4a
@ -1,451 +0,0 @@
|
||||
package com.fox2code.mmm.androidacy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.net.http.SslError;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.SslErrorHandler;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.webkit.WebResourceErrorCompat;
|
||||
import androidx.webkit.WebSettingsCompat;
|
||||
import androidx.webkit.WebViewClientCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
import com.fox2code.foxcompat.app.FoxActivity;
|
||||
import com.fox2code.mmm.BuildConfig;
|
||||
import com.fox2code.mmm.Constants;
|
||||
import com.fox2code.mmm.MainApplication;
|
||||
import com.fox2code.mmm.R;
|
||||
import com.fox2code.mmm.XHooks;
|
||||
import com.fox2code.mmm.utils.IntentHelper;
|
||||
import com.fox2code.mmm.utils.io.net.Http;
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
|
||||
import org.matomo.sdk.extra.TrackHelper;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Per Androidacy repo implementation agreement, no request of this WebView shall be modified.
|
||||
*/
|
||||
public final class AndroidacyActivity extends FoxActivity {
|
||||
|
||||
static {
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
File moduleFile;
|
||||
WebView webView;
|
||||
TextView webViewNote;
|
||||
AndroidacyWebAPI androidacyWebAPI;
|
||||
LinearProgressIndicator progressIndicator;
|
||||
boolean backOnResume;
|
||||
boolean downloadMode;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi", "ClickableViewAccessibility"})
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
this.moduleFile = new File(this.getCacheDir(), "module.zip");
|
||||
super.onCreate(savedInstanceState);
|
||||
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
|
||||
Intent intent = this.getIntent();
|
||||
Uri uri;
|
||||
if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) {
|
||||
Timber.w("Impersonation detected");
|
||||
this.forceBackPressed();
|
||||
return;
|
||||
}
|
||||
String url = uri.toString();
|
||||
if (!AndroidacyUtil.isAndroidacyLink(url, uri)) {
|
||||
Timber.w("Calling non androidacy link in secure WebView: %s", url);
|
||||
this.forceBackPressed();
|
||||
return;
|
||||
}
|
||||
if (!Http.hasWebView()) {
|
||||
Timber.w("No WebView found to load url: %s", url);
|
||||
this.forceBackPressed();
|
||||
return;
|
||||
}
|
||||
// if action bar is shown, hide it
|
||||
this.hideActionBar();
|
||||
Http.markCaptchaAndroidacySolved();
|
||||
if (!url.contains(AndroidacyUtil.REFERRER)) {
|
||||
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
|
||||
url = url + '&' + AndroidacyUtil.REFERRER;
|
||||
} else {
|
||||
url = url + '?' + AndroidacyUtil.REFERRER;
|
||||
}
|
||||
}
|
||||
// Add token to url if not present
|
||||
String token = uri.getQueryParameter("token");
|
||||
if (token == null) {
|
||||
// get from shared preferences
|
||||
url = url + "&token=" + AndroidacyRepoData.token;
|
||||
}
|
||||
// Add device_id to url if not present
|
||||
String device_id = uri.getQueryParameter("device_id");
|
||||
if (device_id == null) {
|
||||
// get from shared preferences
|
||||
device_id = AndroidacyRepoData.generateDeviceId();
|
||||
url = url + "&device_id=" + device_id;
|
||||
}
|
||||
// check if client_id is present
|
||||
String client_id = uri.getQueryParameter("client_id");
|
||||
if (client_id == null) {
|
||||
// get from shared preferences
|
||||
client_id = BuildConfig.ANDROIDACY_CLIENT_ID;
|
||||
url = url + "&client_id=" + client_id;
|
||||
}
|
||||
boolean allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false);
|
||||
String title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE);
|
||||
String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG);
|
||||
int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0);
|
||||
this.setContentView(R.layout.webview);
|
||||
setActionBarBackground(null);
|
||||
this.setDisplayHomeAsUpEnabled(true);
|
||||
if (title == null || title.isEmpty()) {
|
||||
this.setTitle("Androidacy");
|
||||
} else {
|
||||
this.setTitle(title);
|
||||
}
|
||||
if (allowInstall || title == null || title.isEmpty()) {
|
||||
this.hideActionBar();
|
||||
} else { // Only used for note section
|
||||
if (config != null && !config.isEmpty()) {
|
||||
String configPkg = IntentHelper.getPackageOfConfig(config);
|
||||
try {
|
||||
XHooks.checkConfigTargetExists(this, configPkg, config);
|
||||
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
|
||||
IntentHelper.openConfig(this, config);
|
||||
return true;
|
||||
});
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.progressIndicator = this.findViewById(R.id.progress_bar);
|
||||
this.progressIndicator.setMax(100);
|
||||
this.webView = this.findViewById(R.id.webView);
|
||||
WebSettings webSettings = this.webView.getSettings();
|
||||
webSettings.setUserAgentString(Http.getAndroidacyUA());
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
cookieManager.setAcceptCookie(true);
|
||||
cookieManager.setAcceptThirdPartyCookies(this.webView, true);
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
|
||||
webSettings.setAllowFileAccess(false);
|
||||
webSettings.setAllowContentAccess(false);
|
||||
webSettings.setAllowFileAccessFromFileURLs(false);
|
||||
webSettings.setAllowUniversalAccessFromFileURLs(false);
|
||||
webSettings.setMediaPlaybackRequiresUserGesture(false);
|
||||
// enable webview debugging on debug builds
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
// if app is in dark mode, force dark mode on webview
|
||||
if (MainApplication.getINSTANCE().isDarkTheme()) {
|
||||
// for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
|
||||
WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings, true);
|
||||
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||
WebSettingsCompat.setForceDark(webSettings, WebSettingsCompat.FORCE_DARK_ON);
|
||||
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
|
||||
WebSettingsCompat.setForceDarkStrategy(webSettings, WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY);
|
||||
}
|
||||
}
|
||||
// Attempt at fixing CloudFlare captcha.
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) {
|
||||
Set<String> allowList = new HashSet<>();
|
||||
allowList.add("https://*.androidacy.com");
|
||||
WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings, allowList);
|
||||
}
|
||||
// get swipe to refresh layout
|
||||
SwipeRefreshLayout swipeRefreshLayout = this.findViewById(R.id.swipe_refresh_layout);
|
||||
|
||||
this.webView.setWebViewClient(new WebViewClientCompat() {
|
||||
private String pageUrl;
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) {
|
||||
// Don't open non Androidacy urls inside WebView
|
||||
if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
|
||||
if (downloadMode || backOnResume) return true;
|
||||
// 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) {
|
||||
// Block request as Androidacy doesn't allow duplicate requests
|
||||
return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
this.pageUrl = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
progressIndicator.setVisibility(View.INVISIBLE);
|
||||
progressIndicator.setProgressCompat(0, false);
|
||||
}
|
||||
|
||||
private void onReceivedError(String url, int errorCode) {
|
||||
if ((url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith("https://staging-api.androidacy.com/magisk/") || url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) {
|
||||
Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show();
|
||||
AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed);
|
||||
} else if (url.equals(this.pageUrl)) {
|
||||
postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||
this.onReceivedError(failingUrl, errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
|
||||
this.onReceivedError(request.getUrl().toString(), error.getErrorCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
|
||||
super.onReceivedSslError(view, handler, error);
|
||||
// log the error and url of its request
|
||||
Timber.tag("JSLog").e(error.toString());
|
||||
}
|
||||
});
|
||||
// logic for swipe to refresh
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
// reload page
|
||||
webView.reload();
|
||||
});
|
||||
this.webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
|
||||
FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data)));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||
if (BuildConfig.DEBUG_HTTP) {
|
||||
switch (consoleMessage.messageLevel()) {
|
||||
case TIP -> Timber.tag("JSLog").i(consoleMessage.message());
|
||||
case LOG -> Timber.tag("JSLog").d(consoleMessage.message());
|
||||
case WARNING -> Timber.tag("JSLog").w(consoleMessage.message());
|
||||
case ERROR -> Timber.tag("JSLog").e(consoleMessage.message());
|
||||
default -> Timber.tag("JSLog").v(consoleMessage.message());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(WebView view, int newProgress) {
|
||||
if (downloadMode) return;
|
||||
if (newProgress != 100 && progressIndicator.getVisibility() != View.VISIBLE) {
|
||||
Timber.i("Progress: %d, showing progress bar", newProgress);
|
||||
progressIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// if progress is greater than one, set indeterminate to false
|
||||
if (newProgress > 1) {
|
||||
Timber.i("Progress: %d, setting indeterminate to false", newProgress);
|
||||
progressIndicator.setIndeterminate(false);
|
||||
}
|
||||
progressIndicator.setProgressCompat(newProgress, true);
|
||||
if (newProgress == 100 && progressIndicator.getVisibility() != View.INVISIBLE) {
|
||||
Timber.i("Progress: %d, hiding progress bar", newProgress);
|
||||
progressIndicator.setIndeterminate(true);
|
||||
progressIndicator.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
|
||||
if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return;
|
||||
if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) {
|
||||
AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
|
||||
if (androidacyWebAPI != null) {
|
||||
if (!androidacyWebAPI.downloadMode) {
|
||||
// Native module popup may cause download after consumed action
|
||||
if (androidacyWebAPI.consumedAction) return;
|
||||
// Workaround Androidacy bug
|
||||
final String moduleId = moduleIdOfUrl(downloadUrl);
|
||||
if (this.megaIntercept(webView.getUrl(), downloadUrl)) {
|
||||
// Block request as Androidacy doesn't allow duplicate requests
|
||||
return;
|
||||
} else if (moduleId != null) {
|
||||
// Download module
|
||||
Timber.i("megaIntercept failure. Forcing onBackPress");
|
||||
this.onBackPressed();
|
||||
}
|
||||
}
|
||||
androidacyWebAPI.consumedAction = true;
|
||||
androidacyWebAPI.downloadMode = false;
|
||||
}
|
||||
this.backOnResume = true;
|
||||
Timber.i("Exiting WebView %s", AndroidacyUtil.hideToken(downloadUrl));
|
||||
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) {
|
||||
if (downloadUrl.startsWith(prefix)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
IntentHelper.openCustomTab(this, downloadUrl);
|
||||
}
|
||||
});
|
||||
this.androidacyWebAPI = new AndroidacyWebAPI(this, allowInstall);
|
||||
XHooks.onWebViewInitialize(this.webView, allowInstall);
|
||||
this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm");
|
||||
if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel);
|
||||
HashMap<String, String> headers = new HashMap<>();
|
||||
headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag());
|
||||
// set layout to view
|
||||
this.webView.loadUrl(url, headers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (this.backOnResume) {
|
||||
this.backOnResume = false;
|
||||
this.forceBackPressed();
|
||||
} else if (this.androidacyWebAPI != null) {
|
||||
this.androidacyWebAPI.consumedAction = false;
|
||||
}
|
||||
}
|
||||
|
||||
private String moduleIdOfUrl(String url) {
|
||||
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/readme/", "https://staging-api.androidacy.com/magisk/readme/", "https://prodiuction-api.androidacy.com/magisk/info/", "https://staging-api.androidacy.com/magisk/info/"}) { // Make both staging and non staging act the same
|
||||
int i = url.indexOf('?', prefix.length());
|
||||
if (i == -1) i = url.length();
|
||||
if (url.startsWith(prefix)) return url.substring(prefix.length(), i);
|
||||
}
|
||||
if (this.isFileUrl(url)) {
|
||||
int i = url.indexOf("&module=");
|
||||
if (i != -1) {
|
||||
int j = url.indexOf('&', i + 1);
|
||||
if (j == -1) {
|
||||
return url.substring(i + 8);
|
||||
} else {
|
||||
return url.substring(i + 8, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isFileUrl(String url) {
|
||||
if (url == null) return false;
|
||||
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same
|
||||
if (url.startsWith(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDownloadUrl(String url) {
|
||||
for (String prefix : new String[]{"https://production-api.androidacy.com/magisk/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { // Make both staging and non staging act the same
|
||||
if (url.startsWith(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean megaIntercept(String pageUrl, String fileUrl) {
|
||||
if (pageUrl == null || fileUrl == null) return false;
|
||||
// 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) {
|
||||
Timber.i("No module id?");
|
||||
// Re-open the page
|
||||
this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis());
|
||||
}
|
||||
String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl);
|
||||
String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl);
|
||||
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall());
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri downloadFileAsync(String url) throws IOException {
|
||||
this.downloadMode = true;
|
||||
this.runOnUiThread(() -> {
|
||||
progressIndicator.setIndeterminate(false);
|
||||
progressIndicator.setVisibility(View.VISIBLE);
|
||||
});
|
||||
byte[] module;
|
||||
try {
|
||||
module = Http.doHttpGet(url, (downloaded, total, done) -> progressIndicator.setProgressCompat((downloaded * 100) / total, true));
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) {
|
||||
fileOutputStream.write(module);
|
||||
}
|
||||
} finally {
|
||||
//noinspection UnusedAssignment
|
||||
module = null;
|
||||
this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE));
|
||||
}
|
||||
this.backOnResume = true;
|
||||
this.downloadMode = false;
|
||||
return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (webView != null) {
|
||||
SwipeRefreshLayout parent = (SwipeRefreshLayout) webView.getParent();
|
||||
parent.removeView(webView);
|
||||
webView.removeAllViews();
|
||||
webView.destroy(); // fix memory leak
|
||||
}
|
||||
Timber.i("onDestroy for %s", this);
|
||||
}
|
||||
}
|
@ -0,0 +1,500 @@
|
||||
@file:Suppress("ktConcatNullable")
|
||||
|
||||
package com.fox2code.mmm.androidacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.ConsoleMessage.MessageLevel
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.DownloadListener
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import androidx.webkit.WebResourceErrorCompat
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import com.fox2code.foxcompat.app.FoxActivity
|
||||
import com.fox2code.mmm.BuildConfig
|
||||
import com.fox2code.mmm.Constants
|
||||
import com.fox2code.mmm.MainApplication
|
||||
import com.fox2code.mmm.R
|
||||
import com.fox2code.mmm.XHooks.Companion.checkConfigTargetExists
|
||||
import com.fox2code.mmm.XHooks.Companion.onWebViewInitialize
|
||||
import com.fox2code.mmm.utils.IntentHelper
|
||||
import com.fox2code.mmm.utils.io.net.Http
|
||||
import com.fox2code.mmm.utils.io.net.Http.Companion.androidacyUA
|
||||
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
|
||||
import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
|
||||
import com.fox2code.mmm.utils.io.net.Http.Companion.markCaptchaAndroidacySolved
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import org.matomo.sdk.extra.TrackHelper
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Per Androidacy repo implementation agreement, no request of this WebView shall be modified.
|
||||
*/
|
||||
class AndroidacyActivity : FoxActivity() {
|
||||
private var moduleFile: File? = null
|
||||
@JvmField
|
||||
var webView: WebView? = null
|
||||
var webViewNote: TextView? = null
|
||||
private var androidacyWebAPI: AndroidacyWebAPI? = null
|
||||
var progressIndicator: LinearProgressIndicator? = null
|
||||
@JvmField
|
||||
var backOnResume = false
|
||||
var downloadMode = false
|
||||
@SuppressLint(
|
||||
"SetJavaScriptEnabled",
|
||||
"JavascriptInterface",
|
||||
"RestrictedApi",
|
||||
"ClickableViewAccessibility"
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
moduleFile = File(this.cacheDir, "module.zip")
|
||||
super.onCreate(savedInstanceState)
|
||||
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().tracker)
|
||||
val intent = this.intent
|
||||
var uri: Uri? = intent.data
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (!MainApplication.checkSecret(intent) || intent.data.also { uri = it!! } == null) {
|
||||
Timber.w("Impersonation detected")
|
||||
forceBackPressed()
|
||||
return
|
||||
}
|
||||
var url = uri.toString()
|
||||
if (!AndroidacyUtil.isAndroidacyLink(url, uri!!)) {
|
||||
Timber.w("Calling non androidacy link in secure WebView: %s", url)
|
||||
forceBackPressed()
|
||||
return
|
||||
}
|
||||
if (!hasWebView()) {
|
||||
Timber.w("No WebView found to load url: %s", url)
|
||||
forceBackPressed()
|
||||
return
|
||||
}
|
||||
// if action bar is shown, hide it
|
||||
hideActionBar()
|
||||
markCaptchaAndroidacySolved()
|
||||
if (!url.contains(AndroidacyUtil.REFERRER)) {
|
||||
url = if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
|
||||
url + '&' + AndroidacyUtil.REFERRER
|
||||
} else {
|
||||
url + '?' + AndroidacyUtil.REFERRER
|
||||
}
|
||||
}
|
||||
// Add token to url if not present
|
||||
val token = uri!!.getQueryParameter("token")
|
||||
if (token == null) {
|
||||
// get from shared preferences
|
||||
url = url + "&token=" + AndroidacyRepoData.token
|
||||
}
|
||||
// Add device_id to url if not present
|
||||
var deviceId = uri!!.getQueryParameter("device_id")
|
||||
if (deviceId == null) {
|
||||
// get from shared preferences
|
||||
deviceId = AndroidacyRepoData.generateDeviceId()
|
||||
url = "$url&device_id=$deviceId"
|
||||
}
|
||||
// check if client_id is present
|
||||
var clientId = uri!!.getQueryParameter("client_id")
|
||||
if (clientId == null) {
|
||||
// get from shared preferences
|
||||
clientId = BuildConfig.ANDROIDACY_CLIENT_ID
|
||||
url = "$url&client_id=$clientId"
|
||||
}
|
||||
val allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false)
|
||||
var title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE)
|
||||
val config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG)
|
||||
val compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0)
|
||||
this.setContentView(R.layout.webview)
|
||||
setActionBarBackground(null)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
if (title.isNullOrEmpty()) {
|
||||
title = "Androidacy"
|
||||
}
|
||||
if (allowInstall || title.isEmpty()) {
|
||||
hideActionBar()
|
||||
} else { // Only used for note section
|
||||
if (!config.isNullOrEmpty()) {
|
||||
val configPkg = IntentHelper.getPackageOfConfig(config)
|
||||
try {
|
||||
checkConfigTargetExists(this, configPkg, config)
|
||||
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24) { _: MenuItem? ->
|
||||
IntentHelper.openConfig(this, config)
|
||||
true
|
||||
}
|
||||
} catch (ignored: PackageManager.NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
val prgInd = findViewById<LinearProgressIndicator>(R.id.progress_bar)
|
||||
prgInd.max = 100
|
||||
webView = findViewById(R.id.webView)
|
||||
val wbv = webView
|
||||
val webSettings = wbv?.settings
|
||||
webSettings?.userAgentString = androidacyUA
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
cookieManager.setAcceptCookie(true)
|
||||
cookieManager.setAcceptThirdPartyCookies(webView, true)
|
||||
webSettings?.domStorageEnabled = true
|
||||
webSettings?.javaScriptEnabled = true
|
||||
webSettings?.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
webSettings?.allowFileAccess = false
|
||||
webSettings?.allowContentAccess = false
|
||||
webSettings?.mediaPlaybackRequiresUserGesture = false
|
||||
// enable webview debugging on debug builds
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
// if app is in dark mode, force dark mode on webview
|
||||
if (MainApplication.getINSTANCE().isDarkTheme) {
|
||||
// for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
|
||||
WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings!!, true)
|
||||
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||
@Suppress("DEPRECATION")
|
||||
WebSettingsCompat.setForceDark(webSettings!!, WebSettingsCompat.FORCE_DARK_ON)
|
||||
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
|
||||
@Suppress("DEPRECATION")
|
||||
WebSettingsCompat.setForceDarkStrategy(
|
||||
webSettings!!,
|
||||
WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY
|
||||
)
|
||||
}
|
||||
}
|
||||
// Attempt at fixing CloudFlare captcha.
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) {
|
||||
val allowList: MutableSet<String> = HashSet()
|
||||
allowList.add("https://*.androidacy.com")
|
||||
WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings!!, allowList)
|
||||
}
|
||||
// get swipe to refresh layout
|
||||
val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swipe_refresh_layout)
|
||||
wbv?.webViewClient = object : WebViewClientCompat() {
|
||||
private var pageUrl: String? = null
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
// Don't open non Androidacy urls inside WebView
|
||||
if (request.isForMainFrame && !AndroidacyUtil.isAndroidacyLink(request.url)) {
|
||||
if (downloadMode || backOnResume) return true
|
||||
// sanitize url
|
||||
@Suppress("NAME_SHADOWING") var url = request.url.toString()
|
||||
url = AndroidacyUtil.hideToken(url).toString()
|
||||
Timber.i("Exiting WebView %s", url)
|
||||
IntentHelper.openUri(view.context, request.url.toString())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return if (megaIntercept(pageUrl, request.url.toString())) {
|
||||
// Block request as Androidacy doesn't allow duplicate requests
|
||||
WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream(ByteArray(0)))
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
pageUrl = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
progressIndicator?.visibility = View.INVISIBLE
|
||||
progressIndicator?.setProgressCompat(0, false)
|
||||
}
|
||||
|
||||
private fun onReceivedError(url: String, errorCode: Int) {
|
||||
if (url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith(
|
||||
"https://staging-api.androidacy.com/magisk/"
|
||||
) || url == pageUrl && errorCode == 419 || errorCode == 429 || errorCode == 503
|
||||
) {
|
||||
Toast.makeText(this@AndroidacyActivity, "Too many requests!", Toast.LENGTH_LONG)
|
||||
.show()
|
||||
runOnUiThread { forceBackPressed() }
|
||||
} else if (url == pageUrl) {
|
||||
postOnUiThread { webViewNote!!.visibility = View.VISIBLE }
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String,
|
||||
failingUrl: String
|
||||
) {
|
||||
this.onReceivedError(failingUrl, errorCode)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceErrorCompat
|
||||
) {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
|
||||
this.onReceivedError(request.url.toString(), error.errorCode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView,
|
||||
handler: SslErrorHandler,
|
||||
error: SslError
|
||||
) {
|
||||
super.onReceivedSslError(view, handler, error)
|
||||
// log the error and url of its request
|
||||
Timber.tag("JSLog").e(error.toString())
|
||||
}
|
||||
}
|
||||
// logic for swipe to refresh
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
// reload page
|
||||
wbv?.reload()
|
||||
}
|
||||
wbv?.webChromeClient = object : WebChromeClient() {
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView,
|
||||
filePathCallback: ValueCallback<Array<Uri>>,
|
||||
fileChooserParams: FileChooserParams
|
||||
): Boolean {
|
||||
getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent()) { code: Int, data: Intent? ->
|
||||
filePathCallback.onReceiveValue(
|
||||
FileChooserParams.parseResult(code, data)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
if (BuildConfig.DEBUG_HTTP) {
|
||||
when (consoleMessage.messageLevel()) {
|
||||
MessageLevel.TIP -> Timber.tag("JSLog").i(consoleMessage.message())
|
||||
MessageLevel.LOG -> Timber.tag("JSLog").d(consoleMessage.message())
|
||||
MessageLevel.WARNING -> Timber.tag("JSLog").w(consoleMessage.message())
|
||||
MessageLevel.ERROR -> Timber.tag("JSLog").e(consoleMessage.message())
|
||||
else -> Timber.tag("JSLog").v(consoleMessage.message())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
if (downloadMode) return
|
||||
if (newProgress != 100 && prgInd.visibility != View.VISIBLE) {
|
||||
Timber.i("Progress: %d, showing progress bar", newProgress)
|
||||
prgInd.visibility = View.VISIBLE
|
||||
}
|
||||
// if progress is greater than one, set indeterminate to false
|
||||
if (newProgress > 1) {
|
||||
Timber.i("Progress: %d, setting indeterminate to false", newProgress)
|
||||
prgInd.isIndeterminate = false
|
||||
}
|
||||
prgInd.setProgressCompat(newProgress, true)
|
||||
if (newProgress == 100 && prgInd.visibility != View.INVISIBLE) {
|
||||
Timber.i("Progress: %d, hiding progress bar", newProgress)
|
||||
prgInd.isIndeterminate = true
|
||||
prgInd.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
wbv?.setDownloadListener(DownloadListener setDownloadListener@{ downloadUrl: String, _: String?, _: String?, _: String?, _: Long ->
|
||||
if (downloadMode || isDownloadUrl(downloadUrl)) return@setDownloadListener
|
||||
if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !backOnResume) {
|
||||
val androidacyWebAPI = androidacyWebAPI
|
||||
if (androidacyWebAPI != null) {
|
||||
if (!androidacyWebAPI.downloadMode) {
|
||||
// Native module popup may cause download after consumed action
|
||||
if (androidacyWebAPI.consumedAction) return@setDownloadListener
|
||||
// Workaround Androidacy bug
|
||||
val moduleId = moduleIdOfUrl(downloadUrl)
|
||||
if (megaIntercept(wbv.url, downloadUrl)) {
|
||||
// Block request as Androidacy doesn't allow duplicate requests
|
||||
return@setDownloadListener
|
||||
} else if (moduleId != null) {
|
||||
// Download module
|
||||
Timber.i("megaIntercept failure. Forcing onBackPress")
|
||||
forceBackPressed()
|
||||
}
|
||||
}
|
||||
androidacyWebAPI.consumedAction = true
|
||||
androidacyWebAPI.downloadMode = false
|
||||
}
|
||||
backOnResume = true
|
||||
Timber.i("Exiting WebView %s", AndroidacyUtil.hideToken(downloadUrl))
|
||||
for (prefix in arrayOf<String>(
|
||||
"https://production-api.androidacy.com/downloads/",
|
||||
"https://staging-api.androidacy.com/magisk/downloads/"
|
||||
)) {
|
||||
if (downloadUrl.startsWith(prefix)) {
|
||||
return@setDownloadListener
|
||||
}
|
||||
}
|
||||
IntentHelper.openCustomTab(this, downloadUrl)
|
||||
}
|
||||
})
|
||||
androidacyWebAPI = AndroidacyWebAPI(this, allowInstall)
|
||||
onWebViewInitialize(webView, allowInstall)
|
||||
wbv?.addJavascriptInterface(androidacyWebAPI!!, "mmm")
|
||||
if (compatLevel != 0) androidacyWebAPI!!.notifyCompatModeRaw(compatLevel)
|
||||
val headers = HashMap<String, String>()
|
||||
headers["Accept-Language"] = this.resources.configuration.locales.get(0).language
|
||||
// set layout to view
|
||||
wbv?.loadUrl(url, headers)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (backOnResume) {
|
||||
backOnResume = false
|
||||
forceBackPressed()
|
||||
} else if (androidacyWebAPI != null) {
|
||||
androidacyWebAPI!!.consumedAction = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun moduleIdOfUrl(url: String): String? {
|
||||
for (prefix in arrayOf(
|
||||
"https://production-api.androidacy.com/downloads/",
|
||||
"https://staging-api.androidacy.com/downloads/",
|
||||
"https://production-api.androidacy.com/magisk/readme/",
|
||||
"https://staging-api.androidacy.com/magisk/readme/",
|
||||
"https://prodiuction-api.androidacy.com/magisk/info/",
|
||||
"https://staging-api.androidacy.com/magisk/info/"
|
||||
)) { // Make both staging and non staging act the same
|
||||
var i = url.indexOf('?', prefix.length)
|
||||
if (i == -1) i = url.length
|
||||
if (url.startsWith(prefix)) return url.substring(prefix.length, i)
|
||||
}
|
||||
if (isFileUrl(url)) {
|
||||
val i = url.indexOf("&module=")
|
||||
if (i != -1) {
|
||||
val j = url.indexOf('&', i + 1)
|
||||
return if (j == -1) {
|
||||
url.substring(i + 8)
|
||||
} else {
|
||||
url.substring(i + 8, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isFileUrl(url: String?): Boolean {
|
||||
if (url == null) return false
|
||||
for (prefix in arrayOf(
|
||||
"https://production-api.androidacy.com/downloads/",
|
||||
"https://staging-api.androidacy.com/downloads/"
|
||||
)) { // Make both staging and non staging act the same
|
||||
if (url.startsWith(prefix)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isDownloadUrl(url: String): Boolean {
|
||||
for (prefix in arrayOf(
|
||||
"https://production-api.androidacy.com/magisk/downloads/",
|
||||
"https://staging-api.androidacy.com/magisk/downloads/"
|
||||
)) { // Make both staging and non staging act the same
|
||||
if (url.startsWith(prefix)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun megaIntercept(pageUrl: String?, fileUrl: String?): Boolean {
|
||||
if (pageUrl == null || fileUrl == null) return false
|
||||
// ensure neither pageUrl nor fileUrl are going to cause a crash
|
||||
if (pageUrl.contains(" ") || fileUrl.contains(" ")) return false
|
||||
if (!isFileUrl(fileUrl)) {
|
||||
return false
|
||||
}
|
||||
val androidacyWebAPI = androidacyWebAPI
|
||||
val moduleId = AndroidacyUtil.getModuleId(fileUrl)
|
||||
if (moduleId == null) {
|
||||
Timber.i("No module id?")
|
||||
// Re-open the page
|
||||
webView!!.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis())
|
||||
}
|
||||
val checksum = AndroidacyUtil.getChecksumFromURL(fileUrl)
|
||||
val moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl)
|
||||
androidacyWebAPI!!.openNativeModuleDialogRaw(
|
||||
fileUrl,
|
||||
moduleId,
|
||||
moduleTitle,
|
||||
checksum,
|
||||
androidacyWebAPI.canInstall()
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun downloadFileAsync(url: String?): Uri {
|
||||
downloadMode = true
|
||||
runOnUiThread {
|
||||
progressIndicator!!.isIndeterminate = false
|
||||
progressIndicator!!.visibility = View.VISIBLE
|
||||
}
|
||||
var module: ByteArray?
|
||||
try {
|
||||
module = doHttpGet(
|
||||
url!!, ({ downloaded: Int, total: Int, _: Boolean ->
|
||||
progressIndicator!!.setProgressCompat(
|
||||
downloaded * 100 / total, true)
|
||||
|
||||
} as Http.ProgressListener?)!!)
|
||||
FileOutputStream(moduleFile).use { fileOutputStream -> fileOutputStream.write(module) }
|
||||
} finally {
|
||||
module = null
|
||||
runOnUiThread { progressIndicator!!.visibility = View.INVISIBLE }
|
||||
}
|
||||
backOnResume = true
|
||||
downloadMode = false
|
||||
@Suppress("ktConcatNullable")
|
||||
return FileProvider.getUriForFile(this, this.packageName + ".file-provider", moduleFile!!)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (webView != null) {
|
||||
val parent = webView!!.parent as SwipeRefreshLayout
|
||||
parent.removeView(webView)
|
||||
webView!!.removeAllViews()
|
||||
webView!!.destroy() // fix memory leak
|
||||
}
|
||||
Timber.i("onDestroy for %s", this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package com.fox2code.mmm.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
public class LongClickablePreference extends Preference {
|
||||
private OnPreferenceLongClickListener onPreferenceLongClickListener;
|
||||
|
||||
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public LongClickablePreference(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
holder.itemView.setOnLongClickListener(v -> performLongClick());
|
||||
}
|
||||
|
||||
private boolean performLongClick() {
|
||||
if (!this.isEnabled() || !this.isSelectable()) {
|
||||
return false;
|
||||
}
|
||||
if (this.onPreferenceLongClickListener != null) {
|
||||
return this.onPreferenceLongClickListener.onPreferenceLongClick(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setOnPreferenceLongClickListener(OnPreferenceLongClickListener onPreferenceLongClickListener) {
|
||||
this.onPreferenceLongClickListener = onPreferenceLongClickListener;
|
||||
}
|
||||
|
||||
public OnPreferenceLongClickListener getOnPreferenceLongClickListener() {
|
||||
return this.onPreferenceLongClickListener;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OnPreferenceLongClickListener {
|
||||
/**
|
||||
* Called when a preference has been clicked.
|
||||
*
|
||||
* @param preference The preference that was clicked
|
||||
* @return {@code true} if the click was handled
|
||||
*/
|
||||
boolean onPreferenceLongClick(@NonNull Preference preference);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.fox2code.mmm.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
|
||||
@Suppress("unused")
|
||||
class LongClickablePreference : Preference {
|
||||
var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
holder.itemView.setOnLongClickListener { _: View? -> performLongClick() }
|
||||
}
|
||||
|
||||
private fun performLongClick(): Boolean {
|
||||
if (!this.isEnabled || !this.isSelectable) {
|
||||
return false
|
||||
}
|
||||
return if (onPreferenceLongClickListener != null) {
|
||||
onPreferenceLongClickListener!!.onPreferenceLongClick(this)
|
||||
} else false
|
||||
}
|
||||
|
||||
fun interface OnPreferenceLongClickListener {
|
||||
/**
|
||||
* Called when a preference has been clicked.
|
||||
*
|
||||
* @param preference The preference that was clicked
|
||||
* @return `true` if the click was handled
|
||||
*/
|
||||
fun onPreferenceLongClick(preference: Preference): Boolean
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package com.fox2code.mmm.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SharedPreferenceDataStore extends PreferenceDataStore {
|
||||
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
|
||||
public SharedPreferenceDataStore(@NonNull SharedPreferences sharedPreferences) {
|
||||
Timber.d("SharedPreferenceDataStore: %s", sharedPreferences);
|
||||
mSharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
Timber.d("getSharedPreferences: %s", mSharedPreferences);
|
||||
return mSharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
mSharedPreferences.edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putStringSet(String key, @Nullable Set<String> values) {
|
||||
mSharedPreferences.edit().putStringSet(key, values).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putInt(String key, int value) {
|
||||
mSharedPreferences.edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putLong(String key, long value) {
|
||||
mSharedPreferences.edit().putLong(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putFloat(String key, float value) {
|
||||
mSharedPreferences.edit().putFloat(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
mSharedPreferences.edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
return mSharedPreferences.getString(key, defValue);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
|
||||
return mSharedPreferences.getStringSet(key, defValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
return mSharedPreferences.getInt(key, defValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
return mSharedPreferences.getLong(key, defValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
return mSharedPreferences.getFloat(key, defValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
return mSharedPreferences.getBoolean(key, defValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.fox2code.mmm.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import timber.log.Timber
|
||||
|
||||
class SharedPreferenceDataStore(sharedPreferences: SharedPreferences) : PreferenceDataStore() {
|
||||
private val mSharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
Timber.d("SharedPreferenceDataStore: %s", sharedPreferences)
|
||||
mSharedPreferences = sharedPreferences
|
||||
}
|
||||
|
||||
val sharedPreferences: SharedPreferences
|
||||
get() {
|
||||
Timber.d("getSharedPreferences: %s", mSharedPreferences)
|
||||
return mSharedPreferences
|
||||
}
|
||||
|
||||
override fun putString(key: String, value: String?) {
|
||||
mSharedPreferences.edit().putString(key, value).apply()
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String, values: Set<String>?) {
|
||||
mSharedPreferences.edit().putStringSet(key, values).apply()
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int) {
|
||||
mSharedPreferences.edit().putInt(key, value).apply()
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) {
|
||||
mSharedPreferences.edit().putLong(key, value).apply()
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float) {
|
||||
mSharedPreferences.edit().putFloat(key, value).apply()
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) {
|
||||
mSharedPreferences.edit().putBoolean(key, value).apply()
|
||||
}
|
||||
|
||||
override fun getString(key: String, defValue: String?): String? {
|
||||
return mSharedPreferences.getString(key, defValue)
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? {
|
||||
return mSharedPreferences.getStringSet(key, defValues)
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defValue: Int): Int {
|
||||
return mSharedPreferences.getInt(key, defValue)
|
||||
}
|
||||
|
||||
override fun getLong(key: String, defValue: Long): Long {
|
||||
return mSharedPreferences.getLong(key, defValue)
|
||||
}
|
||||
|
||||
override fun getFloat(key: String, defValue: Float): Float {
|
||||
return mSharedPreferences.getFloat(key, defValue)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean {
|
||||
return mSharedPreferences.getBoolean(key, defValue)
|
||||
}
|
||||
}
|
@ -1,47 +1,65 @@
|
||||
package com.fox2code.mmm.utils;
|
||||
package com.fox2code.mmm.utils
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// ProgressDialog is deprecated because it's an bad UX pattern, but sometimes we have no other choice...
|
||||
public enum BudgetProgressDialog {
|
||||
enum class BudgetProgressDialog {
|
||||
;
|
||||
|
||||
public static AlertDialog build(Context context, String title, String message) {
|
||||
Resources r = context.getResources();
|
||||
int padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, r.getDisplayMetrics()));
|
||||
LinearLayoutCompat v = new LinearLayoutCompat(context);
|
||||
v.setOrientation(LinearLayoutCompat.HORIZONTAL);
|
||||
ProgressBar pb = new ProgressBar(context);
|
||||
v.addView(pb, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||
TextView t = new TextView(context);
|
||||
t.setGravity(Gravity.CENTER);
|
||||
v.addView(t, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 4));
|
||||
v.setPadding(padding, padding, padding, padding);
|
||||
|
||||
t.setText(message);
|
||||
return new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setView(v)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
}
|
||||
companion object {
|
||||
fun build(context: Context, title: String?, message: String?): AlertDialog {
|
||||
val r = context.resources
|
||||
val padding = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
20f,
|
||||
r.displayMetrics
|
||||
).roundToInt()
|
||||
val v = LinearLayoutCompat(context)
|
||||
v.orientation = LinearLayoutCompat.HORIZONTAL
|
||||
val pb = ProgressBar(context)
|
||||
v.addView(
|
||||
pb,
|
||||
LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
)
|
||||
)
|
||||
val t = TextView(context)
|
||||
t.gravity = Gravity.CENTER
|
||||
v.addView(
|
||||
t,
|
||||
LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
4f
|
||||
)
|
||||
)
|
||||
v.setPadding(padding, padding, padding, padding)
|
||||
t.text = message
|
||||
return MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setView(v)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
}
|
||||
|
||||
public static AlertDialog build(Context context, int title, String message) {
|
||||
return build(context, context.getString(title), message);
|
||||
}
|
||||
fun build(context: Context, title: Int, message: String?): AlertDialog {
|
||||
return build(context, context.getString(title), message)
|
||||
}
|
||||
|
||||
public static AlertDialog build(Context context, int title, int message) {
|
||||
return build(context, title, context.getString(message));
|
||||
}
|
||||
}
|
||||
@JvmStatic
|
||||
fun build(context: Context, title: Int, message: Int): AlertDialog {
|
||||
return build(context, title, context.getString(message))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,115 +1,129 @@
|
||||
package com.fox2code.mmm.utils;
|
||||
package com.fox2code.mmm.utils
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.util.Supplier
|
||||
import com.fox2code.mmm.Constants
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import timber.log.Timber
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.util.Supplier;
|
||||
|
||||
import com.fox2code.mmm.Constants;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public final class ExternalHelper {
|
||||
public static final ExternalHelper INSTANCE = new ExternalHelper();
|
||||
private static final boolean TEST_MODE = false;
|
||||
private static final String FOX_MMM_OPEN_EXTERNAL = "com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL";
|
||||
private static final String FOX_MMM_EXTRA_REPO_ID = "extra_repo_id";
|
||||
private ComponentName fallback;
|
||||
private CharSequence label;
|
||||
private boolean multi;
|
||||
|
||||
private ExternalHelper() {
|
||||
}
|
||||
|
||||
public void refreshHelper(Context context) {
|
||||
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip"));
|
||||
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
|
||||
class ExternalHelper private constructor() {
|
||||
private var fallback: ComponentName? = null
|
||||
private var label: CharSequence? = null
|
||||
private var multi = false
|
||||
fun refreshHelper(context: Context) {
|
||||
val intent = Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip"))
|
||||
val resolveInfos =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(
|
||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()
|
||||
))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (resolveInfos.isEmpty()) {
|
||||
Timber.i("No external provider installed!");
|
||||
label = TEST_MODE ? "External" : null;
|
||||
multi = TEST_MODE;
|
||||
fallback = null;
|
||||
Timber.i("No external provider installed!")
|
||||
label = if (TEST_MODE) "External" else null
|
||||
multi = TEST_MODE
|
||||
fallback = null
|
||||
} else {
|
||||
ResolveInfo resolveInfo = resolveInfos.get(0);
|
||||
Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName);
|
||||
fallback = new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
|
||||
label = resolveInfo.loadLabel(context.getPackageManager());
|
||||
multi = resolveInfos.size() >= 2;
|
||||
val resolveInfo = resolveInfos[0]
|
||||
Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName)
|
||||
fallback =
|
||||
ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
|
||||
label = resolveInfo.loadLabel(context.packageManager)
|
||||
multi = resolveInfos.size >= 2
|
||||
}
|
||||
}
|
||||
|
||||
public boolean openExternal(Context context, Uri uri, String repoId) {
|
||||
if (label == null)
|
||||
return false;
|
||||
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
|
||||
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri);
|
||||
intent.setFlags(IntentHelper.FLAG_GRANT_URI_PERMISSION);
|
||||
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId);
|
||||
fun openExternal(context: Context, uri: Uri?, repoId: String?): Boolean {
|
||||
if (label == null) return false
|
||||
val param =
|
||||
ActivityOptionsCompat.makeCustomAnimation(context, rikka.core.R.anim.fade_in, rikka.core.R.anim.fade_out)
|
||||
.toBundle()
|
||||
var intent = Intent(FOX_MMM_OPEN_EXTERNAL, uri)
|
||||
intent.flags = IntentHelper.FLAG_GRANT_URI_PERMISSION
|
||||
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId)
|
||||
if (multi) {
|
||||
intent = Intent.createChooser(intent, label);
|
||||
intent = Intent.createChooser(intent, label)
|
||||
} else {
|
||||
intent.putExtra(Constants.EXTRA_FADE_OUT, true);
|
||||
intent.putExtra(Constants.EXTRA_FADE_OUT, true)
|
||||
}
|
||||
try {
|
||||
if (multi) {
|
||||
context.startActivity(intent);
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
context.startActivity(intent, param);
|
||||
context.startActivity(intent, param)
|
||||
}
|
||||
return true;
|
||||
} catch (
|
||||
ActivityNotFoundException e) {
|
||||
Timber.e(e);
|
||||
return true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
if (fallback != null) {
|
||||
if (multi) {
|
||||
intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri);
|
||||
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId);
|
||||
intent.putExtra(Constants.EXTRA_FADE_OUT, true);
|
||||
intent = Intent(FOX_MMM_OPEN_EXTERNAL, uri)
|
||||
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId)
|
||||
intent.putExtra(Constants.EXTRA_FADE_OUT, true)
|
||||
}
|
||||
intent.setComponent(fallback);
|
||||
intent.component = fallback
|
||||
try {
|
||||
context.startActivity(intent, param);
|
||||
return true;
|
||||
} catch (
|
||||
ActivityNotFoundException e) {
|
||||
Timber.e(e);
|
||||
context.startActivity(intent, param)
|
||||
return true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
public void injectButton(AlertDialog.Builder builder, Supplier<Uri> uriSupplier, String repoId) {
|
||||
if (label == null)
|
||||
return;
|
||||
builder.setNeutralButton(label, (dialog, button) -> {
|
||||
Context context = ((Dialog) dialog).getContext();
|
||||
new Thread("Async downloader") {
|
||||
@Override
|
||||
public void run() {
|
||||
final Uri uri = uriSupplier.get();
|
||||
if (uri == null)
|
||||
return;
|
||||
UiThreadHandler.run(() -> {
|
||||
if (!openExternal(context, uri, repoId)) {
|
||||
Toast.makeText(context, "Failed to launch external activity", Toast.LENGTH_SHORT).show();
|
||||
fun injectButton(builder: AlertDialog.Builder, uriSupplier: Supplier<Uri?>, repoId: String?) {
|
||||
if (label == null) return
|
||||
builder.setNeutralButton(label) { dialog: DialogInterface, _: Int ->
|
||||
val context = (dialog as Dialog).context
|
||||
object : Thread("Async downloader") {
|
||||
override fun run() {
|
||||
val uri = uriSupplier.get()
|
||||
if (uri != null) {
|
||||
UiThreadHandler.run {
|
||||
if (!openExternal(context, uri, repoId)) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Failed to launch external activity",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UiThreadHandler.run {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Failed to get download link",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
});
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val INSTANCE = ExternalHelper()
|
||||
private const val TEST_MODE = false
|
||||
private const val FOX_MMM_OPEN_EXTERNAL =
|
||||
"com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL"
|
||||
private const val FOX_MMM_EXTRA_REPO_ID = "extra_repo_id"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,12 @@
|
||||
package com.fox2code.mmm.utils;
|
||||
package com.fox2code.mmm.utils
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class FastException extends RuntimeException {
|
||||
public static final FastException INSTANCE = new FastException();
|
||||
|
||||
private FastException() {}
|
||||
class FastException private constructor() : RuntimeException() {
|
||||
@Synchronized
|
||||
override fun fillInStackTrace(): Throwable {
|
||||
return this
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
return this;
|
||||
companion object {
|
||||
val INSTANCE = FastException()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,395 +1,465 @@
|
||||
package com.fox2code.mmm.utils;
|
||||
@file:Suppress("ktConcatNullable")
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.Toast;
|
||||
package com.fox2code.mmm.utils
|
||||
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.app.BundleCompat;
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.TypedValue
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.BundleCompat
|
||||
import com.fox2code.foxcompat.app.FoxActivity
|
||||
import com.fox2code.mmm.BuildConfig
|
||||
import com.fox2code.mmm.Constants
|
||||
import com.fox2code.mmm.MainActivity
|
||||
import com.fox2code.mmm.MainApplication
|
||||
import com.fox2code.mmm.R
|
||||
import com.fox2code.mmm.XHooks.Companion.getConfigIntent
|
||||
import com.fox2code.mmm.XHooks.Companion.isModuleActive
|
||||
import com.fox2code.mmm.androidacy.AndroidacyActivity
|
||||
import com.fox2code.mmm.installer.InstallerActivity
|
||||
import com.fox2code.mmm.markdown.MarkdownActivity
|
||||
import com.fox2code.mmm.utils.io.Files.Companion.closeSilently
|
||||
import com.fox2code.mmm.utils.io.Files.Companion.copy
|
||||
import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URISyntaxException
|
||||
|
||||
import com.fox2code.foxcompat.app.FoxActivity;
|
||||
import com.fox2code.mmm.BuildConfig;
|
||||
import com.fox2code.mmm.Constants;
|
||||
import com.fox2code.mmm.MainApplication;
|
||||
import com.fox2code.mmm.R;
|
||||
import com.fox2code.mmm.XHooks;
|
||||
import com.fox2code.mmm.androidacy.AndroidacyActivity;
|
||||
import com.fox2code.mmm.installer.InstallerActivity;
|
||||
import com.fox2code.mmm.markdown.MarkdownActivity;
|
||||
import com.fox2code.mmm.utils.io.Files;
|
||||
import com.fox2code.mmm.utils.io.net.Http;
|
||||
import com.topjohnwu.superuser.CallbackList;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
enum class IntentHelper {;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URISyntaxException;
|
||||
companion object {
|
||||
|
||||
import timber.log.Timber;
|
||||
interface OnFileReceivedCallback {
|
||||
fun onReceived(target: File?, uri: Uri?, response: Int)
|
||||
}
|
||||
private const val EXTRA_TAB_SESSION = "android.support.customtabs.extra.SESSION"
|
||||
private const val EXTRA_TAB_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME"
|
||||
private const val EXTRA_TAB_COLOR_SCHEME_DARK = 2
|
||||
private const val EXTRA_TAB_COLOR_SCHEME_LIGHT = 1
|
||||
private const val EXTRA_TAB_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR"
|
||||
private const val EXTRA_TAB_EXIT_ANIMATION_BUNDLE =
|
||||
"android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE"
|
||||
const val FLAG_GRANT_URI_PERMISSION = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
public enum IntentHelper {
|
||||
;
|
||||
private static final String EXTRA_TAB_SESSION =
|
||||
"android.support.customtabs.extra.SESSION";
|
||||
private static final String EXTRA_TAB_COLOR_SCHEME =
|
||||
"androidx.browser.customtabs.extra.COLOR_SCHEME";
|
||||
private static final int EXTRA_TAB_COLOR_SCHEME_DARK = 2;
|
||||
private static final int EXTRA_TAB_COLOR_SCHEME_LIGHT = 1;
|
||||
private static final String EXTRA_TAB_TOOLBAR_COLOR =
|
||||
"android.support.customtabs.extra.TOOLBAR_COLOR";
|
||||
private static final String EXTRA_TAB_EXIT_ANIMATION_BUNDLE =
|
||||
"android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
|
||||
static final int FLAG_GRANT_URI_PERMISSION = Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
@JvmStatic
|
||||
fun openUri(context: Context, uri: String) {
|
||||
if (uri.startsWith("intent://")) {
|
||||
try {
|
||||
startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false)
|
||||
} catch (e: URISyntaxException) {
|
||||
Timber.e(e)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
} else openUrl(context, uri)
|
||||
}
|
||||
|
||||
public static void openUri(Context context, String uri) {
|
||||
if (uri.startsWith("intent://")) {
|
||||
@JvmOverloads
|
||||
fun openUrl(context: Context, url: String?, forceBrowser: Boolean = false) {
|
||||
Timber.d("Opening url: %s, forced browser %b", url, forceBrowser)
|
||||
try {
|
||||
startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false);
|
||||
} catch (URISyntaxException | ActivityNotFoundException e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
} else openUrl(context, uri);
|
||||
}
|
||||
|
||||
public static void openUrl(Context context, String url) {
|
||||
openUrl(context, url, false);
|
||||
}
|
||||
|
||||
public static void openUrl(Context context, String url, boolean forceBrowser) {
|
||||
Timber.d("Opening url: %s, forced browser %b", url, forceBrowser);
|
||||
try {
|
||||
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
myIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
|
||||
if (forceBrowser) {
|
||||
myIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
val myIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
myIntent.flags = FLAG_GRANT_URI_PERMISSION
|
||||
if (forceBrowser) {
|
||||
myIntent.addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
}
|
||||
startActivity(context, myIntent, false)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.d(e, "Could not find suitable activity to handle url")
|
||||
Toast.makeText(
|
||||
context, FoxActivity.getFoxActivity(context).getString(
|
||||
R.string.no_browser
|
||||
), Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
startActivity(context, myIntent, false);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Timber.d(e, "Could not find suitable activity to handle url");
|
||||
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
|
||||
R.string.no_browser), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void openCustomTab(Context context, String url) {
|
||||
Timber.d("Opening url: %s in custom tab", url);
|
||||
try {
|
||||
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
|
||||
Intent tabIntent = new Intent(viewIntent);
|
||||
tabIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
|
||||
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
startActivityEx(context, tabIntent, viewIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Timber.d(e, "Could not find suitable activity to handle url");
|
||||
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
|
||||
R.string.no_browser), Toast.LENGTH_LONG).show();
|
||||
@JvmStatic
|
||||
fun openCustomTab(context: Context, url: String?) {
|
||||
Timber.d("Opening url: %s in custom tab", url)
|
||||
try {
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
viewIntent.flags = FLAG_GRANT_URI_PERMISSION
|
||||
val tabIntent = Intent(viewIntent)
|
||||
tabIntent.flags = FLAG_GRANT_URI_PERMISSION
|
||||
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
startActivityEx(context, tabIntent, viewIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.d(e, "Could not find suitable activity to handle url")
|
||||
Toast.makeText(
|
||||
context, FoxActivity.getFoxActivity(context).getString(
|
||||
R.string.no_browser
|
||||
), Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void openUrlAndroidacy(Context context, String url,boolean allowInstall) {
|
||||
openUrlAndroidacy(context, url, allowInstall, null, null);
|
||||
}
|
||||
|
||||
public static void openUrlAndroidacy(Context context, String url, boolean allowInstall,
|
||||
String title,String config) {
|
||||
if (!Http.hasWebView()) {
|
||||
Timber.w("Using custom tab for: %s", url);
|
||||
openCustomTab(context, url);
|
||||
return;
|
||||
}
|
||||
Uri uri = Uri.parse(url);
|
||||
try {
|
||||
Intent myIntent = new Intent(
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun openUrlAndroidacy(
|
||||
context: Context,
|
||||
url: String?,
|
||||
allowInstall: Boolean,
|
||||
title: String? = null,
|
||||
config: String? = null
|
||||
) {
|
||||
if (!hasWebView()) {
|
||||
Timber.w("Using custom tab for: %s", url)
|
||||
openCustomTab(context, url)
|
||||
return
|
||||
}
|
||||
val uri = Uri.parse(url)
|
||||
try {
|
||||
val myIntent = Intent(
|
||||
Constants.INTENT_ANDROIDACY_INTERNAL,
|
||||
uri, context, AndroidacyActivity.class);
|
||||
myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, allowInstall);
|
||||
if (title != null)
|
||||
myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE, title);
|
||||
if (config != null)
|
||||
myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG, config);
|
||||
MainApplication.addSecret(myIntent);
|
||||
startActivity(context, myIntent, true);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(context, "No application can handle this request."
|
||||
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
|
||||
uri,
|
||||
context,
|
||||
AndroidacyActivity::class.java
|
||||
)
|
||||
myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, allowInstall)
|
||||
if (title != null) myIntent.putExtra(
|
||||
Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE, title
|
||||
)
|
||||
if (config != null) myIntent.putExtra(
|
||||
Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG, config
|
||||
)
|
||||
MainApplication.addSecret(myIntent)
|
||||
startActivity(context, myIntent, true)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"No application can handle this request." + " Please install a web-browser",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPackageOfConfig(String config) {
|
||||
int i = config.indexOf(' ');
|
||||
if (i != -1)
|
||||
config = config.substring(0, i);
|
||||
i = config.indexOf('/');
|
||||
if (i != -1)
|
||||
config = config.substring(0, i);
|
||||
return config;
|
||||
}
|
||||
@Suppress("NAME_SHADOWING")
|
||||
@JvmStatic
|
||||
fun getPackageOfConfig(config: String): String {
|
||||
var config = config
|
||||
var i = config.indexOf(' ')
|
||||
if (i != -1) config = config.substring(0, i)
|
||||
i = config.indexOf('/')
|
||||
if (i != -1) config = config.substring(0, i)
|
||||
return config
|
||||
}
|
||||
|
||||
public static void openConfig(Context context, String config) {
|
||||
String pkg = getPackageOfConfig(config);
|
||||
try {
|
||||
Intent intent = XHooks.getConfigIntent(context, pkg, config);
|
||||
if (intent == null) {
|
||||
if ("org.lsposed.manager".equals(config) && (
|
||||
XHooks.isModuleActive("riru_lsposed") ||
|
||||
XHooks.isModuleActive("zygisk_lsposed"))) {
|
||||
Shell.getShell().newJob().add(
|
||||
"am start -a android.intent.action.MAIN " +
|
||||
"-c org.lsposed.manager.LAUNCH_MANAGER " +
|
||||
"com.android.shell/.BugreportWarningActivity")
|
||||
.to(new CallbackList<>() {
|
||||
@Override
|
||||
public void onAddElement(String str) {
|
||||
Timber.i("LSPosed: %s", str);
|
||||
@JvmStatic
|
||||
fun openConfig(context: Context, config: String) {
|
||||
val pkg = getPackageOfConfig(config)
|
||||
try {
|
||||
var intent = getConfigIntent(context, pkg, config)
|
||||
if (intent == null) {
|
||||
if ("org.lsposed.manager" == config && (isModuleActive("riru_lsposed") || isModuleActive(
|
||||
"zygisk_lsposed"
|
||||
))
|
||||
) {
|
||||
Shell.getShell().newJob().add(
|
||||
"am start -a android.intent.action.MAIN " + "-c org.lsposed.manager.LAUNCH_MANAGER " + "com.android.shell/.BugreportWarningActivity"
|
||||
).to(object : CallbackList<String?>() {
|
||||
override fun onAddElement(str: String?) {
|
||||
Timber.i("LSPosed: %s", str)
|
||||
}
|
||||
}).submit();
|
||||
return;
|
||||
}).submit()
|
||||
return
|
||||
}
|
||||
intent = Intent("android.intent.action.APPLICATION_PREFERENCES")
|
||||
intent.setPackage(pkg)
|
||||
}
|
||||
intent = new Intent("android.intent.action.APPLICATION_PREFERENCES");
|
||||
intent.setPackage(pkg);
|
||||
intent.putExtra(Constants.EXTRA_FROM_MANAGER, true)
|
||||
startActivity(context, intent, false)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context, "Failed to launch module config activity", Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
intent.putExtra(Constants.EXTRA_FROM_MANAGER, true);
|
||||
startActivity(context, intent, false);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(context,
|
||||
"Failed to launch module config activity", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void openMarkdown(Context context, String url, String title, String config, Boolean changeBoot, Boolean needsRamdisk,int minMagisk, int minApi, int maxApi) {
|
||||
try {
|
||||
Intent intent = new Intent(context, MarkdownActivity.class);
|
||||
MainApplication.addSecret(intent);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_CHANGE_BOOT, changeBoot);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK, needsRamdisk);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_MAGISK, minMagisk);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_API, minApi);
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MAX_API, maxApi);
|
||||
if (config != null && !config.isEmpty())
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_CONFIG, config);
|
||||
startActivity(context, intent, true);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(context,
|
||||
"Failed to launch markdown activity", Toast.LENGTH_SHORT).show();
|
||||
@JvmStatic
|
||||
fun openMarkdown(
|
||||
context: Context,
|
||||
url: String?,
|
||||
title: String?,
|
||||
config: String?,
|
||||
changeBoot: Boolean?,
|
||||
needsRamdisk: Boolean?,
|
||||
minMagisk: Int,
|
||||
minApi: Int,
|
||||
maxApi: Int
|
||||
) {
|
||||
try {
|
||||
val intent = Intent(context, MarkdownActivity::class.java)
|
||||
MainApplication.addSecret(intent)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_CHANGE_BOOT, changeBoot)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK, needsRamdisk)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_MAGISK, minMagisk)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_API, minApi)
|
||||
intent.putExtra(Constants.EXTRA_MARKDOWN_MAX_API, maxApi)
|
||||
if (!config.isNullOrEmpty()) intent.putExtra(
|
||||
Constants.EXTRA_MARKDOWN_CONFIG, config
|
||||
)
|
||||
startActivity(context, intent, true)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context, "Failed to launch markdown activity", Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void openInstaller(Context context, String url, String title, String config,
|
||||
String checksum, boolean mmtReborn) {
|
||||
openInstaller(context, url, title, config, checksum, mmtReborn, false);
|
||||
}
|
||||
|
||||
public static void openInstaller(Context context, String url, String title, String config,
|
||||
String checksum, boolean mmtReborn, boolean testDebug) {
|
||||
try {
|
||||
Intent intent = new Intent(context, InstallerActivity.class);
|
||||
intent.setAction(Constants.INTENT_INSTALL_INTERNAL);
|
||||
MainApplication.addSecret(intent);
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_PATH, url);
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_NAME, title);
|
||||
if (config != null && !config.isEmpty())
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_CONFIG, config);
|
||||
if (checksum != null && !checksum.isEmpty())
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_CHECKSUM, checksum);
|
||||
if (mmtReborn) // Allow early styling of install process
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_MMT_REBORN, true);
|
||||
if (testDebug && BuildConfig.DEBUG)
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_TEST_ROOTLESS, true);
|
||||
startActivity(context, intent, true);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(context,
|
||||
"Failed to launch markdown activity", Toast.LENGTH_SHORT).show();
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun openInstaller(
|
||||
context: Context,
|
||||
url: String?,
|
||||
title: String?,
|
||||
config: String?,
|
||||
checksum: String?,
|
||||
mmtReborn: Boolean,
|
||||
testDebug: Boolean = false
|
||||
) {
|
||||
try {
|
||||
val intent = Intent(context, InstallerActivity::class.java)
|
||||
intent.action = Constants.INTENT_INSTALL_INTERNAL
|
||||
MainApplication.addSecret(intent)
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_PATH, url)
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_NAME, title)
|
||||
if (!config.isNullOrEmpty()) intent.putExtra(
|
||||
Constants.EXTRA_INSTALL_CONFIG, config
|
||||
)
|
||||
if (!checksum.isNullOrEmpty()) intent.putExtra(
|
||||
Constants.EXTRA_INSTALL_CHECKSUM, checksum
|
||||
)
|
||||
if (mmtReborn) // Allow early styling of install process
|
||||
intent.putExtra(Constants.EXTRA_INSTALL_MMT_REBORN, true)
|
||||
if (testDebug && BuildConfig.DEBUG) intent.putExtra(
|
||||
Constants.EXTRA_INSTALL_TEST_ROOTLESS, true
|
||||
)
|
||||
startActivity(context, intent, true)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context, "Failed to launch markdown activity", Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void startActivity(Context context, Class<? extends Activity> activityClass) {
|
||||
startActivity(context, new Intent(context, activityClass), true);
|
||||
}
|
||||
|
||||
public static void startActivity(Context context, Intent intent,boolean sameApp)
|
||||
throws ActivityNotFoundException {
|
||||
if (sameApp) {
|
||||
startActivityEx(context, intent, null);
|
||||
} else {
|
||||
startActivityEx(context, null, intent);
|
||||
fun startActivity(context: Context, activityClass: Class<out Activity?>?) {
|
||||
startActivity(context, Intent(context, activityClass), true)
|
||||
}
|
||||
}
|
||||
|
||||
public static void startActivityEx(Context context, Intent intent1,Intent intent2)
|
||||
throws ActivityNotFoundException {
|
||||
if (intent1 == null && intent2 == null)
|
||||
throw new NullPointerException("No intent defined for activity!");
|
||||
changeFlags(intent1, true);
|
||||
changeFlags(intent2, false);
|
||||
Activity activity = getActivity(context);
|
||||
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context,
|
||||
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
|
||||
if (activity == null) {
|
||||
if (intent1 != null) {
|
||||
try {
|
||||
context.startActivity(intent1, param);
|
||||
return;
|
||||
} catch (ActivityNotFoundException e) {
|
||||
if (intent2 == null) throw e;
|
||||
}
|
||||
@Throws(ActivityNotFoundException::class)
|
||||
fun startActivity(context: Context, intent: Intent?, sameApp: Boolean) {
|
||||
if (sameApp) {
|
||||
startActivityEx(context, intent, null)
|
||||
} else {
|
||||
startActivityEx(context, null, intent)
|
||||
}
|
||||
context.startActivity(intent2, param);
|
||||
} else {
|
||||
if (intent1 != null) {
|
||||
// Support Custom Tabs as sameApp intent
|
||||
if (intent1.hasCategory(Intent.CATEGORY_BROWSABLE)) {
|
||||
if (!intent1.hasExtra(EXTRA_TAB_SESSION)) {
|
||||
Bundle bundle = new Bundle();
|
||||
BundleCompat.putBinder(bundle, EXTRA_TAB_SESSION, null);
|
||||
intent1.putExtras(bundle);
|
||||
}
|
||||
|
||||
@Throws(ActivityNotFoundException::class)
|
||||
fun startActivityEx(context: Context, intent1: Intent?, intent2: Intent?) {
|
||||
if (intent1 == null && intent2 == null) throw NullPointerException("No intent defined for activity!")
|
||||
changeFlags(intent1, true)
|
||||
changeFlags(intent2, false)
|
||||
val activity = getActivity(context)
|
||||
val param = ActivityOptionsCompat.makeCustomAnimation(
|
||||
context, android.R.anim.fade_in, android.R.anim.fade_out
|
||||
).toBundle()
|
||||
if (activity == null) {
|
||||
if (intent1 != null) {
|
||||
try {
|
||||
context.startActivity(intent1, param)
|
||||
return
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (intent2 == null) throw e
|
||||
}
|
||||
intent1.putExtra(IntentHelper.EXTRA_TAB_EXIT_ANIMATION_BUNDLE, param);
|
||||
if (activity instanceof FoxActivity) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
activity.getTheme().resolveAttribute(
|
||||
android.R.attr.background, typedValue, true);
|
||||
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
|
||||
typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
|
||||
intent1.putExtra(IntentHelper.EXTRA_TAB_TOOLBAR_COLOR, typedValue.data);
|
||||
intent1.putExtra(IntentHelper.EXTRA_TAB_COLOR_SCHEME,
|
||||
((FoxActivity) activity).isLightTheme() ?
|
||||
IntentHelper.EXTRA_TAB_COLOR_SCHEME_LIGHT :
|
||||
IntentHelper.EXTRA_TAB_COLOR_SCHEME_DARK);
|
||||
}
|
||||
context.startActivity(intent2, param)
|
||||
} else {
|
||||
if (intent1 != null) {
|
||||
// Support Custom Tabs as sameApp intent
|
||||
if (intent1.hasCategory(Intent.CATEGORY_BROWSABLE)) {
|
||||
if (!intent1.hasExtra(EXTRA_TAB_SESSION)) {
|
||||
val bundle = Bundle()
|
||||
BundleCompat.putBinder(bundle, EXTRA_TAB_SESSION, null)
|
||||
intent1.putExtras(bundle)
|
||||
}
|
||||
intent1.putExtra(EXTRA_TAB_EXIT_ANIMATION_BUNDLE, param)
|
||||
if (activity is FoxActivity) {
|
||||
val typedValue = TypedValue()
|
||||
activity.getTheme().resolveAttribute(
|
||||
android.R.attr.background, typedValue, true
|
||||
)
|
||||
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
|
||||
intent1.putExtra(EXTRA_TAB_TOOLBAR_COLOR, typedValue.data)
|
||||
intent1.putExtra(
|
||||
EXTRA_TAB_COLOR_SCHEME,
|
||||
if (activity.isLightTheme) EXTRA_TAB_COLOR_SCHEME_LIGHT else EXTRA_TAB_COLOR_SCHEME_DARK
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
intent1.putExtra(Constants.EXTRA_FADE_OUT, true)
|
||||
activity.overridePendingTransition(
|
||||
android.R.anim.fade_in, android.R.anim.fade_out
|
||||
)
|
||||
activity.startActivity(intent1, param)
|
||||
return
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (intent2 == null) throw e
|
||||
}
|
||||
}
|
||||
try {
|
||||
intent1.putExtra(Constants.EXTRA_FADE_OUT, true);
|
||||
activity.overridePendingTransition(
|
||||
android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
activity.startActivity(intent1, param);
|
||||
return;
|
||||
} catch (ActivityNotFoundException e) {
|
||||
if (intent2 == null) throw e;
|
||||
}
|
||||
activity.startActivity(intent2, param)
|
||||
}
|
||||
activity.startActivity(intent2, param);
|
||||
}
|
||||
}
|
||||
|
||||
private static void changeFlags(Intent intent,boolean sameApp) {
|
||||
if (intent == null) return;
|
||||
int flags = intent.getFlags() &
|
||||
~(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||
if (!sameApp) {
|
||||
flags &= ~Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
|
||||
if (intent.getData() == null) {
|
||||
flags |= Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
} else {
|
||||
flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
|
||||
private fun changeFlags(intent: Intent?, sameApp: Boolean) {
|
||||
if (intent == null) return
|
||||
var flags =
|
||||
intent.flags and (Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT).inv()
|
||||
if (!sameApp) {
|
||||
flags = flags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK.inv()
|
||||
flags = if (intent.data == null) {
|
||||
flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
} else {
|
||||
flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
|
||||
}
|
||||
}
|
||||
intent.flags = flags
|
||||
}
|
||||
intent.setFlags(flags);
|
||||
}
|
||||
|
||||
public static Activity getActivity(Context context) {
|
||||
while (!(context instanceof Activity)) {
|
||||
if (context instanceof ContextWrapper) {
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
} else return null;
|
||||
@Suppress("NAME_SHADOWING")
|
||||
fun getActivity(context: Context?): Activity? {
|
||||
var context = context
|
||||
while (context !is Activity) {
|
||||
context = if (context is ContextWrapper) {
|
||||
context.baseContext
|
||||
} else return null
|
||||
}
|
||||
return context
|
||||
}
|
||||
return (Activity) context;
|
||||
}
|
||||
|
||||
public static final int RESPONSE_ERROR = 0;
|
||||
public static final int RESPONSE_FILE = 1;
|
||||
public static final int RESPONSE_URL = 2;
|
||||
private const val RESPONSE_ERROR = 0
|
||||
const val RESPONSE_FILE = 1
|
||||
const val RESPONSE_URL = 2
|
||||
|
||||
@SuppressLint("SdCardPath")
|
||||
public static void openFileTo(FoxActivity compatActivity, File destination,
|
||||
OnFileReceivedCallback callback) {
|
||||
File destinationFolder;
|
||||
if (destination == null || (destinationFolder = destination.getParentFile()) == null ||
|
||||
(!destinationFolder.mkdirs() && !destinationFolder.isDirectory())) {
|
||||
callback.onReceived(destination, null, RESPONSE_ERROR);
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("application/zip");
|
||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, false);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
Bundle param = ActivityOptionsCompat.makeCustomAnimation(compatActivity,
|
||||
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
|
||||
compatActivity.startActivityForResult(intent, param, (result, data) -> {
|
||||
Uri uri = data == null ? null : data.getData();
|
||||
if (uri == null || (result == Activity.RESULT_CANCELED && !((
|
||||
ContentResolver.SCHEME_FILE.equals(uri.getScheme())
|
||||
&& uri.getPath() != null &&
|
||||
(uri.getPath().startsWith("/sdcard/") ||
|
||||
uri.getPath().startsWith("/data/"))
|
||||
) || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())))) {
|
||||
callback.onReceived(destination, null, RESPONSE_ERROR);
|
||||
return;
|
||||
}
|
||||
Timber.i("FilePicker returned %s", uri);
|
||||
if ("http".equals(uri.getScheme()) ||
|
||||
"https".equals(uri.getScheme())) {
|
||||
callback.onReceived(destination, uri, RESPONSE_URL);
|
||||
return;
|
||||
@SuppressLint("SdCardPath")
|
||||
fun openFileTo(
|
||||
compatActivity: FoxActivity, destination: File?, callback: OnFileReceivedCallback
|
||||
) {
|
||||
var destinationFolder: File? = null
|
||||
if ((destination == null) || (destination.parentFile.also {
|
||||
destinationFolder = it
|
||||
} == null) || (!destinationFolder?.mkdirs()!! && !destinationFolder!!.isDirectory)) {
|
||||
callback.onReceived(destination, null, RESPONSE_ERROR)
|
||||
return
|
||||
}
|
||||
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) ||
|
||||
(result != Activity.RESULT_OK && result != Activity.RESULT_FIRST_USER)) {
|
||||
Toast.makeText(compatActivity,
|
||||
R.string.file_picker_wierd,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
if (path.startsWith("/sdcard/")) { // Fix file paths
|
||||
path = Environment.getExternalStorageDirectory()
|
||||
.getAbsolutePath() + path.substring(7);
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip")
|
||||
intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, false)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
val param = ActivityOptionsCompat.makeCustomAnimation(
|
||||
compatActivity, android.R.anim.fade_in, android.R.anim.fade_out
|
||||
).toBundle()
|
||||
compatActivity.startActivityForResult(intent, param) { result: Int, data: Intent? ->
|
||||
val uri = data?.data
|
||||
if (uri == null || result == Activity.RESULT_CANCELED && ContentResolver.SCHEME_FILE == uri.scheme && uri.path != null && (uri.path!!.startsWith(
|
||||
"/sdcard/"
|
||||
) || uri.path!!.startsWith("/data/")) || ContentResolver.SCHEME_ANDROID_RESOURCE != uri.scheme
|
||||
) {
|
||||
callback.onReceived(destination, null, RESPONSE_ERROR)
|
||||
return@startActivityForResult
|
||||
}
|
||||
Timber.i("FilePicker returned %s", uri)
|
||||
if ("http" == uri.scheme || "https" == uri.scheme) {
|
||||
callback.onReceived(destination, uri, RESPONSE_URL)
|
||||
return@startActivityForResult
|
||||
}
|
||||
if (ContentResolver.SCHEME_FILE == uri.scheme || result != Activity.RESULT_OK && result != Activity.RESULT_FIRST_USER) {
|
||||
Toast.makeText(
|
||||
compatActivity, R.string.file_picker_wierd, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
var success = false
|
||||
try {
|
||||
if (ContentResolver.SCHEME_FILE == uri.scheme) {
|
||||
var path = uri.path
|
||||
if (path!!.startsWith("/sdcard/")) { // Fix file paths
|
||||
path =
|
||||
Environment.getExternalStorageDirectory().absolutePath + path.substring(
|
||||
7
|
||||
)
|
||||
}
|
||||
inputStream = SuFileInputStream.open(
|
||||
File(path).absoluteFile
|
||||
)
|
||||
} else {
|
||||
inputStream = compatActivity.contentResolver.openInputStream(uri)
|
||||
}
|
||||
inputStream = SuFileInputStream.open(
|
||||
new File(path).getAbsoluteFile());
|
||||
} else {
|
||||
inputStream = compatActivity.getContentResolver()
|
||||
.openInputStream(uri);
|
||||
outputStream = FileOutputStream(destination)
|
||||
copy(inputStream!!, outputStream)
|
||||
Timber.i("File saved at %s", destination)
|
||||
success = true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Toast.makeText(
|
||||
compatActivity, R.string.file_picker_failure, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} finally {
|
||||
closeSilently(inputStream)
|
||||
closeSilently(outputStream)
|
||||
if (!success && destination.exists() && !destination.delete()) Timber.e("Failed to delete artefact!")
|
||||
}
|
||||
outputStream = new FileOutputStream(destination);
|
||||
Files.copy(inputStream, outputStream);
|
||||
Timber.i("File saved at %s", destination);
|
||||
success = true;
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
Toast.makeText(compatActivity,
|
||||
R.string.file_picker_failure,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} finally {
|
||||
Files.closeSilently(inputStream);
|
||||
Files.closeSilently(outputStream);
|
||||
if (!success && destination.exists() && !destination.delete())
|
||||
Timber.e("Failed to delete artefact!");
|
||||
callback.onReceived(
|
||||
destination, uri, if (success) RESPONSE_FILE else RESPONSE_ERROR
|
||||
)
|
||||
}
|
||||
callback.onReceived(destination, uri, success ? RESPONSE_FILE : RESPONSE_ERROR);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnFileReceivedCallback {
|
||||
void onReceived(File target,Uri uri,int response);
|
||||
fun openFileTo(
|
||||
compatActivity: FoxActivity?,
|
||||
destination: File,
|
||||
callback: (File, Uri, Int) -> Unit
|
||||
) {
|
||||
openFileTo(compatActivity!!, destination, object : OnFileReceivedCallback {
|
||||
override fun onReceived(target: File?, uri: Uri?, response: Int) {
|
||||
if (response == RESPONSE_ERROR) {
|
||||
MainActivity.getFoxActivity(compatActivity)?.runOnUiThread {
|
||||
Toast.makeText(
|
||||
compatActivity, R.string.no_file_provided, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
callback(target!!, uri!!, response)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
compatActivity.forceBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue