You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
822 lines
45 KiB
Java
822 lines
45 KiB
Java
package com.fox2code.mmm;
|
|
|
|
import static com.fox2code.mmm.MainApplication.Iof;
|
|
import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE;
|
|
|
|
import android.Manifest;
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.Configuration;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Color;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.provider.Settings;
|
|
import android.util.TypedValue;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.WindowManager;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.CheckBox;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.appcompat.widget.SearchView;
|
|
import androidx.cardview.widget.CardView;
|
|
import androidx.core.app.NotificationManagerCompat;
|
|
import androidx.core.content.ContextCompat;
|
|
import androidx.preference.PreferenceManager;
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
|
|
import com.fox2code.foxcompat.app.FoxActivity;
|
|
import com.fox2code.foxcompat.view.FoxDisplay;
|
|
import com.fox2code.mmm.background.BackgroundUpdateChecker;
|
|
import com.fox2code.mmm.installer.InstallerInitializer;
|
|
import com.fox2code.mmm.manager.LocalModuleInfo;
|
|
import com.fox2code.mmm.manager.ModuleManager;
|
|
import com.fox2code.mmm.module.ModuleViewAdapter;
|
|
import com.fox2code.mmm.module.ModuleViewListBuilder;
|
|
import com.fox2code.mmm.repo.RepoManager;
|
|
import com.fox2code.mmm.settings.SettingsActivity;
|
|
import com.fox2code.mmm.utils.ExternalHelper;
|
|
import com.fox2code.mmm.utils.io.net.Http;
|
|
import com.fox2code.mmm.utils.realm.ReposList;
|
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
|
import com.google.android.material.snackbar.Snackbar;
|
|
|
|
import org.matomo.sdk.extra.TrackHelper;
|
|
|
|
import java.sql.Timestamp;
|
|
import java.util.Objects;
|
|
|
|
import io.realm.Realm;
|
|
import io.realm.RealmConfiguration;
|
|
import timber.log.Timber;
|
|
|
|
public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper {
|
|
private static final int PRECISION = 10000;
|
|
public static boolean doSetupNowRunning = true;
|
|
public static boolean doSetupRestarting = false;
|
|
public final ModuleViewListBuilder moduleViewListBuilder;
|
|
public final ModuleViewListBuilder moduleViewListBuilderOnline;
|
|
public LinearProgressIndicator progressIndicator;
|
|
private ModuleViewAdapter moduleViewAdapter;
|
|
private ModuleViewAdapter moduleViewAdapterOnline;
|
|
private SwipeRefreshLayout swipeRefreshLayout;
|
|
private int swipeRefreshLayoutOrigStartOffset;
|
|
private int swipeRefreshLayoutOrigEndOffset;
|
|
private long swipeRefreshBlocker = 0;
|
|
private int overScrollInsetTop;
|
|
private int overScrollInsetBottom;
|
|
private RecyclerView moduleList;
|
|
private RecyclerView moduleListOnline;
|
|
private CardView searchCard;
|
|
private SearchView searchView;
|
|
private boolean initMode;
|
|
|
|
public MainActivity() {
|
|
this.moduleViewListBuilder = new ModuleViewListBuilder(this);
|
|
this.moduleViewListBuilderOnline = new ModuleViewListBuilder(this);
|
|
this.moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
BackgroundUpdateChecker.onMainActivityResume(this);
|
|
super.onResume();
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
this.initMode = true;
|
|
if (doSetupRestarting) {
|
|
doSetupRestarting = false;
|
|
}
|
|
BackgroundUpdateChecker.onMainActivityCreate(this);
|
|
super.onCreate(savedInstanceState);
|
|
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
|
|
// track enabled repos
|
|
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build();
|
|
Realm realm = Realm.getInstance(realmConfig);
|
|
StringBuilder enabledRepos = new StringBuilder();
|
|
realm.executeTransaction(r -> {
|
|
for (ReposList r2 : r.where(ReposList.class).equalTo("enabled", true).findAll()) {
|
|
enabledRepos.append(r2.getUrl()).append(":").append(r2.getName()).append(",");
|
|
}
|
|
});
|
|
if (enabledRepos.length() > 0) {
|
|
enabledRepos.setLength(enabledRepos.length() - 1);
|
|
}
|
|
TrackHelper.track().event("enabled_repos", enabledRepos.toString()).with(MainApplication.getINSTANCE().getTracker());
|
|
realm.close();
|
|
// hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug
|
|
if (BuildConfig.ENABLE_PROTECTION && !Iof && !BuildConfig.DEBUG) {
|
|
throw new RuntimeException("This is not an official build of FoxMMM");
|
|
} else if (!Iof && !BuildConfig.DEBUG) {
|
|
Timber.w("You may be running an untrusted build.");
|
|
// Show a toast to warn the user
|
|
Toast.makeText(this, R.string.not_official_build, Toast.LENGTH_LONG).show();
|
|
}
|
|
Timestamp ts = new Timestamp(System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000));
|
|
// check if this build has expired
|
|
Timestamp buildTime = new Timestamp(BuildConfig.BUILD_TIME);
|
|
// if the build time is more than 30 days ago, throw an exception
|
|
if (ts.getTime() >= buildTime.getTime()) {
|
|
throw new IllegalStateException("This build has expired. Please download a stable build or update to the latest version.");
|
|
}
|
|
setContentView(R.layout.activity_main);
|
|
this.setTitle(R.string.app_name);
|
|
// set window flags to ignore status bar
|
|
this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes();
|
|
layoutParams.layoutInDisplayCutoutMode = // Support cutout in Android 9
|
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
|
this.getWindow().setAttributes(layoutParams);
|
|
}
|
|
this.progressIndicator = findViewById(R.id.progress_bar);
|
|
this.swipeRefreshLayout = findViewById(R.id.swipe_refresh);
|
|
this.swipeRefreshLayoutOrigStartOffset = this.swipeRefreshLayout.getProgressViewStartOffset();
|
|
this.swipeRefreshLayoutOrigEndOffset = this.swipeRefreshLayout.getProgressViewEndOffset();
|
|
this.swipeRefreshBlocker = Long.MAX_VALUE;
|
|
this.moduleList = findViewById(R.id.module_list);
|
|
this.moduleListOnline = findViewById(R.id.module_list_online);
|
|
this.searchCard = findViewById(R.id.search_card);
|
|
this.searchView = findViewById(R.id.search_bar);
|
|
this.searchView.setIconified(true);
|
|
this.moduleViewAdapter = new ModuleViewAdapter();
|
|
this.moduleViewAdapterOnline = new ModuleViewAdapter();
|
|
this.moduleList.setAdapter(this.moduleViewAdapter);
|
|
this.moduleListOnline.setAdapter(this.moduleViewAdapterOnline);
|
|
this.moduleList.setLayoutManager(new LinearLayoutManager(this));
|
|
this.moduleListOnline.setLayoutManager(new LinearLayoutManager(this));
|
|
this.moduleList.setItemViewCacheSize(4); // Default is 2
|
|
this.swipeRefreshLayout.setOnRefreshListener(this);
|
|
// add background blur if enabled
|
|
this.updateBlurState();
|
|
//hideActionBar();
|
|
checkShowInitialSetup();
|
|
this.moduleList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
@Override
|
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
|
if (newState != RecyclerView.SCROLL_STATE_IDLE)
|
|
MainActivity.this.searchView.clearFocus();
|
|
}
|
|
});
|
|
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
|
|
this.searchView.setMinimumHeight(FoxDisplay.dpToPixel(16));
|
|
this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | EditorInfo.IME_FLAG_NO_FULLSCREEN);
|
|
this.searchView.setOnQueryTextListener(this);
|
|
this.searchView.setOnCloseListener(this);
|
|
this.searchView.setOnQueryTextFocusChangeListener((v, h) -> {
|
|
if (!h) {
|
|
String query = this.searchView.getQuery().toString();
|
|
if (query.isEmpty()) {
|
|
this.searchView.setIconified(true);
|
|
}
|
|
}
|
|
this.cardIconifyUpdate();
|
|
});
|
|
this.searchView.setEnabled(false); // Enabled later
|
|
this.cardIconifyUpdate();
|
|
this.updateScreenInsets(this.getResources().getConfiguration());
|
|
|
|
// on the bottom nav, there's a settings item. open the settings activity when it's clicked.
|
|
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
|
|
bottomNavigationView.setOnItemSelectedListener(item -> {
|
|
if (item.getItemId() == R.id.settings_menu_item) {
|
|
TrackHelper.track().event("view_list", "settings").with(MainApplication.getINSTANCE().getTracker());
|
|
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
|
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
|
finish();
|
|
} else if (item.getItemId() == R.id.online_menu_item) {
|
|
TrackHelper.track().event("view_list", "online_modules").with(MainApplication.getINSTANCE().getTracker());
|
|
// set module_list_online as visible and module_list as gone. fade in/out
|
|
this.moduleListOnline.setAlpha(0F);
|
|
this.moduleListOnline.setVisibility(View.VISIBLE);
|
|
this.moduleListOnline.animate().alpha(1F).setDuration(300).setListener(null);
|
|
this.moduleList.animate().alpha(0F).setDuration(300).setListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
MainActivity.this.moduleList.setVisibility(View.GONE);
|
|
}
|
|
});
|
|
// clear search view
|
|
this.searchView.setQuery("", false);
|
|
this.searchView.clearFocus();
|
|
} else if (item.getItemId() == R.id.installed_menu_item) {
|
|
TrackHelper.track().event("view_list", "installed_modules").with(MainApplication.getINSTANCE().getTracker());
|
|
// set module_list_online as gone and module_list as visible. fade in/out
|
|
this.moduleList.setAlpha(0F);
|
|
this.moduleList.setVisibility(View.VISIBLE);
|
|
this.moduleList.animate().alpha(1F).setDuration(300).setListener(null);
|
|
this.moduleListOnline.animate().alpha(0F).setDuration(300).setListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
MainActivity.this.moduleListOnline.setVisibility(View.GONE);
|
|
}
|
|
});
|
|
// set search view to cleared
|
|
this.searchView.setQuery("", false);
|
|
this.searchView.clearFocus();
|
|
}
|
|
return true;
|
|
});
|
|
// update the padding of blur_frame to match the new bottom nav height
|
|
View blurFrame = findViewById(R.id.blur_frame);
|
|
blurFrame.post(() -> blurFrame.setPadding(blurFrame.getPaddingLeft(), blurFrame.getPaddingTop(), blurFrame.getPaddingRight(), bottomNavigationView.getHeight()));
|
|
// for some reason, root_container has a margin at the top. remove it.
|
|
View rootContainer = findViewById(R.id.root_container);
|
|
rootContainer.post(() -> {
|
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) rootContainer.getLayoutParams();
|
|
params.topMargin = 0;
|
|
rootContainer.setLayoutParams(params);
|
|
rootContainer.setY(0F);
|
|
});
|
|
// reset update module and update module count in main application
|
|
MainApplication.getINSTANCE().resetUpdateModule();
|
|
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
|
|
@Override
|
|
public void onPathReceived(String path) {
|
|
Timber.i("Got magisk path: %s", path);
|
|
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
|
|
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
|
|
if (!MainApplication.isShowcaseMode())
|
|
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
|
|
ModuleManager.getINSTANCE().scan();
|
|
ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
|
|
this.commonNext();
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(int error) {
|
|
Timber.e("Failed to get magisk path!");
|
|
moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
|
|
moduleViewListBuilderOnline.addNotification(InstallerInitializer.getErrorNotification());
|
|
this.commonNext();
|
|
}
|
|
|
|
public void commonNext() {
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("Common next");
|
|
moduleViewListBuilder.addNotification(NotificationType.DEBUG);
|
|
}
|
|
NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline);
|
|
// hide progress bar is repo-manager says we have no internet
|
|
if (!RepoManager.getINSTANCE().hasConnectivity()) {
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setVisibility(View.GONE);
|
|
progressIndicator.setIndeterminate(false);
|
|
progressIndicator.setMax(PRECISION);
|
|
});
|
|
}
|
|
updateScreenInsets(); // Fix an edge case
|
|
if (waitInitialSetupFinished()) {
|
|
Timber.d("waiting...");
|
|
return;
|
|
}
|
|
swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
|
|
if (MainApplication.isShowcaseMode())
|
|
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
|
|
if (!Http.hasWebView()) {
|
|
// Check Http for WebView availability
|
|
moduleViewListBuilder.addNotification(NotificationType.NO_WEB_VIEW);
|
|
// disable online tab
|
|
runOnUiThread(() -> {
|
|
bottomNavigationView.getMenu().getItem(1).setEnabled(false);
|
|
bottomNavigationView.setSelectedItemId(R.id.installed_menu_item);
|
|
});
|
|
}
|
|
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setIndeterminate(false);
|
|
progressIndicator.setMax(PRECISION);
|
|
// Fix insets not being accounted for correctly
|
|
updateScreenInsets(getResources().getConfiguration());
|
|
});
|
|
|
|
Timber.i("Scanning for modules!");
|
|
if (BuildConfig.DEBUG) Timber.i("Initialize Update");
|
|
final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
|
|
if (RepoManager.getINSTANCE().getCustomRepoManager() != null && RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
|
|
Timber.w("Need update on create");
|
|
} else if (RepoManager.getINSTANCE().getCustomRepoManager() == null) {
|
|
Timber.w("CustomRepoManager is null");
|
|
}
|
|
// update compat metadata
|
|
if (BuildConfig.DEBUG) Timber.i("Check Update Compat");
|
|
AppUpdateManager.getAppUpdateManager().checkUpdateCompat();
|
|
if (BuildConfig.DEBUG) Timber.i("Check Update");
|
|
// update repos
|
|
if (Http.hasWebView()) {
|
|
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
|
|
}
|
|
// various notifications
|
|
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
|
|
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilderOnline);
|
|
NotificationType.DEBUG.autoAdd(moduleViewListBuilder);
|
|
NotificationType.DEBUG.autoAdd(moduleViewListBuilderOnline);
|
|
if (Http.hasWebView() && !NotificationType.REPO_UPDATE_FAILED.shouldRemove()) {
|
|
moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED);
|
|
} else {
|
|
if (!Http.hasWebView()) {
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setProgressCompat(PRECISION, true);
|
|
progressIndicator.setVisibility(View.GONE);
|
|
searchView.setEnabled(false);
|
|
updateScreenInsets(getResources().getConfiguration());
|
|
});
|
|
return;
|
|
}
|
|
// Compatibility data still needs to be updated
|
|
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
|
|
if (BuildConfig.DEBUG) Timber.i("Check App Update");
|
|
if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
|
|
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
|
|
if (BuildConfig.DEBUG) Timber.i("Check Json Update");
|
|
if (max != 0) {
|
|
int current = 0;
|
|
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
|
|
// if it has updateJson and FLAG_MM_REMOTE_MODULE is not set on flags, check for json update
|
|
// this is a dirty hack until we better store if it's a remote module
|
|
// the reasoning is that remote repos are considered "validated" while local modules are not
|
|
// for instance, a potential attacker could hijack a perfectly legitimate module and inject an updateJson with a malicious update - thereby bypassing any checks repos may have, without anyone noticing until it's too late
|
|
if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
|
|
if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
|
|
try {
|
|
localModuleInfo.checkModuleUpdate();
|
|
} catch (Exception e) {
|
|
Timber.e(e);
|
|
}
|
|
current++;
|
|
final int currentTmp = current;
|
|
runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setProgressCompat(PRECISION, true);
|
|
progressIndicator.setVisibility(View.GONE);
|
|
searchView.setEnabled(true);
|
|
updateScreenInsets(getResources().getConfiguration());
|
|
});
|
|
if (BuildConfig.DEBUG) Timber.i("Apply");
|
|
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
|
|
// logic to handle updateable modules
|
|
moduleViewListBuilder.applyTo(moduleListOnline, moduleViewAdapterOnline);
|
|
moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
|
|
// if moduleViewListBuilderOnline has the upgradeable notification, show a badge on the online repo nav item
|
|
if (MainApplication.getINSTANCE().modulesHaveUpdates) {
|
|
Timber.i("Applying badge");
|
|
new Handler(Looper.getMainLooper()).post(() -> {
|
|
final var badge = bottomNavigationView.getOrCreateBadge(R.id.online_menu_item);
|
|
badge.setVisible(true);
|
|
badge.setNumber(MainApplication.getINSTANCE().updateModuleCount);
|
|
badge.applyTheme(MainApplication.getInitialApplication().getTheme());
|
|
Timber.i("Badge applied");
|
|
});
|
|
}
|
|
Timber.i("Finished app opening state!");
|
|
}
|
|
}, true);
|
|
// if system lang is not in MainApplication.supportedLocales, show a snackbar to ask user to help translate
|
|
if (!MainApplication.supportedLocales.contains(this.getResources().getConfiguration().getLocales().get(0).getLanguage())) {
|
|
// call showWeblateSnackbar() with language code and language name
|
|
showWeblateSnackbar(this.getResources().getConfiguration().getLocales().get(0).getLanguage(), this.getResources().getConfiguration().getLocales().get(0).getDisplayLanguage());
|
|
}
|
|
ExternalHelper.INSTANCE.refreshHelper(this);
|
|
this.initMode = false;
|
|
// add preference listener to set isMatomoAllowed
|
|
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> {
|
|
if (key.equals("pref_analytics_enabled")) {
|
|
MainApplication.getINSTANCE().isMatomoAllowed = sharedPreferences.getBoolean(key, false);
|
|
MainApplication.getINSTANCE().getTracker().setOptOut(MainApplication.getINSTANCE().isMatomoAllowed);
|
|
Timber.d("Matomo is allowed change: %s", MainApplication.getINSTANCE().isMatomoAllowed);
|
|
}
|
|
if (MainApplication.getINSTANCE().isMatomoAllowed) {
|
|
String value = sharedPreferences.getString(key, null);
|
|
// then log
|
|
if (value != null) {
|
|
TrackHelper.track().event("pref_changed", key + "=" + value).with(MainApplication.getINSTANCE().getTracker());
|
|
}
|
|
}
|
|
Timber.d("Preference changed: %s", key);
|
|
};
|
|
MainApplication.getSharedPreferences("mmm").registerOnSharedPreferenceChangeListener(listener);
|
|
}
|
|
|
|
private void cardIconifyUpdate() {
|
|
boolean iconified = this.searchView.isIconified();
|
|
int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? com.google.android.material.R.attr.colorSecondaryContainer : // Monet is special...
|
|
com.google.android.material.R.attr.colorSecondary : com.google.android.material.R.attr.colorPrimarySurface;
|
|
Resources.Theme theme = this.searchCard.getContext().getTheme();
|
|
TypedValue value = new TypedValue();
|
|
theme.resolveAttribute(backgroundAttr, value, true);
|
|
this.searchCard.setCardBackgroundColor(value.data);
|
|
this.searchCard.setAlpha(iconified ? 0.80F : 1F);
|
|
}
|
|
|
|
private void updateScreenInsets() {
|
|
this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration()));
|
|
}
|
|
|
|
private void updateScreenInsets(Configuration configuration) {
|
|
boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
|
|
int bottomInset = (landscape ? 0 : this.getNavigationBarHeight());
|
|
int statusBarHeight = getStatusBarHeight() + FoxDisplay.dpToPixel(2);
|
|
this.swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayoutOrigStartOffset + statusBarHeight, swipeRefreshLayoutOrigEndOffset + statusBarHeight);
|
|
this.moduleViewListBuilder.setHeaderPx(statusBarHeight);
|
|
this.moduleViewListBuilderOnline.setHeaderPx(statusBarHeight);
|
|
this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight());
|
|
this.moduleViewListBuilderOnline.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight());
|
|
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
|
|
this.moduleViewListBuilder.updateInsets();
|
|
//this.actionBarBlur.invalidate();
|
|
this.overScrollInsetTop = statusBarHeight;
|
|
this.overScrollInsetBottom = bottomInset;
|
|
// set root_container to have zero padding
|
|
findViewById(R.id.root_container).setPadding(0, statusBarHeight, 0, 0);
|
|
}
|
|
|
|
private void updateBlurState() {
|
|
if (MainApplication.isBlurEnabled()) {
|
|
// set bottom navigation bar color to transparent blur
|
|
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
|
|
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
|
|
bottomNavigationView.setAlpha(0.8F);
|
|
// set dialogs to have transparent blur
|
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void refreshUI() {
|
|
super.refreshUI();
|
|
if (this.initMode) return;
|
|
this.initMode = true;
|
|
Timber.i("Item Before");
|
|
this.searchView.setQuery("", false);
|
|
this.searchView.clearFocus();
|
|
this.searchView.setIconified(true);
|
|
this.cardIconifyUpdate();
|
|
this.updateScreenInsets();
|
|
this.updateBlurState();
|
|
this.moduleViewListBuilder.setQuery(null);
|
|
Timber.i("Item After");
|
|
this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter);
|
|
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
|
|
@Override
|
|
public void onPathReceived(String path) {
|
|
checkShowInitialSetup();
|
|
// Wait for doSetupNow to finish
|
|
while (doSetupNowRunning) {
|
|
try {
|
|
//noinspection BusyWait
|
|
Thread.sleep(100);
|
|
} catch (InterruptedException ignored) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
|
|
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
|
|
if (!MainApplication.isShowcaseMode())
|
|
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
|
|
ModuleManager.getINSTANCE().scan();
|
|
ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
|
|
this.commonNext();
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(int error) {
|
|
Timber.e("Error: %s", error);
|
|
moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
|
|
moduleViewListBuilderOnline.addNotification(InstallerInitializer.getErrorNotification());
|
|
this.commonNext();
|
|
}
|
|
|
|
public void commonNext() {
|
|
Timber.i("Common Before");
|
|
if (MainApplication.isShowcaseMode())
|
|
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
|
|
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilderOnline);
|
|
NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline);
|
|
if (AppUpdateManager.getAppUpdateManager().checkUpdate(false))
|
|
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
|
|
RepoManager.getINSTANCE().updateEnabledStates();
|
|
if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setIndeterminate(false);
|
|
progressIndicator.setMax(PRECISION);
|
|
});
|
|
if (BuildConfig.DEBUG) Timber.i("Check Update");
|
|
RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
|
|
runOnUiThread(() -> {
|
|
progressIndicator.setProgressCompat(PRECISION, true);
|
|
progressIndicator.setVisibility(View.GONE);
|
|
});
|
|
}
|
|
if (BuildConfig.DEBUG) Timber.i("Apply");
|
|
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
|
|
Timber.i("Common Before applyTo");
|
|
moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
|
|
Timber.i("Common After");
|
|
}
|
|
});
|
|
this.initMode = false;
|
|
}
|
|
|
|
@Override
|
|
protected void onWindowUpdated() {
|
|
this.updateScreenInsets();
|
|
}
|
|
|
|
@Override
|
|
public void onRefresh() {
|
|
if (this.swipeRefreshBlocker > System.currentTimeMillis() || this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE || doSetupNowRunning) {
|
|
this.swipeRefreshLayout.setRefreshing(false);
|
|
return; // Do not double scan
|
|
}
|
|
if (BuildConfig.DEBUG) Timber.i("Refresh");
|
|
this.progressIndicator.setVisibility(View.VISIBLE);
|
|
this.progressIndicator.setProgressCompat(0, false);
|
|
this.swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
|
|
// this.swipeRefreshLayout.setRefreshing(true); ??
|
|
new Thread(() -> {
|
|
Http.cleanDnsCache(); // Allow DNS reload from network
|
|
final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
|
|
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
|
|
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
|
|
if (!NotificationType.NO_INTERNET.shouldRemove()) {
|
|
moduleViewListBuilderOnline.addNotification(NotificationType.NO_INTERNET);
|
|
} else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) {
|
|
moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED);
|
|
} else {
|
|
// Compatibility data still needs to be updated
|
|
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
|
|
if (BuildConfig.DEBUG) Timber.i("Check App Update");
|
|
if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
|
|
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
|
|
if (BuildConfig.DEBUG) Timber.i("Check Json Update");
|
|
if (max != 0) {
|
|
int current = 0;
|
|
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
|
|
if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
|
|
if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
|
|
try {
|
|
localModuleInfo.checkModuleUpdate();
|
|
} catch (Exception e) {
|
|
Timber.e(e);
|
|
}
|
|
current++;
|
|
final int currentTmp = current;
|
|
runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (BuildConfig.DEBUG) Timber.i("Apply");
|
|
runOnUiThread(() -> {
|
|
this.progressIndicator.setVisibility(View.GONE);
|
|
this.swipeRefreshLayout.setRefreshing(false);
|
|
});
|
|
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
|
|
RepoManager.getINSTANCE().updateEnabledStates();
|
|
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
|
|
this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
|
|
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
|
|
}, "Repo update thread").start();
|
|
}
|
|
|
|
@Override
|
|
public boolean onQueryTextSubmit(final String query) {
|
|
this.searchView.clearFocus();
|
|
if (this.initMode) return false;
|
|
TrackHelper.track().search(query).with(MainApplication.getINSTANCE().getTracker());
|
|
if (this.moduleViewListBuilder.setQueryChange(query)) {
|
|
Timber.i("Query submit: %s on offline list", query);
|
|
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
|
|
}
|
|
// same for online list
|
|
if (this.moduleViewListBuilderOnline.setQueryChange(query)) {
|
|
Timber.i("Query submit: %s on online list", query);
|
|
new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onQueryTextChange(String query) {
|
|
if (this.initMode) return false;
|
|
TrackHelper.track().search(query).with(MainApplication.getINSTANCE().getTracker());
|
|
if (this.moduleViewListBuilder.setQueryChange(query)) {
|
|
Timber.i("Query submit: %s on offline list", query);
|
|
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
|
|
}
|
|
// same for online list
|
|
if (this.moduleViewListBuilderOnline.setQueryChange(query)) {
|
|
Timber.i("Query submit: %s on online list", query);
|
|
new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onClose() {
|
|
if (this.initMode) return false;
|
|
if (this.moduleViewListBuilder.setQueryChange(null)) {
|
|
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
|
|
}
|
|
// same for online list
|
|
if (this.moduleViewListBuilderOnline.setQueryChange(null)) {
|
|
new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public int getOverScrollInsetTop() {
|
|
return this.overScrollInsetTop;
|
|
}
|
|
|
|
@Override
|
|
public int getOverScrollInsetBottom() {
|
|
return this.overScrollInsetBottom;
|
|
}
|
|
|
|
@Override
|
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
|
super.onConfigurationChanged(newConfig);
|
|
this.updateScreenInsets();
|
|
}
|
|
|
|
@Override
|
|
public void onWindowFocusChanged(boolean hasFocus) {
|
|
super.onWindowFocusChanged(hasFocus);
|
|
this.updateScreenInsets();
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
private void ensurePermissions() {
|
|
if (BuildConfig.DEBUG) Timber.i("Ensure Permissions");
|
|
// First, check if user has said don't ask again by checking if pref_dont_ask_again_notification_permission is true
|
|
if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("pref_dont_ask_again_notification_permission", false)) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
if (BuildConfig.DEBUG) Timber.i("Request Notification Permission");
|
|
if (FoxActivity.getFoxActivity(this).shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
|
|
// Show a dialog explaining why we need this permission, which is to show
|
|
// notifications for updates
|
|
runOnUiThread(() -> {
|
|
if (BuildConfig.DEBUG) Timber.i("Show Notification Permission Dialog");
|
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
|
builder.setTitle(R.string.permission_notification_title);
|
|
builder.setMessage(R.string.permission_notification_message);
|
|
// Don't ask again checkbox
|
|
View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
|
|
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
|
checkBox.setText(R.string.dont_ask_again);
|
|
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
|
|
builder.setView(view);
|
|
builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
|
|
// Request the permission
|
|
this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
|
|
doSetupNowRunning = false;
|
|
});
|
|
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
|
// Set pref_background_update_check to false and dismiss dialog
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
prefs.edit().putBoolean("pref_background_update_check", false).apply();
|
|
dialog.dismiss();
|
|
doSetupNowRunning = false;
|
|
});
|
|
builder.show();
|
|
if (BuildConfig.DEBUG) Timber.i("Show Notification Permission Dialog Done");
|
|
});
|
|
} else {
|
|
// Request the permission
|
|
if (BuildConfig.DEBUG) Timber.i("Request Notification Permission");
|
|
this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
|
|
if (BuildConfig.DEBUG) {
|
|
// Log if granted via onRequestPermissionsResult
|
|
boolean granted = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
|
|
Timber.i("Request Notification Permission Done. Result: %s", granted);
|
|
}
|
|
doSetupNowRunning = false;
|
|
}
|
|
// Next branch is for < android 13 and user has blocked notifications
|
|
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(this).areNotificationsEnabled()) {
|
|
runOnUiThread(() -> {
|
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
|
builder.setTitle(R.string.permission_notification_title);
|
|
builder.setMessage(R.string.permission_notification_message);
|
|
// Don't ask again checkbox
|
|
View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
|
|
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
|
checkBox.setText(R.string.dont_ask_again);
|
|
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
|
|
builder.setView(view);
|
|
builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
|
|
// Open notification settings
|
|
Intent intent = new Intent();
|
|
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
|
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
|
intent.setData(uri);
|
|
startActivity(intent);
|
|
doSetupNowRunning = false;
|
|
});
|
|
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
|
// Set pref_background_update_check to false and dismiss dialog
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
prefs.edit().putBoolean("pref_background_update_check", false).apply();
|
|
dialog.dismiss();
|
|
doSetupNowRunning = false;
|
|
});
|
|
builder.show();
|
|
});
|
|
} else {
|
|
doSetupNowRunning = false;
|
|
}
|
|
} else {
|
|
if (BuildConfig.DEBUG)
|
|
Timber.i("Notification Permission Already Granted or Don't Ask Again");
|
|
doSetupNowRunning = false;
|
|
}
|
|
}
|
|
|
|
// Method to show a setup box on first launch
|
|
@SuppressLint({"InflateParams", "RestrictedApi", "UnspecifiedImmutableFlag", "ApplySharedPref"})
|
|
private void checkShowInitialSetup() {
|
|
if (BuildConfig.DEBUG) Timber.i("Checking if we need to run setup");
|
|
// Check if this is the first launch using prefs and if doSetupRestarting was passed in the intent
|
|
SharedPreferences prefs = MainApplication.getSharedPreferences("mmm");
|
|
boolean firstLaunch = !Objects.equals(prefs.getString("last_shown_setup", null), "v1");
|
|
// First launch
|
|
// this is intentionally separate from the above if statement, because it needs to be checked even if the first launch check is true due to some weird edge cases
|
|
if (getIntent().getBooleanExtra("doSetupRestarting", false)) {
|
|
// Restarting setup
|
|
firstLaunch = false;
|
|
}
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.i("First launch: %s, pref value: %s", firstLaunch, prefs.getString("last_shown_setup", null));
|
|
}
|
|
if (firstLaunch) {
|
|
doSetupNowRunning = true;
|
|
// Launch setup wizard
|
|
if (BuildConfig.DEBUG) Timber.i("Launching setup wizard");
|
|
// Show setup activity
|
|
Intent intent = new Intent(this, SetupActivity.class);
|
|
finish();
|
|
startActivity(intent);
|
|
} else {
|
|
ensurePermissions();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if the load workflow must be stopped.
|
|
*/
|
|
private boolean waitInitialSetupFinished() {
|
|
if (BuildConfig.DEBUG) Timber.i("waitInitialSetupFinished");
|
|
if (doSetupNowRunning) updateScreenInsets(); // Fix an edge case
|
|
try {
|
|
// Wait for doSetupNow to finish
|
|
while (doSetupNowRunning) {
|
|
//noinspection BusyWait
|
|
Thread.sleep(50);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
return true;
|
|
}
|
|
return doSetupRestarting;
|
|
}
|
|
|
|
/**
|
|
* Shows a snackbar offering to take users to Weblate if their language is not available.
|
|
*
|
|
* @param language The language code.
|
|
* @param languageName The language name.
|
|
*/
|
|
@SuppressLint("RestrictedApi")
|
|
private void showWeblateSnackbar(String language, String languageName) {
|
|
Snackbar snackbar = Snackbar.make(findViewById(R.id.root_container), getString(R.string.language_not_available, languageName), Snackbar.LENGTH_LONG);
|
|
snackbar.setAction(R.string.ok, v -> {
|
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
intent.setData(Uri.parse("https://translate.nift4.org/engage/foxmmm/?language=" + language));
|
|
startActivity(intent);
|
|
});
|
|
snackbar.show();
|
|
}
|
|
} |