fixed bugs

added some more for later 😈

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/27/head
androidacy-user 2 years ago
parent a196ca32d3
commit 9424c16b4a

@ -1,5 +1,5 @@
@file:Suppress("KotlinConstantConditions", "UNINITIALIZED_ENUM_COMPANION_WARNING",
"ktConcatNullable", "BlockingMethodInNonBlockingContext"
"ktConcatNullable", "BlockingMethodInNonBlockingContext", "UnusedEquals"
)
package com.fox2code.mmm
@ -160,7 +160,7 @@ enum class NotificationType constructor(
compatActivity.cacheDir,
"installer" + File.separator + "module.zip"
)
IntentHelper.openFileTo(compatActivity, module) { d: File, u: Uri, s: Int ->
IntentHelper.openFileTo(compatActivity, module, { d: File, u: Uri, s: Int ->
if (s == IntentHelper.RESPONSE_FILE) {
try {
if (needPatch(d)) {
@ -202,7 +202,7 @@ enum class NotificationType constructor(
InstallerInitializer.peekMagiskPath() == null
)
}
}
})
},
false
) {

@ -165,9 +165,9 @@ class SetupActivity : FoxActivity(), LanguageActivity {
// Setup language selector
val languageSelector = view.findViewById<MaterialButton>(R.id.setup_language_button)
languageSelector.setOnClickListener { _: View? ->
val ls = LanguageSwitcher(Objects.requireNonNull(IntentHelper.getActivity(this)))
ls.setSupportedStringLocales(MainApplication.supportedLocales)
ls.showChangeLanguageDialog(IntentHelper.getActivity(this) as FragmentActivity)
val ls = IntentHelper.getActivity(this)?.let { LanguageSwitcher(it) }
ls?.setSupportedStringLocales(MainApplication.supportedLocales)
ls?.showChangeLanguageDialog(IntentHelper.getActivity(this) as FragmentActivity)
}
// Set up the buttons
// Setup button

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

@ -185,7 +185,7 @@ public class AndroidacyWebAPI {
if (BuildConfig.DEBUG)
Timber.d("Received openUrl request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
IntentHelper.openUrl(this.activity, url);
IntentHelper.Companion.openUrl(this.activity, url);
}
}
@ -261,7 +261,11 @@ public class AndroidacyWebAPI {
if (this.effectiveCompatMode < 1) {
if (!this.canInstall()) {
this.downloadMode = true;
this.activity.runOnUiThread(() -> this.activity.webView.loadUrl(moduleUrl));
this.activity.runOnUiThread(() -> {
if (this.activity.webView != null) {
this.activity.webView.loadUrl(moduleUrl);
}
});
} else {
this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
}

@ -10,7 +10,7 @@ class BackgroundBootListener : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (BOOT_COMPLETED != intent.action) return
if (!MainApplication.isBackgroundUpdateCheckEnabled()) return
if (!Http.hasConnectivity()) return
if (!Http.hasConnectivity(context)) return
// clear boot shared prefs
MainApplication.getBootSharedPreferences().edit().clear().apply()
synchronized(BackgroundUpdateChecker.lock) {

@ -235,7 +235,7 @@ public enum ActionButtonType {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
IntentHelper.Companion.openUrl(button.getContext(), Objects.requireNonNull(moduleHolder.getMainModuleInfo().support));
}
}, DONATE() {
@Override
@ -254,7 +254,7 @@ public enum ActionButtonType {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
IntentHelper.Companion.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
}
}, WARNING() {
@Override

@ -345,7 +345,7 @@ public final class RepoManager extends SyncManager {
}
public boolean hasConnectivity() {
return Http.hasConnectivity();
return Http.hasConnectivity(MainApplication.getINSTANCE().getApplicationContext());
}
private RepoData addRepoData(String url, String fallBackName) {

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

@ -224,16 +224,6 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
// track all non empty values
SharedPreferences sharedPreferences = dataStore.getSharedPreferences();
// disabled until EncryptedSharedPreferences fixes getAll()
/* StringBuilder keys = new StringBuilder();
for (String key : sharedPreferences.getAll().keySet()) {
String value = sharedPreferences.getString(key, null);
if (value != null) {
keys.append(key).append(",");
}
}
if (keys.length() > 0) {
TrackHelper.track().event("prefs_all", keys.toString()).with(MainApplication.getINSTANCE().getTracker());
}*/
// add bottom navigation bar to the settings
BottomNavigationView bottomNavigationView = requireActivity().findViewById(R.id.bottom_navigation);
if (bottomNavigationView != null) {
@ -726,7 +716,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
devModeStepFirstBootIgnore = true;
IntentHelper.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues");
IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -768,7 +758,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
return true;
}
// build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end
IntentHelper.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH);
IntentHelper.Companion.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -780,7 +770,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
// Next, the pref_androidacy_thanks should lead to the androidacy website
linkClickable = findPreference("pref_androidacy_thanks");
linkClickable.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager");
IntentHelper.Companion.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -792,7 +782,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
// pref_fox2code_thanks should lead to https://github.com/Fox2Code
linkClickable = findPreference("pref_fox2code_thanks");
linkClickable.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code");
IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Fox2Code");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -855,7 +845,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
url = url.substring(0, url.length() - 4);
}
url += "/graphs/contributors";
IntentHelper.openUrl(p.getContext(), url);
IntentHelper.Companion.openUrl(p.getContext(), url);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -873,7 +863,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
linkClickable = findPreference("pref_support");
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
IntentHelper.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat");
IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -886,7 +876,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
linkClickable = findPreference("pref_announcements");
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
IntentHelper.openUrl(p.getContext(), "https://t.me/androidacy");
IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/androidacy");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
@ -923,7 +913,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
if (ref.versionClicks == 7) {
ref.versionClicks = 0;
IntentHelper.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
IntentHelper.Companion.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
}
return true;
});
@ -932,7 +922,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (!BuildConfig.FLAVOR.equals("play")) {
pref_donate_fox.setOnPreferenceClickListener(p -> {
// open fox
IntentHelper.openUrl(getFoxActivity(this), "https://paypal.me/fox2code");
IntentHelper.Companion.openUrl(getFoxActivity(this), "https://paypal.me/fox2code");
return true;
});
// handle long click on pref_donate_fox
@ -956,7 +946,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "FOX2CODE"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
// open androidacy
IntentHelper.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate");
IntentHelper.Companion.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate");
return true;
});
// handle long click on pref_donate_androidacy
@ -1480,7 +1470,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
// set website, support, and submitmodule as well as donate
if (repoData.getWebsite() != null) {
findPreference(preferenceName + "_website").setOnPreferenceClickListener((preference1 -> {
IntentHelper.openUrl(getFoxActivity(this), repoData.getWebsite());
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getWebsite());
return true;
}));
} else {
@ -1488,7 +1478,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
if (repoData.getSupport() != null) {
findPreference(preferenceName + "_support").setOnPreferenceClickListener((preference1 -> {
IntentHelper.openUrl(getFoxActivity(this), repoData.getSupport());
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSupport());
return true;
}));
} else {
@ -1496,7 +1486,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
if (repoData.getSubmitModule() != null) {
findPreference(preferenceName + "_submit").setOnPreferenceClickListener((preference1 -> {
IntentHelper.openUrl(getFoxActivity(this), repoData.getSubmitModule());
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSubmitModule());
return true;
}));
} else {
@ -1504,7 +1494,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
if (repoData.getDonate() != null) {
findPreference(preferenceName + "_donate").setOnPreferenceClickListener((preference1 -> {
IntentHelper.openUrl(getFoxActivity(this), repoData.getDonate());
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getDonate());
return true;
}));
} else {
@ -1543,7 +1533,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (!homepage.isEmpty()) {
preference.setVisible(true);
preference.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(getFoxActivity(this), homepage);
IntentHelper.Companion.openUrl(getFoxActivity(this), homepage);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
@ -1563,7 +1553,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
preference.setVisible(true);
preference.setIcon(ActionButtonType.supportIconForUrl(supportUrl));
preference.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(getFoxActivity(this), supportUrl);
IntentHelper.Companion.openUrl(getFoxActivity(this), supportUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
@ -1583,7 +1573,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
preference.setVisible(true);
preference.setIcon(ActionButtonType.donateIconForUrl(donateUrl));
preference.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(getFoxActivity(this), donateUrl);
IntentHelper.Companion.openUrl(getFoxActivity(this), donateUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
@ -1602,7 +1592,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (submissionUrl != null && !submissionUrl.isEmpty()) {
preference.setVisible(true);
preference.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(getFoxActivity(this), submissionUrl);
IntentHelper.Companion.openUrl(getFoxActivity(this), submissionUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {

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

@ -3,13 +3,12 @@
package com.fox2code.mmm.utils.io.net
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.system.ErrnoException
import android.system.Os
import android.webkit.CookieManager
@ -24,7 +23,6 @@ import com.fox2code.mmm.androidacy.AndroidacyUtil
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
import com.fox2code.mmm.utils.io.Files.Companion.makeBuffer
import com.google.android.material.snackbar.Snackbar
import com.google.net.cronet.okhttptransport.CronetInterceptor
import okhttp3.Cache
import okhttp3.Dns
@ -57,11 +55,9 @@ import java.net.UnknownHostException
import java.nio.charset.StandardCharsets
import java.util.Objects
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLException
import kotlin.system.exitProcess
enum class Http {
;
enum class Http {;
interface ProgressListener {
fun onUpdate(downloaded: Int, total: Int, done: Boolean)
@ -84,8 +80,9 @@ enum class Http {
init {
sharedPreferences = context.getSharedPreferences("mmm_dns", Context.MODE_PRIVATE)
this.parent = parent
this.fallbacks = HashSet(listOf(*fallbacks)).toString().replaceAfter("]", "").replace("[", "")
.split(",").toHashSet()
this.fallbacks =
HashSet(listOf(*fallbacks)).toString().replaceAfter("]", "").replace("[", "")
.split(",").toHashSet()
fallbackCache = HashMap()
}
@ -103,8 +100,8 @@ enum class Http {
hostname
)
fallbackCache[hostname] = addresses
sharedPreferences.edit().putString(hostname.replace('.', '_'), toString(addresses))
.apply()
sharedPreferences.edit()
.putString(hostname.replace('.', '_'), toString(addresses)).apply()
} catch (e: UnknownHostException) {
val key = sharedPreferences.getString(hostname.replace('.', '_'), "")
if (key!!.isEmpty()) throw e
@ -180,11 +177,13 @@ enum class Http {
}
companion object {
private var limitedRetries: Int = 0
private var httpClient: OkHttpClient? = null
private var httpClientDoH: OkHttpClient? = null
private var httpClientWithCache: OkHttpClient? = null
private var httpClientWithCacheDoH: OkHttpClient? = null
private var fallbackDNS: FallBackDNS? = null
@JvmStatic
var androidacyUA: String? = null
private var hasWebView = false
@ -214,23 +213,18 @@ enum class Http {
} catch (t: Exception) {
Timber.e(t, "No WebView support!")
// show a toast
val context: Context =
mainApplication.applicationContext
val context: Context = mainApplication.applicationContext
MainActivity.getFoxActivity(context).runOnUiThread {
Toast.makeText(
mainApplication,
R.string.error_creating_cookie_database,
Toast.LENGTH_LONG
mainApplication, R.string.error_creating_cookie_database, Toast.LENGTH_LONG
).show()
}
}
// get webview version
var webviewVersion = "0.0.0"
val pi =
WebViewCompat.getCurrentWebViewPackage(mainApplication)
val pi = WebViewCompat.getCurrentWebViewPackage(mainApplication)
if (pi != null) {
webviewVersion =
pi.versionName
webviewVersion = pi.versionName
}
// webviewVersionMajor is the everything before the first dot
val webviewVersionCode: Int
@ -243,14 +237,11 @@ enum class Http {
} else {
// use the first dot
webviewVersion.substring(
0,
dot
0, dot
).toInt()
}
Timber.d(
"Webview version: %s (%d)",
webviewVersion,
webviewVersionCode
"Webview version: %s (%d)", webviewVersion, webviewVersionCode
)
hasWebView =
cookieManager != null && webviewVersionCode >= 83 // 83 is the first version Androidacy supports due to errors in 82
@ -317,13 +308,10 @@ enum class Http {
)
}
}
if (chain.request()
.header("Accept-Language") == null
) {
if (chain.request().header("Accept-Language") == null) {
request.header(
"Accept-Language", // Send system language to the server
mainApplication.resources
.configuration.locales.get(0).toLanguageTag()
mainApplication.resources.configuration.locales.get(0).toLanguageTag()
)
}
// add client hints
@ -331,16 +319,13 @@ enum class Http {
request.header("Sec-CH-UA-Mobile", "?1")
request.header("Sec-CH-UA-Platform", "Android")
request.header(
"Sec-CH-UA-Platform-Version",
Build.VERSION.RELEASE
"Sec-CH-UA-Platform-Version", Build.VERSION.RELEASE
)
request.header(
"Sec-CH-UA-Arch",
Build.SUPPORTED_ABIS[0]
"Sec-CH-UA-Arch", Build.SUPPORTED_ABIS[0]
)
request.header(
"Sec-CH-UA-Full-Version",
BuildConfig.VERSION_NAME
"Sec-CH-UA-Full-Version", BuildConfig.VERSION_NAME
)
request.header("Sec-CH-UA-Model", Build.DEVICE)
request.header(
@ -370,20 +355,17 @@ enum class Http {
builder.enableQuic(true)
// Cache size is 10MB
// Make the directory if it does not exist
val cacheDir =
File(mainApplication.cacheDir, "cronet")
val cacheDir = File(mainApplication.cacheDir, "cronet")
if (!cacheDir.exists()) {
if (!cacheDir.mkdirs()) {
throw IOException("Failed to create cronet cache directory")
}
}
builder.setStoragePath(
mainApplication.cacheDir
.absolutePath + "/cronet"
mainApplication.cacheDir.absolutePath + "/cronet"
)
builder.enableHttpCache(
CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP,
(10 * 1024 * 1024).toLong()
CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, (10 * 1024 * 1024).toLong()
)
// Add quic hint
builder.addQuicHint("github.com", 443, 443)
@ -418,32 +400,30 @@ enum class Http {
"production-api.androidacy.com"
)
httpclientBuilder.dns(Dns.SYSTEM)
httpClient =
followRedirects(httpclientBuilder, true).build()
httpClient = followRedirects(httpclientBuilder, true).build()
followRedirects(httpclientBuilder, false).build()
httpclientBuilder.dns(fallbackDNS!!)
httpClientDoH =
followRedirects(httpclientBuilder, true).build()
httpClientDoH = followRedirects(httpclientBuilder, true).build()
followRedirects(httpclientBuilder, false).build()
httpclientBuilder.cache(
Cache(
File(
mainApplication.cacheDir,
"http_cache"
mainApplication.cacheDir, "http_cache"
), 16L * 1024L * 1024L
)
) // 16Mib of cache
httpclientBuilder.dns(Dns.SYSTEM)
httpClientWithCache =
followRedirects(httpclientBuilder, true).build()
httpClientWithCache = followRedirects(httpclientBuilder, true).build()
httpclientBuilder.dns(fallbackDNS!!)
httpClientWithCacheDoH =
followRedirects(httpclientBuilder, true).build()
httpClientWithCacheDoH = followRedirects(httpclientBuilder, true).build()
Timber.i("Initialized Http successfully!")
doh = MainApplication.isDohEnabled()
}
private fun followRedirects(builder: OkHttpClient.Builder, followRedirects: Boolean): OkHttpClient.Builder {
private fun followRedirects(
builder: OkHttpClient.Builder,
followRedirects: Boolean
): OkHttpClient.Builder {
return builder.followRedirects(followRedirects).followSslRedirects(followRedirects)
}
@ -503,8 +483,7 @@ enum class Http {
if (response.code != 200 && response.code != 204 && (response.code != 304 || !allowCache)) {
Timber.e(
"Failed to fetch " + url.replace(
"=[^&]*".toRegex(),
"=****"
"=[^&]*".toRegex(), "=****"
) + " with code " + response.code
)
checkNeedCaptchaAndroidacy(url, response.code)
@ -513,6 +492,34 @@ enum class Http {
// Regenerate the token
throw HttpException("Androidacy token is invalid", 401)
}
if (response.code == 429) {
val retryAfter = response.header("Retry-After")
if (retryAfter != null) {
try {
val seconds = Integer.parseInt(retryAfter)
Timber.d("Sleeping for $seconds seconds")
Thread.sleep(seconds * 1000L)
} catch (e: NumberFormatException) {
Timber.e(e, "Failed to parse Retry-After header")
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
} else {// start with one second and try up to five times
if (limitedRetries < 5) {
limitedRetries++
Timber.d("Sleeping for 1 second")
try {
Thread.sleep(1000L * limitedRetries)
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
return doHttpGet(url, allowCache)
} else {
Timber.e("Failed to fetch " + url + ", code: " + response.code)
throw HttpException(response.code)
}
}
}
throw HttpException(response.code)
}
}
@ -566,6 +573,34 @@ enum class Http {
if (response.code != 200 && response.code != 204 && (response.code != 304 || !allowCache)) {
if (BuildConfig.DEBUG_HTTP) Timber.e("Failed to fetch " + url + ", code: " + response.code + ", body: " + response.body.string())
checkNeedCaptchaAndroidacy(url, response.code)
if (response.code == 429) {
val retryAfter = response.header("Retry-After")
if (retryAfter != null) {
try {
val seconds = Integer.parseInt(retryAfter)
Timber.d("Sleeping for $seconds seconds")
Thread.sleep(seconds * 1000L)
} catch (e: NumberFormatException) {
Timber.e(e, "Failed to parse Retry-After header")
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
} else {// start with one second and try up to five times
if (limitedRetries < 5) {
limitedRetries++
Timber.d("Sleeping for 1 second")
try {
Thread.sleep(1000L * limitedRetries)
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
return doHttpPostRaw(url, data, allowCache)
} else {
Timber.e("Failed to fetch " + url + ", code: " + response.code)
throw HttpException(response.code)
}
}
}
throw HttpException(response.code)
}
var responseBody = response.body
@ -580,11 +615,40 @@ enum class Http {
@JvmStatic
@Throws(IOException::class)
fun doHttpGet(url: String, progressListener: ProgressListener): ByteArray {
val response = getHttpClient()!!
.newCall(Request.Builder().url(url).get().build()).execute()
val response =
getHttpClient()!!.newCall(Request.Builder().url(url).get().build()).execute()
if (response.code != 200 && response.code != 204) {
Timber.e("Failed to fetch " + url + ", code: " + response.code)
checkNeedCaptchaAndroidacy(url, response.code)
// if error is 429, exponential backoff
if (response.code == 429) {
val retryAfter = response.header("Retry-After")
if (retryAfter != null) {
try {
val seconds = Integer.parseInt(retryAfter)
Timber.d("Sleeping for $seconds seconds")
Thread.sleep(seconds * 1000L)
} catch (e: NumberFormatException) {
Timber.e(e, "Failed to parse Retry-After header")
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
} else {// start with one second and try up to five times
if (limitedRetries < 5) {
limitedRetries++
Timber.d("Sleeping for 1 second")
try {
Thread.sleep(1000L * limitedRetries)
} catch (e: InterruptedException) {
Timber.e(e, "Failed to sleep")
}
return doHttpGet(url, progressListener)
} else {
Timber.e("Failed to fetch " + url + ", code: " + response.code)
throw HttpException(response.code)
}
}
}
throw HttpException(response.code)
}
val responseBody = Objects.requireNonNull(response.body)
@ -611,17 +675,13 @@ enum class Http {
if (nextUpdate < currentUpdate) {
nextUpdate = currentUpdate + updateInterval
progressListener.onUpdate(
(downloaded / divider).toInt(),
(target / divider).toInt(),
false
(downloaded / divider).toInt(), (target / divider).toInt(), false
)
}
}
inputStream.close()
progressListener.onUpdate(
(downloaded / divider).toInt(),
(target / divider).toInt(),
true
(downloaded / divider).toInt(), (target / divider).toInt(), true
)
return byteArrayOutputStream.toByteArray()
}
@ -654,36 +714,16 @@ enum class Http {
}
@JvmStatic
fun hasConnectivity(): Boolean {
// Check if we have internet connection
Timber.d("Checking internet connection...")
// this url is actually hosted by Cloudflare and is not dependent on Androidacy servers being up
val resp: ByteArray = try {
doHttpGet("https://production-api.androidacy.com/cdn-cgi/trace", false)
} catch (e: Exception) {
Timber.e(e, "Failed to check internet connection. Assuming no internet connection.")
// check if it's a security or ssl exception
if (e is SSLException || e is SecurityException) {
// if it is, user installed a certificate that blocks the connection
// show a snackbar to inform the user
val context: Activity? = MainApplication.getINSTANCE().lastCompatActivity
Handler(Looper.getMainLooper()).post {
if (context != null) {
Snackbar.make(
context.findViewById(android.R.id.content),
R.string.certificate_error,
Snackbar.LENGTH_LONG
).show()
}
}
}
return false
}
// get the response body
val response = String(resp, StandardCharsets.UTF_8)
// check if the response body contains "visit_scheme=https" and "http/<some number>"
// if it does, we have internet connection
return response.contains("visit_scheme=https") && response.contains("http/")
fun hasConnectivity(context: Context): Boolean {
// Check if we have internet connection using connectivity manager
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// are we connected to a network with internet capabilities?
val networkCapabilities =
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return networkCapabilities != null && networkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_INTERNET
)
}
}
}
Loading…
Cancel
Save