diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..0c1a13c1e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +.idea/ +/build +app/release +*.hprof +.externalNativeBuild/ +src/full/res/raw/util_functions.sh +public.certificate.x509.pem +private.key.pk8 +*.apk diff --git a/app/README.md b/app/README.md new file mode 100644 index 000000000..19a1f6142 --- /dev/null +++ b/app/README.md @@ -0,0 +1,7 @@ +# Magisk Manager +This repo is no longer an independent component. It is a submodule of the [Magisk Project](https://github.com/topjohnwu/Magisk). + +# Translations +The default (English) string resources are scattered in these files: `src/full/res/values/strings.xml`, `src/main/res/values/strings.xml`, `src/stub/res/values/strings.xml`. +Place the translated XMLs in the corresponding folder to the locale. +Translations are highly appreciated via pull requests here on Github. diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..7621a2f68 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,65 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + applicationId "com.topjohnwu.magisk" + minSdkVersion 21 + targetSdkVersion rootProject.ext.compileSdkVersion + javaCompileOptions { + annotationProcessorOptions { + argument('butterknife.debuggable', 'false') + } + } + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + flavorDimensions "mode" + + productFlavors { + full { + versionCode 127 + versionName "5.8.1" + } + stub { + versionCode 1 + versionName "stub" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + dexOptions { + preDexLibraries true + javaMaxHeapSize "2g" + } + lintOptions { + disable 'MissingTranslation' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + fullImplementation project(':utils') + implementation "com.android.support:support-core-utils:${rootProject.ext.supportLibVersion}" + fullImplementation "com.android.support:preference-v7:${rootProject.ext.supportLibVersion}" + fullImplementation "com.android.support:recyclerview-v7:${rootProject.ext.supportLibVersion}" + fullImplementation "com.android.support:cardview-v7:${rootProject.ext.supportLibVersion}" + fullImplementation "com.android.support:design:${rootProject.ext.supportLibVersion}" + fullImplementation 'com.github.topjohnwu:libsu:1.3.0' + fullImplementation 'com.atlassian.commonmark:commonmark:0.11.0' + fullImplementation 'org.kamranzafar:jtar:2.3' + fullImplementation 'com.jakewharton:butterknife:8.8.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..6002ebcc6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,29 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Keep all names, we are open source anyway :) +-keepnames class ** { *; } + +# BouncyCastle +-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; } +-keep class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; } +-keep class org.bouncycastle.jcajce.provider.digest.SHA1** { *; } +-dontwarn javax.naming.** + +# Gson +-keepattributes Signature \ No newline at end of file diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml new file mode 100644 index 000000000..089a603e0 --- /dev/null +++ b/app/src/full/AndroidManifest.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/java/com/topjohnwu/magisk/AboutActivity.java b/app/src/full/java/com/topjohnwu/magisk/AboutActivity.java new file mode 100644 index 000000000..b32f703b6 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/AboutActivity.java @@ -0,0 +1,80 @@ +package com.topjohnwu.magisk; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.View; + +import com.topjohnwu.magisk.asyncs.MarkDownWindow; +import com.topjohnwu.magisk.components.AboutCardRow; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.utils.Const; + +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class AboutActivity extends Activity { + + @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.app_version_info) AboutCardRow appVersionInfo; + @BindView(R.id.app_changelog) AboutCardRow appChangelog; + @BindView(R.id.app_translators) AboutCardRow appTranslators; + @BindView(R.id.app_source_code) AboutCardRow appSourceCode; + @BindView(R.id.support_thread) AboutCardRow supportThread; + @BindView(R.id.donation) AboutCardRow donation; + + @Override + public int getDarkTheme() { + return R.style.AppTheme_StatusBar_Dark; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + ButterKnife.bind(this); + + setSupportActionBar(toolbar); + toolbar.setNavigationOnClickListener(view -> finish()); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.about); + ab.setDisplayHomeAsUpEnabled(true); + } + + appVersionInfo.setSummary(String.format(Locale.US, "%s (%d) (%s)", + BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getPackageName())); + + appChangelog.removeSummary(); + appChangelog.setOnClickListener(v -> { + new MarkDownWindow(this, getString(R.string.app_changelog), + getResources().openRawResource(R.raw.changelog)).exec(); + }); + + String translators = getString(R.string.translators); + if (TextUtils.isEmpty(translators)) { + appTranslators.setVisibility(View.GONE); + } else { + appTranslators.setSummary(translators); + } + + appSourceCode.removeSummary(); + appSourceCode.setOnClickListener(view -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Const.Url.SOURCE_CODE_URL)))); + + supportThread.removeSummary(); + supportThread.setOnClickListener(view -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Const.Url.XDA_THREAD)))); + + donation.removeSummary(); + donation.setOnClickListener(view -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Const.Url.DONATION_URL)))); + + setFloating(); + } + +} diff --git a/app/src/full/java/com/topjohnwu/magisk/FlashActivity.java b/app/src/full/java/com/topjohnwu/magisk/FlashActivity.java new file mode 100644 index 000000000..8c892d724 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/FlashActivity.java @@ -0,0 +1,155 @@ +package com.topjohnwu.magisk; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import com.topjohnwu.magisk.asyncs.FlashZip; +import com.topjohnwu.magisk.asyncs.InstallMagisk; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.RootUtils; +import com.topjohnwu.superuser.CallbackList; +import com.topjohnwu.superuser.Shell; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +public class FlashActivity extends Activity { + + @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.txtLog) TextView flashLogs; + @BindView(R.id.button_panel) public LinearLayout buttonPanel; + @BindView(R.id.reboot) public Button reboot; + @BindView(R.id.scrollView) ScrollView sv; + + private List logs; + + @OnClick(R.id.no_thanks) + void dismiss() { + finish(); + } + + @OnClick(R.id.reboot) + void reboot() { + Shell.Async.su("/system/bin/reboot"); + } + + @OnClick(R.id.save_logs) + void saveLogs() { + Calendar now = Calendar.getInstance(); + String filename = String.format(Locale.US, + "install_log_%04d%02d%02d_%02d%02d%02d.log", + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, + now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), + now.get(Calendar.MINUTE), now.get(Calendar.SECOND)); + + File logFile = new File(Const.EXTERNAL_PATH + "/logs", filename); + logFile.getParentFile().mkdirs(); + try (FileWriter writer = new FileWriter(logFile)) { + for (String s : logs) { + writer.write(s); + writer.write('\n'); + } + } catch (IOException e) { + e.printStackTrace(); + return; + } + MagiskManager.toast(logFile.getPath(), Toast.LENGTH_LONG); + } + + @Override + public int getDarkTheme() { + return R.style.AppTheme_StatusBar_Dark; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flash); + ButterKnife.bind(this); + setSupportActionBar(toolbar); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.flashing); + } + setFloating(); + setFinishOnTouchOutside(false); + if (!Shell.rootAccess()) + reboot.setVisibility(View.GONE); + + logs = new ArrayList<>(); + CallbackList console = new CallbackList(new ArrayList<>()) { + @Override + public void onAddElement(String s) { + logs.add(s); + flashLogs.setText(TextUtils.join("\n", this)); + sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10); + } + }; + + // We must receive a Uri of the target zip + Intent intent = getIntent(); + Uri uri = intent.getData(); + + switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) { + case Const.Value.FLASH_ZIP: + new FlashZip(this, uri, console, logs).exec(); + break; + case Const.Value.UNINSTALL: + new UninstallMagisk(this, uri, console, logs).exec(); + break; + case Const.Value.FLASH_MAGISK: + new InstallMagisk(this, console, logs, uri, InstallMagisk.DIRECT_MODE).exec(); + break; + case Const.Value.FLASH_SECOND_SLOT: + new InstallMagisk(this, console, logs, uri, InstallMagisk.SECOND_SLOT_MODE).exec(); + break; + case Const.Value.PATCH_BOOT: + new InstallMagisk(this, console, logs, uri, + intent.getParcelableExtra(Const.Key.FLASH_SET_BOOT)).exec(); + break; + } + } + + @Override + public void onBackPressed() { + // Prevent user accidentally press back button + } + + private static class UninstallMagisk extends FlashZip { + + private UninstallMagisk(Activity context, Uri uri, List console, List logs) { + super(context, uri, console, logs); + } + + @Override + protected void onPostExecute(Integer result) { + if (result == 1) { + new Handler().postDelayed(() -> + RootUtils.uninstallPkg(getActivity().getPackageName()), 3000); + } else { + super.onPostExecute(result); + } + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/LogFragment.java b/app/src/full/java/com/topjohnwu/magisk/LogFragment.java new file mode 100644 index 000000000..e61e275e1 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/LogFragment.java @@ -0,0 +1,55 @@ +package com.topjohnwu.magisk; + + +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.view.ViewPager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.topjohnwu.magisk.adapters.TabFragmentAdapter; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.utils.Const; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class LogFragment extends Fragment { + + private Unbinder unbinder; + + @BindView(R.id.container) ViewPager viewPager; + @BindView(R.id.tab) TabLayout tab; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View v = inflater.inflate(R.layout.fragment_log, container, false); + unbinder = ButterKnife.bind(this, v); + + ((MainActivity) getActivity()).toolbar.setElevation(0); + + TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager()); + + if (!(Const.USER_ID > 0 && getApplication().multiuserMode == Const.Value.MULTIUSER_MODE_OWNER_MANAGED)) { + adapter.addTab(new SuLogFragment(), getString(R.string.superuser)); + } + adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk)); + tab.setupWithViewPager(viewPager); + tab.setVisibility(View.VISIBLE); + + viewPager.setAdapter(adapter); + + return v; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + +} diff --git a/app/src/full/java/com/topjohnwu/magisk/MagiskFragment.java b/app/src/full/java/com/topjohnwu/magisk/MagiskFragment.java new file mode 100644 index 000000000..46567a9f2 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/MagiskFragment.java @@ -0,0 +1,313 @@ +package com.topjohnwu.magisk; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.CardView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.topjohnwu.magisk.asyncs.CheckSafetyNet; +import com.topjohnwu.magisk.asyncs.CheckUpdates; +import com.topjohnwu.magisk.components.AlertDialogBuilder; +import com.topjohnwu.magisk.components.ExpandableView; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.ISafetyNetHelper; +import com.topjohnwu.magisk.utils.ShowUI; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import butterknife.BindColor; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; + +public class MagiskFragment extends Fragment + implements Topic.Subscriber, SwipeRefreshLayout.OnRefreshListener, ExpandableView { + + private Container expandableContainer = new Container(); + + private MagiskManager mm; + private Unbinder unbinder; + private static boolean shownDialog = false; + + @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; + + @BindView(R.id.magisk_update) RelativeLayout magiskUpdate; + @BindView(R.id.magisk_update_icon) ImageView magiskUpdateIcon; + @BindView(R.id.magisk_update_status) TextView magiskUpdateText; + @BindView(R.id.magisk_update_progress) ProgressBar magiskUpdateProgress; + @BindView(R.id.magisk_status_icon) ImageView magiskStatusIcon; + @BindView(R.id.magisk_version) TextView magiskVersionText; + + @BindView(R.id.safetyNet_card) CardView safetyNetCard; + @BindView(R.id.safetyNet_refresh) ImageView safetyNetRefreshIcon; + @BindView(R.id.safetyNet_status) TextView safetyNetStatusText; + @BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress; + @BindView(R.id.expand_layout) LinearLayout expandLayout; + @BindView(R.id.cts_status_icon) ImageView ctsStatusIcon; + @BindView(R.id.cts_status) TextView ctsStatusText; + @BindView(R.id.basic_status_icon) ImageView basicStatusIcon; + @BindView(R.id.basic_status) TextView basicStatusText; + + @BindView(R.id.install_option_card) CardView installOptionCard; + @BindView(R.id.keep_force_enc) CheckBox keepEncChkbox; + @BindView(R.id.keep_verity) CheckBox keepVerityChkbox; + @BindView(R.id.install_button) CardView installButton; + @BindView(R.id.install_text) TextView installText; + @BindView(R.id.uninstall_button) CardView uninstallButton; + + @BindColor(R.color.red500) int colorBad; + @BindColor(R.color.green500) int colorOK; + @BindColor(R.color.yellow500) int colorWarn; + @BindColor(R.color.grey500) int colorNeutral; + @BindColor(R.color.blue500) int colorInfo; + + @OnClick(R.id.safetyNet_title) + void safetyNet() { + Runnable task = () -> { + safetyNetProgress.setVisibility(View.VISIBLE); + safetyNetRefreshIcon.setVisibility(View.GONE); + safetyNetStatusText.setText(R.string.checking_safetyNet_status); + new CheckSafetyNet(getActivity()).exec(); + collapse(); + }; + if (!TextUtils.equals(mm.getPackageName(), Const.ORIG_PKG_NAME)) { + new AlertDialogBuilder(getActivity()) + .setTitle(R.string.cannot_check_sn_title) + .setMessage(R.string.cannot_check_sn_notice) + .setCancelable(true) + .setPositiveButton(R.string.ok, null) + .show(); + } else if (!CheckSafetyNet.dexPath.exists()) { + // Show dialog + new AlertDialogBuilder(getActivity()) + .setTitle(R.string.proprietary_title) + .setMessage(R.string.proprietary_notice) + .setCancelable(true) + .setPositiveButton(R.string.yes, (d, i) -> task.run()) + .setNegativeButton(R.string.no_thanks, null) + .show(); + } else { + task.run(); + } + + } + + @OnClick(R.id.install_button) + void install() { + shownDialog = true; + + // Show Manager update first + if (mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE) { + ShowUI.managerInstallDialog(getActivity()); + return; + } + + ((NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll(); + ShowUI.magiskInstallDialog(getActivity()); + } + + @OnClick(R.id.uninstall_button) + void uninstall() { + ShowUI.uninstallDialog(getActivity()); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_magisk, container, false); + unbinder = ButterKnife.bind(this, v); + getActivity().setTitle(R.string.magisk); + + mm = getApplication(); + + expandableContainer.expandLayout = expandLayout; + setupExpandable(); + + keepVerityChkbox.setChecked(mm.keepVerity); + keepVerityChkbox.setOnCheckedChangeListener((view, checked) -> mm.keepVerity = checked); + keepEncChkbox.setChecked(mm.keepEnc); + keepEncChkbox.setOnCheckedChangeListener((view, checked) -> mm.keepEnc = checked); + + mSwipeRefreshLayout.setOnRefreshListener(this); + updateUI(); + + return v; + } + + @Override + public void onRefresh() { + mm.loadMagiskInfo(); + updateUI(); + + magiskUpdateText.setText(R.string.checking_for_updates); + magiskUpdateProgress.setVisibility(View.VISIBLE); + magiskUpdateIcon.setVisibility(View.GONE); + + safetyNetStatusText.setText(R.string.safetyNet_check_text); + + mm.safetyNetDone.reset(); + mm.updateCheckDone.reset(); + mm.remoteMagiskVersionString = null; + mm.remoteMagiskVersionCode = -1; + collapse(); + + shownDialog = false; + + // Trigger state check + if (Utils.checkNetworkStatus()) { + new CheckUpdates().exec(); + } else { + mSwipeRefreshLayout.setRefreshing(false); + } + } + + @Override + public void onTopicPublished(Topic topic) { + if (topic == mm.updateCheckDone) { + updateCheckUI(); + } else if (topic == mm.safetyNetDone) { + updateSafetyNetUI((int) topic.getResults()[0]); + } + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { mm.updateCheckDone, mm.safetyNetDone }; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @Override + public Container getContainer() { + return expandableContainer; + } + + private void updateUI() { + ((MainActivity) getActivity()).checkHideSection(); + + boolean hasNetwork = Utils.checkNetworkStatus(); + boolean hasRoot = Shell.rootAccess(); + boolean isUpToDate = mm.magiskVersionCode > Const.MAGISK_VER.UNIFIED; + + magiskUpdate.setVisibility(hasNetwork ? View.VISIBLE : View.GONE); + safetyNetCard.setVisibility(hasNetwork ? View.VISIBLE : View.GONE); + installOptionCard.setVisibility(hasNetwork ? View.VISIBLE : View.GONE); + uninstallButton.setVisibility(isUpToDate && hasRoot ? View.VISIBLE : View.GONE); + + int image, color; + + if (mm.magiskVersionCode < 0) { + color = colorBad; + image = R.drawable.ic_cancel; + magiskVersionText.setText(R.string.magisk_version_error); + } else { + color = colorOK; + image = R.drawable.ic_check_circle; + magiskVersionText.setText(getString(R.string.current_magisk_title, "v" + mm.magiskVersionString)); + } + + magiskStatusIcon.setImageResource(image); + magiskStatusIcon.setColorFilter(color); + } + + private void updateCheckUI() { + int image, color; + + if (mm.remoteMagiskVersionCode < 0) { + color = colorNeutral; + image = R.drawable.ic_help; + magiskUpdateText.setText(R.string.invalid_update_channel); + installButton.setVisibility(View.GONE); + } else { + color = colorOK; + image = R.drawable.ic_check_circle; + magiskUpdateText.setText(getString(R.string.install_magisk_title, "v" + mm.remoteMagiskVersionString)); + installButton.setVisibility(View.VISIBLE); + if (mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE) { + installText.setText(getString(R.string.update, getString(R.string.app_name))); + } else if (mm.magiskVersionCode > 0 && mm.remoteMagiskVersionCode > mm.magiskVersionCode) { + installText.setText(getString(R.string.update, getString(R.string.magisk))); + } else { + installText.setText(R.string.install); + } + } + + magiskUpdateIcon.setImageResource(image); + magiskUpdateIcon.setColorFilter(color); + magiskUpdateIcon.setVisibility(View.VISIBLE); + + magiskUpdateProgress.setVisibility(View.GONE); + mSwipeRefreshLayout.setRefreshing(false); + + if (!shownDialog) { + if (mm.remoteMagiskVersionCode > mm.magiskVersionCode + || mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE) { + install(); + } else if (mm.remoteMagiskVersionCode >= Const.MAGISK_VER.FIX_ENV && + !ShellUtils.fastCmdResult("env_check")) { + ShowUI.envFixDialog(getActivity()); + } + } + } + + private void updateSafetyNetUI(int response) { + safetyNetProgress.setVisibility(View.GONE); + safetyNetRefreshIcon.setVisibility(View.VISIBLE); + if ((response & 0x0F) == 0) { + safetyNetStatusText.setText(R.string.safetyNet_check_success); + + boolean b; + b = (response & ISafetyNetHelper.CTS_PASS) != 0; + ctsStatusText.setText("ctsProfile: " + b); + ctsStatusIcon.setImageResource(b ? R.drawable.ic_check_circle : R.drawable.ic_cancel); + ctsStatusIcon.setColorFilter(b ? colorOK : colorBad); + + b = (response & ISafetyNetHelper.BASIC_PASS) != 0; + basicStatusText.setText("basicIntegrity: " + b); + basicStatusIcon.setImageResource(b ? R.drawable.ic_check_circle : R.drawable.ic_cancel); + basicStatusIcon.setColorFilter(b ? colorOK : colorBad); + + expand(); + } else { + @StringRes int resid; + switch (response) { + case ISafetyNetHelper.CAUSE_SERVICE_DISCONNECTED: + resid = R.string.safetyNet_network_loss; + break; + case ISafetyNetHelper.CAUSE_NETWORK_LOST: + resid = R.string.safetyNet_service_disconnected; + break; + case ISafetyNetHelper.RESPONSE_ERR: + resid = R.string.safetyNet_res_invalid; + break; + case ISafetyNetHelper.CONNECTION_FAIL: + default: + resid = R.string.safetyNet_api_error; + break; + } + safetyNetStatusText.setText(resid); + } + } +} + diff --git a/app/src/full/java/com/topjohnwu/magisk/MagiskHideFragment.java b/app/src/full/java/com/topjohnwu/magisk/MagiskHideFragment.java new file mode 100644 index 000000000..679fe89e0 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/MagiskHideFragment.java @@ -0,0 +1,92 @@ +package com.topjohnwu.magisk; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SearchView; + +import com.topjohnwu.magisk.adapters.ApplicationAdapter; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.utils.Topic; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class MagiskHideFragment extends Fragment implements Topic.Subscriber { + + private Unbinder unbinder; + @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; + @BindView(R.id.recyclerView) RecyclerView recyclerView; + + private ApplicationAdapter appAdapter; + + private SearchView.OnQueryTextListener searchListener; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false); + unbinder = ButterKnife.bind(this, view); + + mSwipeRefreshLayout.setRefreshing(true); + mSwipeRefreshLayout.setOnRefreshListener(() -> appAdapter.refresh()); + + appAdapter = new ApplicationAdapter(); + recyclerView.setAdapter(appAdapter); + + searchListener = new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + appAdapter.filter(query); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + appAdapter.filter(newText); + return false; + } + }; + + getActivity().setTitle(R.string.magiskhide); + + return view; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_magiskhide, menu); + SearchView search = (SearchView) menu.findItem(R.id.app_search).getActionView(); + search.setOnQueryTextListener(searchListener); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @Override + public void onTopicPublished(Topic topic) { + mSwipeRefreshLayout.setRefreshing(false); + appAdapter.filter(null); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { getApplication().magiskHideDone }; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/MagiskLogFragment.java b/app/src/full/java/com/topjohnwu/magisk/MagiskLogFragment.java new file mode 100644 index 000000000..eef4e01c6 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/MagiskLogFragment.java @@ -0,0 +1,145 @@ +package com.topjohnwu.magisk; + +import android.Manifest; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class MagiskLogFragment extends Fragment { + + private Unbinder unbinder; + + @BindView(R.id.txtLog) TextView txtLog; + @BindView(R.id.svLog) ScrollView svLog; + @BindView(R.id.hsvLog) HorizontalScrollView hsvLog; + @BindView(R.id.progressBar) ProgressBar progressBar; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_magisk_log, container, false); + unbinder = ButterKnife.bind(this, view); + setHasOptionsMenu(true); + txtLog.setTextIsSelectable(true); + return view; + } + + @Override + public void onStart() { + super.onStart(); + getActivity().setTitle(R.string.log); + } + + @Override + public void onResume() { + super.onResume(); + readLogs(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_log, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh: + readLogs(); + return true; + case R.id.menu_save: + runWithPermission(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, this::saveLogs); + return true; + case R.id.menu_clear: + clearLogs(); + return true; + default: + return true; + } + } + + public void readLogs() { + Shell.Async.su(new Shell.Async.Callback() { + @Override + public void onTaskResult(@Nullable List out, @Nullable List err) { + progressBar.setVisibility(View.GONE); + if (ShellUtils.isValidOutput(out)) { + txtLog.setText(TextUtils.join("\n", out)); + } else { + txtLog.setText(R.string.log_is_empty); + } + svLog.postDelayed(() -> svLog.fullScroll(ScrollView.FOCUS_DOWN), 100); + hsvLog.postDelayed(() -> hsvLog.fullScroll(ScrollView.FOCUS_LEFT), 100); + } + + @Override + public void onTaskError(@NonNull Throwable throwable) { + txtLog.setText(R.string.log_is_empty); + } + }, "cat " + Const.MAGISK_LOG + " | tail -n 5000"); + } + + public void saveLogs() { + Calendar now = Calendar.getInstance(); + String filename = Utils.fmt("magisk_log_%04d%02d%02d_%02d%02d%02d.log", + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, + now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), + now.get(Calendar.MINUTE), now.get(Calendar.SECOND)); + + File targetFile = new File(Const.EXTERNAL_PATH + "/logs", filename); + targetFile.getParentFile().mkdirs(); + try { + targetFile.createNewFile(); + } catch (IOException e) { + return; + } + Shell.Async.su(new Shell.Async.Callback() { + @Override + public void onTaskResult(@Nullable List out, @Nullable List err) { + SnackbarMaker.make(txtLog, targetFile.getPath(), Snackbar.LENGTH_SHORT).show(); + } + + @Override + public void onTaskError(@NonNull Throwable throwable) {} + }, "cat " + Const.MAGISK_LOG + " > " + targetFile); + } + + public void clearLogs() { + Shell.Async.su("echo -n > " + Const.MAGISK_LOG); + SnackbarMaker.make(txtLog, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/MagiskManager.java b/app/src/full/java/com/topjohnwu/magisk/MagiskManager.java new file mode 100644 index 000000000..2f777a188 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/MagiskManager.java @@ -0,0 +1,292 @@ +package com.topjohnwu.magisk; + +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Xml; + +import com.topjohnwu.magisk.components.Application; +import com.topjohnwu.magisk.container.Module; +import com.topjohnwu.magisk.database.MagiskDatabaseHelper; +import com.topjohnwu.magisk.database.RepoDatabaseHelper; +import com.topjohnwu.magisk.services.UpdateCheckService; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.RootUtils; +import com.topjohnwu.magisk.utils.ShellInitializer; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileInputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class MagiskManager extends Application implements Shell.Container { + + // Topics + public final Topic magiskHideDone = new Topic(); + public final Topic reloadActivity = new Topic(); + public final Topic moduleLoadDone = new Topic(); + public final Topic repoLoadDone = new Topic(); + public final Topic updateCheckDone = new Topic(); + public final Topic safetyNetDone = new Topic(); + public final Topic localeDone = new Topic(); + + // Info + public boolean hasInit = false; + public String magiskVersionString; + public int magiskVersionCode = -1; + public String remoteMagiskVersionString; + public int remoteMagiskVersionCode = -1; + public String remoteManagerVersionString; + public int remoteManagerVersionCode = -1; + + public String magiskLink; + public String magiskNoteLink; + public String managerLink; + public String managerNoteLink; + public String uninstallerLink; + + public boolean keepVerity = false; + public boolean keepEnc = false; + + // Data + public Map moduleMap; + public List locales; + + public boolean magiskHide; + public boolean isDarkTheme; + public int suRequestTimeout; + public int suLogTimeout = 14; + public int suAccessState; + public int multiuserMode; + public int suResponseType; + public int suNotificationType; + public int suNamespaceMode; + public String localeConfig; + public int updateChannel; + public String bootFormat; + public int repoOrder; + + // Global resources + public SharedPreferences prefs; + public MagiskDatabaseHelper mDB; + public RepoDatabaseHelper repoDB; + + private volatile Shell mShell; + + public MagiskManager() { + weakSelf = new WeakReference<>(this); + Shell.setContainer(this); + } + + @Nullable + @Override + public Shell getShell() { + return mShell; + } + + @Override + public void setShell(@Nullable Shell shell) { + mShell = shell; + } + + @Override + public void onCreate() { + super.onCreate(); + + Shell.setFlags(Shell.FLAG_MOUNT_MASTER); + Shell.verboseLogging(BuildConfig.DEBUG); + Shell.setInitializer(ShellInitializer.class); + + prefs = PreferenceManager.getDefaultSharedPreferences(this); + mDB = MagiskDatabaseHelper.getInstance(this); + + String pkg = mDB.getStrings(Const.Key.SU_MANAGER, null); + if (pkg != null && getPackageName().equals(Const.ORIG_PKG_NAME)) { + mDB.setStrings(Const.Key.SU_MANAGER, null); + RootUtils.uninstallPkg(pkg); + } + if (TextUtils.equals(pkg, getPackageName())) { + try { + // We are the manager, remove com.topjohnwu.magisk as it could be malware + getPackageManager().getApplicationInfo(Const.ORIG_PKG_NAME, 0); + RootUtils.uninstallPkg(Const.ORIG_PKG_NAME); + } catch (PackageManager.NameNotFoundException ignored) {} + } + + setLocale(); + loadConfig(); + } + + public static MagiskManager get() { + return (MagiskManager) weakSelf.get(); + } + + public void setLocale() { + localeConfig = prefs.getString(Const.Key.LOCALE, ""); + if (localeConfig.isEmpty()) { + locale = defaultLocale; + } else { + locale = Locale.forLanguageTag(localeConfig); + } + Resources res = getBaseContext().getResources(); + Configuration config = new Configuration(res.getConfiguration()); + config.setLocale(locale); + res.updateConfiguration(config, res.getDisplayMetrics()); + } + + public void loadConfig() { + // su + suRequestTimeout = Utils.getPrefsInt(prefs, Const.Key.SU_REQUEST_TIMEOUT, Const.Value.timeoutList[2]); + suResponseType = Utils.getPrefsInt(prefs, Const.Key.SU_AUTO_RESPONSE, Const.Value.SU_PROMPT); + suNotificationType = Utils.getPrefsInt(prefs, Const.Key.SU_NOTIFICATION, Const.Value.NOTIFICATION_TOAST); + suAccessState = mDB.getSettings(Const.Key.ROOT_ACCESS, Const.Value.ROOT_ACCESS_APPS_AND_ADB); + multiuserMode = mDB.getSettings(Const.Key.SU_MULTIUSER_MODE, Const.Value.MULTIUSER_MODE_OWNER_ONLY); + suNamespaceMode = mDB.getSettings(Const.Key.SU_MNT_NS, Const.Value.NAMESPACE_MODE_REQUESTER); + + // config + isDarkTheme = prefs.getBoolean(Const.Key.DARK_THEME, false); + updateChannel = Utils.getPrefsInt(prefs, Const.Key.UPDATE_CHANNEL, Const.Value.STABLE_CHANNEL); + bootFormat = prefs.getString(Const.Key.BOOT_FORMAT, ".img"); + repoOrder = prefs.getInt(Const.Key.REPO_ORDER, Const.Value.ORDER_NAME); + } + + public void writeConfig() { + prefs.edit() + .putBoolean(Const.Key.DARK_THEME, isDarkTheme) + .putBoolean(Const.Key.MAGISKHIDE, magiskHide) + .putBoolean(Const.Key.HOSTS, Const.MAGISK_HOST_FILE.exists()) + .putBoolean(Const.Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists()) + .putString(Const.Key.SU_REQUEST_TIMEOUT, String.valueOf(suRequestTimeout)) + .putString(Const.Key.SU_AUTO_RESPONSE, String.valueOf(suResponseType)) + .putString(Const.Key.SU_NOTIFICATION, String.valueOf(suNotificationType)) + .putString(Const.Key.ROOT_ACCESS, String.valueOf(suAccessState)) + .putString(Const.Key.SU_MULTIUSER_MODE, String.valueOf(multiuserMode)) + .putString(Const.Key.SU_MNT_NS, String.valueOf(suNamespaceMode)) + .putString(Const.Key.UPDATE_CHANNEL, String.valueOf(updateChannel)) + .putString(Const.Key.LOCALE, localeConfig) + .putString(Const.Key.BOOT_FORMAT, bootFormat) + .putInt(Const.Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER) + .putInt(Const.Key.REPO_ORDER, repoOrder) + .apply(); + } + + public void loadMagiskInfo() { + try { + magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0]; + magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V")); + String s = ShellUtils.fastCmd((magiskVersionCode >= Const.MAGISK_VER.RESETPROP_PERSIST ? + "resetprop -p " : "getprop ") + Const.MAGISKHIDE_PROP); + magiskHide = s == null || Integer.parseInt(s) != 0; + } catch (Exception ignored) {} + } + + public void getDefaultInstallFlags() { + keepVerity = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPVERITY")); + keepEnc = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPFORCEENCRYPT")); + } + + public void setupUpdateCheck() { + JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE); + + if (prefs.getBoolean(Const.Key.CHECK_UPDATES, true)) { + if (scheduler.getAllPendingJobs().isEmpty() || + Const.UPDATE_SERVICE_VER > prefs.getInt(Const.Key.UPDATE_SERVICE_VER, -1)) { + ComponentName service = new ComponentName(this, UpdateCheckService.class); + JobInfo info = new JobInfo.Builder(Const.ID.UPDATE_SERVICE_ID, service) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setPersisted(true) + .setPeriodic(8 * 60 * 60 * 1000) + .build(); + scheduler.schedule(info); + } + } else { + scheduler.cancel(Const.UPDATE_SERVICE_VER); + } + } + + public void dumpPrefs() { + // Flush prefs to disk + prefs.edit().commit(); + File xml = new File(getFilesDir().getParent() + "/shared_prefs", + getPackageName() + "_preferences.xml"); + Shell.Sync.su(Utils.fmt("for usr in /data/user/*; do cat %s > ${usr}/%s; done", xml, Const.MANAGER_CONFIGS)); + } + + public void loadPrefs() { + SuFile config = new SuFile(Utils.fmt("/data/user/%d/%s", Const.USER_ID, Const.MANAGER_CONFIGS)); + if (config.exists()) { + SharedPreferences.Editor editor = prefs.edit(); + try { + SuFileInputStream is = new SuFileInputStream(config); + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(is, "UTF-8"); + parser.nextTag(); + parser.require(XmlPullParser.START_TAG, null, "map"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) + continue; + String key = parser.getAttributeValue(null, "name"); + String value = parser.getAttributeValue(null, "value"); + switch (parser.getName()) { + case "string": + parser.require(XmlPullParser.START_TAG, null, "string"); + editor.putString(key, parser.nextText()); + parser.require(XmlPullParser.END_TAG, null, "string"); + break; + case "boolean": + parser.require(XmlPullParser.START_TAG, null, "boolean"); + editor.putBoolean(key, Boolean.parseBoolean(value)); + parser.nextTag(); + parser.require(XmlPullParser.END_TAG, null, "boolean"); + break; + case "int": + parser.require(XmlPullParser.START_TAG, null, "int"); + editor.putInt(key, Integer.parseInt(value)); + parser.nextTag(); + parser.require(XmlPullParser.END_TAG, null, "int"); + break; + case "long": + parser.require(XmlPullParser.START_TAG, null, "long"); + editor.putLong(key, Long.parseLong(value)); + parser.nextTag(); + parser.require(XmlPullParser.END_TAG, null, "long"); + break; + case "float": + parser.require(XmlPullParser.START_TAG, null, "int"); + editor.putFloat(key, Float.parseFloat(value)); + parser.nextTag(); + parser.require(XmlPullParser.END_TAG, null, "int"); + break; + default: + parser.next(); + } + } + } catch (IOException | XmlPullParserException e) { + e.printStackTrace(); + } + editor.remove(Const.Key.ETAG_KEY); + editor.apply(); + loadConfig(); + config.delete(); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/MainActivity.java b/app/src/full/java/com/topjohnwu/magisk/MainActivity.java new file mode 100644 index 000000000..75fa63866 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/MainActivity.java @@ -0,0 +1,219 @@ +package com.topjohnwu.magisk; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class MainActivity extends Activity + implements NavigationView.OnNavigationItemSelectedListener, Topic.Subscriber { + + private final Handler mDrawerHandler = new Handler(); + private int mDrawerItem; + private boolean fromShortcut = true; + + @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.drawer_layout) DrawerLayout drawer; + @BindView(R.id.nav_view) public NavigationView navigationView; + + private float toolbarElevation; + + @Override + public int getDarkTheme() { + return R.style.AppTheme_Dark; + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + + MagiskManager mm = getMagiskManager(); + + if (!mm.hasInit) { + Intent intent = new Intent(this, SplashActivity.class); + String section = getIntent().getStringExtra(Const.Key.OPEN_SECTION); + if (section != null) { + intent.putExtra(Const.Key.OPEN_SECTION, section); + } + startActivity(intent); + finish(); + } + + String perm = getIntent().getStringExtra(Const.Key.INTENT_PERM); + if (perm != null) { + ActivityCompat.requestPermissions(this, new String[] { perm }, 0); + } + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + + setSupportActionBar(toolbar); + + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed tate + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, 0); // this disables the animation + } + }; + + toolbarElevation = toolbar.getElevation(); + + drawer.addDrawerListener(toggle); + toggle.syncState(); + + if (savedInstanceState == null) + navigate(getIntent().getStringExtra(Const.Key.OPEN_SECTION)); + + navigationView.setNavigationItemSelectedListener(this); + } + + @Override + protected void onResume() { + super.onResume(); + checkHideSection(); + } + + @Override + public void onBackPressed() { + if (drawer.isDrawerOpen(navigationView)) { + drawer.closeDrawer(navigationView); + } else if (mDrawerItem != R.id.magisk && !fromShortcut) { + navigate(R.id.magisk); + } else { + finish(); + } + } + + @Override + public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) { + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250); + drawer.closeDrawer(navigationView); + return true; + } + + @Override + public void onTopicPublished(Topic topic) { + recreate(); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { getMagiskManager().reloadActivity }; + } + + public void checkHideSection() { + MagiskManager mm = getMagiskManager(); + Menu menu = navigationView.getMenu(); + menu.findItem(R.id.magiskhide).setVisible( + Shell.rootAccess() && mm.magiskVersionCode >= Const.MAGISK_VER.UNIFIED + && mm.prefs.getBoolean(Const.Key.MAGISKHIDE, false)); + menu.findItem(R.id.modules).setVisible(!mm.prefs.getBoolean(Const.Key.COREONLY, false) && + Shell.rootAccess() && mm.magiskVersionCode >= 0); + menu.findItem(R.id.downloads).setVisible(!mm.prefs.getBoolean(Const.Key.COREONLY, false) + && Utils.checkNetworkStatus() && Shell.rootAccess() && mm.magiskVersionCode >= 0); + menu.findItem(R.id.log).setVisible(Shell.rootAccess()); + menu.findItem(R.id.superuser).setVisible(Shell.rootAccess() && + !(Const.USER_ID > 0 && mm.multiuserMode == Const.Value.MULTIUSER_MODE_OWNER_MANAGED)); + } + + public void navigate(String item) { + int itemId = R.id.magisk; + if (item != null) { + switch (item) { + case "superuser": + itemId = R.id.superuser; + break; + case "modules": + itemId = R.id.modules; + break; + case "downloads": + itemId = R.id.downloads; + break; + case "magiskhide": + itemId = R.id.magiskhide; + break; + case "log": + itemId = R.id.log; + break; + case "settings": + itemId = R.id.settings; + break; + case "about": + itemId = R.id.app_about; + break; + } + } + navigate(itemId); + } + + public void navigate(int itemId) { + int bak = mDrawerItem; + mDrawerItem = itemId; + navigationView.setCheckedItem(itemId); + switch (itemId) { + case R.id.magisk: + fromShortcut = false; + displayFragment(new MagiskFragment(), true); + break; + case R.id.superuser: + displayFragment(new SuperuserFragment(), true); + break; + case R.id.modules: + displayFragment(new ModulesFragment(), true); + break; + case R.id.downloads: + displayFragment(new ReposFragment(), true); + break; + case R.id.magiskhide: + displayFragment(new MagiskHideFragment(), true); + break; + case R.id.log: + displayFragment(new LogFragment(), false); + break; + case R.id.settings: + startActivity(new Intent(this, SettingsActivity.class)); + mDrawerItem = bak; + break; + case R.id.app_about: + startActivity(new Intent(this, AboutActivity.class)); + mDrawerItem = bak; + break; + } + } + + private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) { + supportInvalidateOptionsMenu(); + getSupportFragmentManager() + .beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .replace(R.id.content_frame, navFragment) + .commitNow(); + toolbar.setElevation(setElevation ? toolbarElevation : 0); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/ModulesFragment.java b/app/src/full/java/com/topjohnwu/magisk/ModulesFragment.java new file mode 100644 index 000000000..db3a79ebd --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/ModulesFragment.java @@ -0,0 +1,144 @@ +package com.topjohnwu.magisk; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.topjohnwu.magisk.adapters.ModulesAdapter; +import com.topjohnwu.magisk.asyncs.LoadModules; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.container.Module; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.superuser.Shell; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; + +public class ModulesFragment extends Fragment implements Topic.Subscriber { + + private Unbinder unbinder; + @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; + @BindView(R.id.recyclerView) RecyclerView recyclerView; + @BindView(R.id.empty_rv) TextView emptyRv; + @OnClick(R.id.fab) + public void selectFile() { + runWithPermission(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, () -> { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("application/zip"); + startActivityForResult(intent, Const.ID.FETCH_ZIP); + }); + } + + private List listModules = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_modules, container, false); + unbinder = ButterKnife.bind(this, view); + setHasOptionsMenu(true); + + mSwipeRefreshLayout.setOnRefreshListener(() -> { + recyclerView.setVisibility(View.GONE); + new LoadModules().exec(); + }); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mSwipeRefreshLayout.setEnabled(recyclerView.getChildAt(0).getTop() >= 0); + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + }); + + getActivity().setTitle(R.string.modules); + + return view; + } + + @Override + public void onTopicPublished(Topic topic) { + updateUI(); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { getApplication().moduleLoadDone }; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) { + // Get the URI of the selected file + Intent intent = new Intent(getActivity(), FlashActivity.class); + intent.setData(data.getData()).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP); + startActivity(intent); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_reboot, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.reboot: + Shell.Async.su("/system/bin/reboot"); + return true; + case R.id.reboot_recovery: + Shell.Async.su("/system/bin/reboot recovery"); + return true; + case R.id.reboot_bootloader: + Shell.Async.su("/system/bin/reboot bootloader"); + return true; + case R.id.reboot_download: + Shell.Async.su("/system/bin/reboot download"); + return true; + default: + return false; + } + } + + private void updateUI() { + listModules.clear(); + listModules.addAll(getApplication().moduleMap.values()); + if (listModules.size() == 0) { + emptyRv.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyRv.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + recyclerView.setAdapter(new ModulesAdapter(listModules)); + } + mSwipeRefreshLayout.setRefreshing(false); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/NoUIActivity.java b/app/src/full/java/com/topjohnwu/magisk/NoUIActivity.java new file mode 100644 index 000000000..00f5714b8 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/NoUIActivity.java @@ -0,0 +1,26 @@ +package com.topjohnwu.magisk; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; + +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.utils.Const; + +public class NoUIActivity extends Activity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String[] perms = getIntent().getStringArrayExtra(Const.Key.INTENT_PERM); + if (perms != null) { + ActivityCompat.requestPermissions(this, perms, 0); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + finish(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/ReposFragment.java b/app/src/full/java/com/topjohnwu/magisk/ReposFragment.java new file mode 100644 index 000000000..d80dfc855 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/ReposFragment.java @@ -0,0 +1,126 @@ +package com.topjohnwu.magisk; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SearchView; +import android.widget.TextView; + +import com.topjohnwu.magisk.adapters.ReposAdapter; +import com.topjohnwu.magisk.asyncs.UpdateRepos; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Topic; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class ReposFragment extends Fragment implements Topic.Subscriber { + + private Unbinder unbinder; + private MagiskManager mm; + @BindView(R.id.recyclerView) RecyclerView recyclerView; + @BindView(R.id.empty_rv) TextView emptyRv; + @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; + + public static ReposAdapter adapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_repos, container, false); + unbinder = ButterKnife.bind(this, view); + mm = getApplication(); + + mSwipeRefreshLayout.setRefreshing(mm.repoLoadDone.isPending()); + + mSwipeRefreshLayout.setOnRefreshListener(() -> { + recyclerView.setVisibility(View.VISIBLE); + emptyRv.setVisibility(View.GONE); + new UpdateRepos(true).exec(); + }); + + getActivity().setTitle(R.string.downloads); + + return view; + } + + @Override + public void onResume() { + adapter = new ReposAdapter(mm.repoDB, mm.moduleMap); + recyclerView.setAdapter(adapter); + super.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + adapter = null; + } + + @Override + public void onTopicPublished(Topic topic) { + mSwipeRefreshLayout.setRefreshing(false); + recyclerView.setVisibility(adapter.getItemCount() == 0 ? View.GONE : View.VISIBLE); + emptyRv.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { mm.repoLoadDone }; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_repo, menu); + SearchView search = (SearchView) menu.findItem(R.id.repo_search).getActionView(); + search.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + adapter.filter(newText); + return false; + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.repo_sort) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.sorting_order) + .setSingleChoiceItems(R.array.sorting_orders, mm.repoOrder, (d, which) -> { + mm.repoOrder = which; + mm.prefs.edit().putInt(Const.Key.REPO_ORDER, mm.repoOrder).apply(); + adapter.notifyDBChanged(); + d.dismiss(); + }).show(); + } + return true; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/SettingsActivity.java b/app/src/full/java/com/topjohnwu/magisk/SettingsActivity.java new file mode 100644 index 000000000..ef0d06170 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/SettingsActivity.java @@ -0,0 +1,321 @@ +package com.topjohnwu.magisk; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceCategory; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.PreferenceScreen; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.Toast; + +import com.topjohnwu.magisk.asyncs.CheckUpdates; +import com.topjohnwu.magisk.asyncs.HideManager; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.receivers.DownloadReceiver; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.FingerprintHelper; +import com.topjohnwu.magisk.utils.RootUtils; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.IOException; +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class SettingsActivity extends Activity implements Topic.Subscriber { + + @BindView(R.id.toolbar) Toolbar toolbar; + + @Override + public int getDarkTheme() { + return R.style.AppTheme_StatusBar_Dark; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + ButterKnife.bind(this); + + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(view -> finish()); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.settings); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction().add(R.id.container, new SettingsFragment()).commit(); + } + + } + + @Override + public void onTopicPublished(Topic topic) { + recreate(); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { getMagiskManager().reloadActivity }; + } + + public static class SettingsFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener, Topic.Subscriber { + + private SharedPreferences prefs; + private PreferenceScreen prefScreen; + + private ListPreference updateChannel, suAccess, autoRes, suNotification, + requestTimeout, multiuserMode, namespaceMode; + private MagiskManager mm; + private PreferenceCategory generalCatagory; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.app_settings, rootKey); + mm = Utils.getMagiskManager(getActivity()); + prefs = mm.prefs; + prefScreen = getPreferenceScreen(); + + generalCatagory = (PreferenceCategory) findPreference("general"); + PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk"); + PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser"); + Preference hideManager = findPreference("hide"); + Preference restoreManager = findPreference("restore"); + findPreference("clear").setOnPreferenceClickListener((pref) -> { + prefs.edit().remove(Const.Key.ETAG_KEY).apply(); + mm.repoDB.clearRepo(); + MagiskManager.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT); + return true; + }); + + updateChannel = (ListPreference) findPreference(Const.Key.UPDATE_CHANNEL); + suAccess = (ListPreference) findPreference(Const.Key.ROOT_ACCESS); + autoRes = (ListPreference) findPreference(Const.Key.SU_AUTO_RESPONSE); + requestTimeout = (ListPreference) findPreference(Const.Key.SU_REQUEST_TIMEOUT); + suNotification = (ListPreference) findPreference(Const.Key.SU_NOTIFICATION); + multiuserMode = (ListPreference) findPreference(Const.Key.SU_MULTIUSER_MODE); + namespaceMode = (ListPreference) findPreference(Const.Key.SU_MNT_NS); + SwitchPreference reauth = (SwitchPreference) findPreference(Const.Key.SU_REAUTH); + SwitchPreference fingerprint = (SwitchPreference) findPreference(Const.Key.SU_FINGERPRINT); + + updateChannel.setOnPreferenceChangeListener((pref, o) -> { + mm.updateChannel = Integer.parseInt((String) o); + if (mm.updateChannel == Const.Value.CUSTOM_CHANNEL) { + View v = LayoutInflater.from(getActivity()).inflate(R.layout.custom_channel_dialog, null); + EditText url = v.findViewById(R.id.custom_url); + url.setText(mm.prefs.getString(Const.Key.CUSTOM_CHANNEL, "")); + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.settings_update_custom) + .setView(v) + .setPositiveButton(R.string.ok, (d, i) -> + prefs.edit().putString(Const.Key.CUSTOM_CHANNEL, + url.getText().toString()).apply()) + .setNegativeButton(R.string.close, null) + .show(); + } + return true; + }); + + setSummary(); + + // Disable dangerous settings in secondary user + if (Const.USER_ID > 0) { + suCategory.removePreference(multiuserMode); + } + + // Disable re-authentication option on Android O, it will not work + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + reauth.setEnabled(false); + reauth.setSummary(R.string.android_o_not_support); + } + + // Disable fingerprint option if not possible + if (!FingerprintHelper.canUseFingerprint()) { + fingerprint.setEnabled(false); + fingerprint.setSummary(R.string.disable_fingerprint); + } + + if (mm.magiskVersionCode >= Const.MAGISK_VER.MANAGER_HIDE) { + if (mm.getPackageName().equals(Const.ORIG_PKG_NAME)) { + hideManager.setOnPreferenceClickListener((pref) -> { + new HideManager(getActivity()).exec(); + return true; + }); + generalCatagory.removePreference(restoreManager); + } else { + if (Utils.checkNetworkStatus()) { + restoreManager.setOnPreferenceClickListener((pref) -> { + Utils.dlAndReceive( + getActivity(), new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + mm.dumpPrefs(); + if (ShellUtils.fastCmdResult("pm install " + uri.getPath())) + RootUtils.uninstallPkg(context.getPackageName()); + } + }, + mm.managerLink, + Utils.fmt("MagiskManager-v%s.apk", mm.remoteManagerVersionString) + ); + return true; + }); + } else { + generalCatagory.removePreference(restoreManager); + } + generalCatagory.removePreference(hideManager); + } + } else { + generalCatagory.removePreference(restoreManager); + generalCatagory.removePreference(hideManager); + } + + if (!Shell.rootAccess() || (Const.USER_ID > 0 && + mm.multiuserMode == Const.Value.MULTIUSER_MODE_OWNER_MANAGED)) { + prefScreen.removePreference(suCategory); + } + + if (!Shell.rootAccess()) { + prefScreen.removePreference(magiskCategory); + generalCatagory.removePreference(hideManager); + } else if (mm.magiskVersionCode < Const.MAGISK_VER.UNIFIED) { + prefScreen.removePreference(magiskCategory); + } + } + + private void setLocalePreference(ListPreference lp) { + CharSequence[] entries = new CharSequence[mm.locales.size() + 1]; + CharSequence[] entryValues = new CharSequence[mm.locales.size() + 1]; + entries[0] = Utils.getLocaleString(MagiskManager.defaultLocale, R.string.system_default); + entryValues[0] = ""; + int i = 1; + for (Locale locale : mm.locales) { + entries[i] = locale.getDisplayName(locale); + entryValues[i++] = locale.toLanguageTag(); + } + lp.setEntries(entries); + lp.setEntryValues(entryValues); + lp.setSummary(MagiskManager.locale.getDisplayName(MagiskManager.locale)); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + prefs.registerOnSharedPreferenceChangeListener(this); + subscribeTopics(); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onDestroyView() { + prefs.unregisterOnSharedPreferenceChangeListener(this); + unsubscribeTopics(); + super.onDestroyView(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + + switch (key) { + case Const.Key.DARK_THEME: + mm.isDarkTheme = prefs.getBoolean(key, false); + mm.reloadActivity.publish(false); + return; + case Const.Key.COREONLY: + if (prefs.getBoolean(key, false)) { + try { + Const.MAGISK_DISABLE_FILE.createNewFile(); + } catch (IOException ignored) {} + } else { + Const.MAGISK_DISABLE_FILE.delete(); + } + Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show(); + break; + case Const.Key.MAGISKHIDE: + if (prefs.getBoolean(key, false)) { + Shell.Async.su("magiskhide --enable"); + } else { + Shell.Async.su("magiskhide --disable"); + } + break; + case Const.Key.HOSTS: + if (prefs.getBoolean(key, false)) { + Shell.Async.su( + "cp -af /system/etc/hosts " + Const.MAGISK_HOST_FILE, + "mount -o bind " + Const.MAGISK_HOST_FILE + " /system/etc/hosts"); + } else { + Shell.Async.su( + "umount -l /system/etc/hosts", + "rm -f " + Const.MAGISK_HOST_FILE); + } + break; + case Const.Key.ROOT_ACCESS: + case Const.Key.SU_MULTIUSER_MODE: + case Const.Key.SU_MNT_NS: + mm.mDB.setSettings(key, Utils.getPrefsInt(prefs, key)); + break; + case Const.Key.LOCALE: + mm.setLocale(); + mm.reloadActivity.publish(false); + break; + case Const.Key.UPDATE_CHANNEL: + new CheckUpdates().exec(); + break; + case Const.Key.CHECK_UPDATES: + mm.setupUpdateCheck(); + break; + } + mm.loadConfig(); + setSummary(); + } + + private void setSummary() { + updateChannel.setSummary(getResources() + .getStringArray(R.array.update_channel)[mm.updateChannel]); + suAccess.setSummary(getResources() + .getStringArray(R.array.su_access)[mm.suAccessState]); + autoRes.setSummary(getResources() + .getStringArray(R.array.auto_response)[mm.suResponseType]); + suNotification.setSummary(getResources() + .getStringArray(R.array.su_notification)[mm.suNotificationType]); + requestTimeout.setSummary( + getString(R.string.request_timeout_summary, prefs.getString(Const.Key.SU_REQUEST_TIMEOUT, "10"))); + multiuserMode.setSummary(getResources() + .getStringArray(R.array.multiuser_summary)[mm.multiuserMode]); + namespaceMode.setSummary(getResources() + .getStringArray(R.array.namespace_summary)[mm.suNamespaceMode]); + } + + @Override + public void onTopicPublished(Topic topic) { + setLocalePreference((ListPreference) findPreference(Const.Key.LOCALE)); + } + + @Override + public Topic[] getSubscription() { + return new Topic[] { mm.localeDone }; + } + } + +} diff --git a/app/src/full/java/com/topjohnwu/magisk/SplashActivity.java b/app/src/full/java/com/topjohnwu/magisk/SplashActivity.java new file mode 100644 index 000000000..2bb1e3c50 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/SplashActivity.java @@ -0,0 +1,88 @@ +package com.topjohnwu.magisk; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + +import com.topjohnwu.magisk.asyncs.CheckUpdates; +import com.topjohnwu.magisk.asyncs.LoadModules; +import com.topjohnwu.magisk.asyncs.ParallelTask; +import com.topjohnwu.magisk.asyncs.UpdateRepos; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.database.RepoDatabaseHelper; +import com.topjohnwu.magisk.receivers.ShortcutReceiver; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.RootUtils; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; + +public class SplashActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + RootUtils.init(); + MagiskManager mm = getMagiskManager(); + + mm.repoDB = new RepoDatabaseHelper(this); + mm.loadMagiskInfo(); + mm.getDefaultInstallFlags(); + mm.loadPrefs(); + + // Dynamic detect all locales + new LoadLocale().exec(); + + // Create notification channel on Android O + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(Const.ID.NOTIFICATION_CHANNEL, + getString(R.string.magisk_updates), NotificationManager.IMPORTANCE_DEFAULT); + getSystemService(NotificationManager.class).createNotificationChannel(channel); + } + + // Setup shortcuts + sendBroadcast(new Intent(this, ShortcutReceiver.class)); + + LoadModules loadModuleTask = new LoadModules(); + + if (Utils.checkNetworkStatus()) { + // Fire update check + new CheckUpdates().exec(); + // Add repo update check + loadModuleTask.setCallBack(() -> new UpdateRepos(false).exec()); + } + + // Magisk working as expected + if (Shell.rootAccess() && mm.magiskVersionCode > 0) { + // Update check service + mm.setupUpdateCheck(); + // Fire asynctasks + loadModuleTask.exec(); + } + + // Write back default values + mm.writeConfig(); + + mm.hasInit = true; + + Intent intent = new Intent(this, MainActivity.class); + intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION)); + intent.putExtra(Const.Key.INTENT_PERM, getIntent().getStringExtra(Const.Key.INTENT_PERM)); + startActivity(intent); + finish(); + } + + static class LoadLocale extends ParallelTask { + @Override + protected Void doInBackground(Void... voids) { + MagiskManager.get().locales = Utils.getAvailableLocale(); + return null; + } + @Override + protected void onPostExecute(Void aVoid) { + MagiskManager.get().localeDone.publish(); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/SuLogFragment.java b/app/src/full/java/com/topjohnwu/magisk/SuLogFragment.java new file mode 100644 index 000000000..44bb9a932 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/SuLogFragment.java @@ -0,0 +1,89 @@ +package com.topjohnwu.magisk; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.topjohnwu.magisk.adapters.SuLogAdapter; +import com.topjohnwu.magisk.components.Fragment; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class SuLogFragment extends Fragment { + + @BindView(R.id.empty_rv) TextView emptyRv; + @BindView(R.id.recyclerView) RecyclerView recyclerView; + + private Unbinder unbinder; + private MagiskManager mm; + private SuLogAdapter adapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_log, menu); + menu.findItem(R.id.menu_save).setVisible(false); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View v = inflater.inflate(R.layout.fragment_su_log, container, false); + unbinder = ButterKnife.bind(this, v); + mm = getApplication(); + adapter = new SuLogAdapter(mm.mDB); + recyclerView.setAdapter(adapter); + + updateList(); + + return v; + } + + private void updateList() { + adapter.notifyDBChanged(); + + if (adapter.getSectionCount() == 0) { + emptyRv.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyRv.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh: + updateList(); + return true; + case R.id.menu_clear: + mm.mDB.clearLogs(); + updateList(); + return true; + default: + return true; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/SuperuserFragment.java b/app/src/full/java/com/topjohnwu/magisk/SuperuserFragment.java new file mode 100644 index 000000000..1d6f2d79e --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/SuperuserFragment.java @@ -0,0 +1,63 @@ +package com.topjohnwu.magisk; + +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.topjohnwu.magisk.adapters.PolicyAdapter; +import com.topjohnwu.magisk.components.Fragment; +import com.topjohnwu.magisk.container.Policy; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public class SuperuserFragment extends Fragment { + + private Unbinder unbinder; + @BindView(R.id.recyclerView) RecyclerView recyclerView; + @BindView(R.id.empty_rv) TextView emptyRv; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_superuser, container, false); + unbinder = ButterKnife.bind(this, view); + + PackageManager pm = getActivity().getPackageManager(); + MagiskManager mm = getApplication(); + + List policyList = mm.mDB.getPolicyList(pm); + + if (policyList.size() == 0) { + emptyRv.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + recyclerView.setAdapter(new PolicyAdapter(policyList, mm.mDB, pm)); + emptyRv.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + + return view; + } + + @Override + public void onStart() { + super.onStart(); + getActivity().setTitle(getString(R.string.superuser)); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java new file mode 100644 index 000000000..8cad5b196 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java @@ -0,0 +1,158 @@ +package com.topjohnwu.magisk.adapters; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.Filter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.asyncs.ParallelTask; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.superuser.Shell; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ApplicationAdapter extends RecyclerView.Adapter { + + private List fullList, showList; + private List hideList; + private PackageManager pm; + private ApplicationFilter filter; + + public ApplicationAdapter() { + fullList = showList = Collections.emptyList(); + hideList = Collections.emptyList(); + filter = new ApplicationFilter(); + pm = MagiskManager.get().getPackageManager(); + new LoadApps().exec(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false); + return new ViewHolder(mView); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + ApplicationInfo info = showList.get(position); + + holder.appIcon.setImageDrawable(info.loadIcon(pm)); + holder.appName.setText(info.loadLabel(pm)); + holder.appPackage.setText(info.packageName); + + holder.checkBox.setOnCheckedChangeListener(null); + holder.checkBox.setChecked(hideList.contains(info.packageName)); + holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + Shell.Async.su("magiskhide --add " + info.packageName); + hideList.add(info.packageName); + } else { + Shell.Async.su("magiskhide --rm " + info.packageName); + hideList.remove(info.packageName); + } + }); + } + + @Override + public int getItemCount() { + return showList.size(); + } + + public void filter(String constraint) { + filter.filter(constraint); + } + + public void refresh() { + new LoadApps().exec(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.app_icon) ImageView appIcon; + @BindView(R.id.app_name) TextView appName; + @BindView(R.id.package_name) TextView appPackage; + @BindView(R.id.checkbox) CheckBox checkBox; + + ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + private class ApplicationFilter extends Filter { + + private boolean lowercaseContains(String s, CharSequence filter) { + return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint == null || constraint.length() == 0) { + showList = fullList; + } else { + showList = new ArrayList<>(); + String filter = constraint.toString().toLowerCase(); + for (ApplicationInfo info : fullList) { + if (lowercaseContains(info.loadLabel(pm).toString(), filter) + || lowercaseContains(info.packageName, filter)) { + showList.add(info); + } + } + } + return null; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + notifyDataSetChanged(); + } + } + + private class LoadApps extends ParallelTask { + + @Override + protected Void doInBackground(Void... voids) { + fullList = pm.getInstalledApplications(0); + hideList = Shell.Sync.su("magiskhide --ls"); + for (Iterator i = fullList.iterator(); i.hasNext(); ) { + ApplicationInfo info = i.next(); + if (Const.HIDE_BLACKLIST.contains(info.packageName) || !info.enabled) { + i.remove(); + } + } + Collections.sort(fullList, (a, b) -> { + boolean ah = hideList.contains(a.packageName); + boolean bh = hideList.contains(b.packageName); + if (ah == bh) { + return a.loadLabel(pm).toString().toLowerCase().compareTo( + b.loadLabel(pm).toString().toLowerCase()); + } else if (ah) { + return -1; + } else { + return 1; + } + }); + return null; + } + + @Override + protected void onPostExecute(Void v) { + MagiskManager.get().magiskHideDone.publish(false); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/ModulesAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/ModulesAdapter.java new file mode 100644 index 000000000..9ce3614f6 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/ModulesAdapter.java @@ -0,0 +1,125 @@ +package com.topjohnwu.magisk.adapters; + +import android.content.Context; +import android.support.design.widget.Snackbar; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.container.Module; +import com.topjohnwu.superuser.Shell; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ModulesAdapter extends RecyclerView.Adapter { + + private final List mList; + + public ModulesAdapter(List list) { + mList = list; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + Context context = holder.itemView.getContext(); + final Module module = mList.get(position); + + String version = module.getVersion(); + String author = module.getAuthor(); + String description = module.getDescription(); + String noInfo = context.getString(R.string.no_info_provided); + + holder.title.setText(module.getName()); + holder.versionName.setText( TextUtils.isEmpty(version) ? noInfo : version); + holder.author.setText( TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author)); + holder.description.setText( TextUtils.isEmpty(description) ? noInfo : description); + + holder.checkBox.setOnCheckedChangeListener(null); + holder.checkBox.setChecked(module.isEnabled()); + holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> { + int snack; + if (isChecked) { + module.removeDisableFile(); + snack = R.string.disable_file_removed; + } else { + module.createDisableFile(); + snack = R.string.disable_file_created; + } + SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show(); + }); + + holder.delete.setOnClickListener(v -> { + boolean removed = module.willBeRemoved(); + int snack; + if (removed) { + module.deleteRemoveFile(); + snack = R.string.remove_file_deleted; + } else { + module.createRemoveFile(); + snack = R.string.remove_file_created; + } + SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show(); + updateDeleteButton(holder, module); + }); + + if (module.isUpdated()) { + holder.notice.setVisibility(View.VISIBLE); + holder.notice.setText(R.string.update_file_created); + holder.delete.setEnabled(false); + } else { + updateDeleteButton(holder, module); + } + } + + private void updateDeleteButton(ViewHolder holder, Module module) { + holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE); + + if (module.willBeRemoved()) { + holder.delete.setImageResource(R.drawable.ic_undelete); + } else { + holder.delete.setImageResource(R.drawable.ic_delete); + } + } + + @Override + public int getItemCount() { + return mList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.title) TextView title; + @BindView(R.id.version_name) TextView versionName; + @BindView(R.id.description) TextView description; + @BindView(R.id.notice) TextView notice; + @BindView(R.id.checkbox) CheckBox checkBox; + @BindView(R.id.author) TextView author; + @BindView(R.id.delete) ImageView delete; + + ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + + if (!Shell.rootAccess()) { + checkBox.setEnabled(false); + delete.setEnabled(false); + } + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/PolicyAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/PolicyAdapter.java new file mode 100644 index 000000000..abaa4e8c3 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/PolicyAdapter.java @@ -0,0 +1,150 @@ +package com.topjohnwu.magisk.adapters; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.design.widget.Snackbar; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.components.AlertDialogBuilder; +import com.topjohnwu.magisk.components.ExpandableView; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.container.Policy; +import com.topjohnwu.magisk.database.MagiskDatabaseHelper; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class PolicyAdapter extends RecyclerView.Adapter { + + private List policyList; + private MagiskDatabaseHelper dbHelper; + private PackageManager pm; + private Set expandList = new HashSet<>(); + + public PolicyAdapter(List list, MagiskDatabaseHelper db, PackageManager pm) { + policyList = list; + dbHelper = db; + this.pm = pm; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Policy policy = policyList.get(position); + + holder.setExpanded(expandList.contains(policy)); + + holder.itemView.setOnClickListener(view -> { + if (holder.isExpanded()) { + holder.collapse(); + expandList.remove(policy); + } else { + holder.expand(); + expandList.add(policy); + } + }); + + holder.appName.setText(policy.appName); + holder.packageName.setText(policy.packageName); + holder.appIcon.setImageDrawable(policy.info.loadIcon(pm)); + holder.masterSwitch.setOnCheckedChangeListener((v, isChecked) -> { + if ((isChecked && policy.policy == Policy.DENY) || + (!isChecked && policy.policy == Policy.ALLOW)) { + policy.policy = isChecked ? Policy.ALLOW : Policy.DENY; + String message = v.getContext().getString( + isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName); + SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show(); + dbHelper.updatePolicy(policy); + } + }); + holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> { + if ((isChecked && !policy.notification) || + (!isChecked && policy.notification)) { + policy.notification = isChecked; + String message = v.getContext().getString( + isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName); + SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show(); + dbHelper.updatePolicy(policy); + } + }); + holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> { + if ((isChecked && !policy.logging) || + (!isChecked && policy.logging)) { + policy.logging = isChecked; + String message = v.getContext().getString( + isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName); + SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show(); + dbHelper.updatePolicy(policy); + } + }); + holder.delete.setOnClickListener(v -> new AlertDialogBuilder((Activity) v.getContext()) + .setTitle(R.string.su_revoke_title) + .setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName)) + .setPositiveButton(R.string.yes, (dialog, which) -> { + policyList.remove(position); + notifyItemRemoved(position); + notifyItemRangeChanged(position, policyList.size()); + SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName), + Snackbar.LENGTH_SHORT).show(); + dbHelper.deletePolicy(policy); + }) + .setNegativeButton(R.string.no_thanks, null) + .setCancelable(true) + .show()); + holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW); + holder.notificationSwitch.setChecked(policy.notification); + holder.loggingSwitch.setChecked(policy.logging); + + // Hide for now + holder.moreInfo.setVisibility(View.GONE); + } + + @Override + public int getItemCount() { + return policyList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder implements ExpandableView { + + @BindView(R.id.app_name) TextView appName; + @BindView(R.id.package_name) TextView packageName; + @BindView(R.id.app_icon) ImageView appIcon; + @BindView(R.id.master_switch) Switch masterSwitch; + @BindView(R.id.notification_switch) Switch notificationSwitch; + @BindView(R.id.logging_switch) Switch loggingSwitch; + @BindView(R.id.expand_layout) ViewGroup expandLayout; + + @BindView(R.id.delete) ImageView delete; + @BindView(R.id.more_info) ImageView moreInfo; + + private Container container = new Container(); + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + container.expandLayout = expandLayout; + setupExpandable(); + } + + @Override + public Container getContainer() { + return container; + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/ReposAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/ReposAdapter.java new file mode 100644 index 000000000..959d074dd --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/ReposAdapter.java @@ -0,0 +1,192 @@ +package com.topjohnwu.magisk.adapters; + +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.asyncs.MarkDownWindow; +import com.topjohnwu.magisk.asyncs.ProcessRepoZip; +import com.topjohnwu.magisk.components.AlertDialogBuilder; +import com.topjohnwu.magisk.container.Module; +import com.topjohnwu.magisk.container.Repo; +import com.topjohnwu.magisk.database.RepoDatabaseHelper; +import com.topjohnwu.magisk.utils.Utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ReposAdapter extends SectionedAdapter { + + private static final int UPDATES = 0; + private static final int INSTALLED = 1; + private static final int OTHERS = 2; + + private Cursor repoCursor = null; + private Map moduleMap; + private RepoDatabaseHelper repoDB; + private List>> repoPairs; + + public ReposAdapter(RepoDatabaseHelper db, Map map) { + repoDB = db; + moduleMap = map; + repoPairs = new ArrayList<>(); + notifyDBChanged(); + } + + + @Override + public int getSectionCount() { + return repoPairs.size(); + } + + @Override + public int getItemCount(int section) { + return repoPairs.get(section).second.size(); + } + + @Override + public SectionHolder onCreateSectionViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false); + return new SectionHolder(v); + } + + @Override + public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false); + return new RepoHolder(v); + } + + @Override + public void onBindSectionViewHolder(SectionHolder holder, int section) { + switch (repoPairs.get(section).first) { + case UPDATES: + holder.sectionText.setText(R.string.update_available); + break; + case INSTALLED: + holder.sectionText.setText(R.string.installed); + break; + case OTHERS: + holder.sectionText.setText(R.string.not_installed); + break; + } + } + + @Override + public void onBindItemViewHolder(RepoHolder holder, int section, int position) { + Repo repo = repoPairs.get(section).second.get(position); + Context context = holder.itemView.getContext(); + + holder.title.setText(repo.getName()); + holder.versionName.setText(repo.getVersion()); + String author = repo.getAuthor(); + holder.author.setText(TextUtils.isEmpty(author) ? null : context.getString(R.string.author, author)); + holder.description.setText(repo.getDescription()); + holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString())); + + holder.infoLayout.setOnClickListener(v -> + new MarkDownWindow((Activity) context, null, repo.getDetailUrl()).exec()); + + holder.downloadImage.setOnClickListener(v -> { + String filename = repo.getName() + "-" + repo.getVersion() + ".zip"; + new AlertDialogBuilder((Activity) context) + .setTitle(context.getString(R.string.repo_install_title, repo.getName())) + .setMessage(context.getString(R.string.repo_install_msg, filename)) + .setCancelable(true) + .setPositiveButton(R.string.install, (d, i) -> + new ProcessRepoZip((Activity) context, repo.getZipUrl(), + Utils.getLegalFilename(filename), true).exec() + ) + .setNeutralButton(R.string.download, (d, i) -> + new ProcessRepoZip((Activity) context, repo.getZipUrl(), + Utils.getLegalFilename(filename), false).exec()) + .setNegativeButton(R.string.no_thanks, null) + .show(); + }); + } + + public void notifyDBChanged() { + if (repoCursor != null) + repoCursor.close(); + repoCursor = repoDB.getRepoCursor(); + filter(""); + } + + public void filter(String s) { + List updates = new ArrayList<>(); + List installed = new ArrayList<>(); + List others = new ArrayList<>(); + + repoPairs.clear(); + while (repoCursor.moveToNext()) { + Repo repo = new Repo(repoCursor); + if (repo.getName().toLowerCase().contains(s.toLowerCase()) + || repo.getAuthor().toLowerCase().contains(s.toLowerCase()) + || repo.getDescription().toLowerCase().contains(s.toLowerCase()) + ) { + // Passed the repoFilter + Module module = moduleMap.get(repo.getId()); + if (module != null) { + if (repo.getVersionCode() > module.getVersionCode()) { + // Updates + updates.add(repo); + } else { + installed.add(repo); + } + } else { + others.add(repo); + } + } + } + repoCursor.moveToFirst(); + + if (!updates.isEmpty()) + repoPairs.add(new Pair<>(UPDATES, updates)); + if (!installed.isEmpty()) + repoPairs.add(new Pair<>(INSTALLED, installed)); + if (!others.isEmpty()) + repoPairs.add(new Pair<>(OTHERS, others)); + + notifyDataSetChanged(); + } + + static class SectionHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.section_text) TextView sectionText; + + SectionHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + static class RepoHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.title) TextView title; + @BindView(R.id.version_name) TextView versionName; + @BindView(R.id.description) TextView description; + @BindView(R.id.author) TextView author; + @BindView(R.id.info_layout) LinearLayout infoLayout; + @BindView(R.id.download) ImageView downloadImage; + @BindView(R.id.update_time) TextView updateTime; + + RepoHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/SectionedAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/SectionedAdapter.java new file mode 100644 index 000000000..cbfab0698 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/SectionedAdapter.java @@ -0,0 +1,93 @@ +package com.topjohnwu.magisk.adapters; + +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +public abstract class SectionedAdapter + extends RecyclerView.Adapter { + + private static final int SECTION_TYPE = Integer.MIN_VALUE; + + @Override + final public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == SECTION_TYPE) + return onCreateSectionViewHolder(parent); + return onCreateItemViewHolder(parent, viewType); + } + + @Override + @SuppressWarnings("unchecked") + final public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + PositionInfo info = getPositionInfo(position); + if (info.position == -1) + onBindSectionViewHolder((S) holder, info.section); + else + onBindItemViewHolder((C) holder, info.section, info.position); + } + + @Override + final public int getItemCount() { + int size, sec; + size = sec = getSectionCount(); + for (int i = 0; i < sec; ++i){ + size += getItemCount(i); + } + return size; + } + + @Override + final public int getItemViewType(int position) { + PositionInfo info = getPositionInfo(position); + if (info.position == -1) + return SECTION_TYPE; + else + return getItemViewType(info.section, info.position); + } + + public int getItemViewType(int section, int position) { + return 0; + } + + protected int getSectionPosition(int section) { + return getItemPosition(section, -1); + } + + protected int getItemPosition(int section, int position) { + int realPosition = 0; + // Previous sections + for (int i = 0; i < section; ++i) { + realPosition += getItemCount(i) + 1; + } + // Current section + realPosition += position + 1; + return realPosition; + } + + private PositionInfo getPositionInfo(int position) { + int section = 0; + while (true) { + if (position == 0) + return new PositionInfo(section, -1); + position -= 1; + if (position < getItemCount(section)) + return new PositionInfo(section, position); + position -= getItemCount(section++); + } + } + + private static class PositionInfo { + int section; + int position; + PositionInfo(int section, int position) { + this.section = section; + this.position = position; + } + } + + public abstract int getSectionCount(); + public abstract int getItemCount(int section); + public abstract S onCreateSectionViewHolder(ViewGroup parent); + public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType); + public abstract void onBindSectionViewHolder(S holder, int section); + public abstract void onBindItemViewHolder(C holder, int section, int position); +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/SuLogAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/SuLogAdapter.java new file mode 100644 index 000000000..0d9138d9b --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/SuLogAdapter.java @@ -0,0 +1,155 @@ +package com.topjohnwu.magisk.adapters; + +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.components.ExpandableView; +import com.topjohnwu.magisk.container.SuLogEntry; +import com.topjohnwu.magisk.database.MagiskDatabaseHelper; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class SuLogAdapter extends SectionedAdapter { + + private List> logEntryList; + private Set itemExpanded, sectionExpanded; + private MagiskDatabaseHelper suDB; + private Cursor suLogCursor = null; + + public SuLogAdapter(MagiskDatabaseHelper db) { + suDB = db; + logEntryList = Collections.emptyList(); + sectionExpanded = new HashSet<>(); + itemExpanded = new HashSet<>(); + } + + @Override + public int getSectionCount() { + return logEntryList.size(); + } + + @Override + public int getItemCount(int section) { + return sectionExpanded.contains(section) ? logEntryList.get(section).size() : 0; + } + + @Override + public SectionHolder onCreateSectionViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false); + return new SectionHolder(v); + } + + @Override + public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false); + return new LogViewHolder(v); + } + + @Override + public void onBindSectionViewHolder(SectionHolder holder, int section) { + suLogCursor.moveToPosition(logEntryList.get(section).get(0)); + SuLogEntry entry = new SuLogEntry(suLogCursor); + holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0); + holder.itemView.setOnClickListener(v -> { + RotateAnimation rotate; + if (sectionExpanded.contains(section)) { + holder.arrow.setRotation(0); + rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + sectionExpanded.remove(section); + notifyItemRangeRemoved(getItemPosition(section, 0), logEntryList.get(section).size()); + } else { + rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + sectionExpanded.add(section); + notifyItemRangeInserted(getItemPosition(section, 0), logEntryList.get(section).size()); + } + rotate.setDuration(300); + rotate.setFillAfter(true); + holder.arrow.setAnimation(rotate); + }); + holder.date.setText(entry.getDateString()); + } + + @Override + public void onBindItemViewHolder(LogViewHolder holder, int section, int position) { + int sqlPosition = logEntryList.get(section).get(position); + suLogCursor.moveToPosition(sqlPosition); + SuLogEntry entry = new SuLogEntry(suLogCursor); + holder.setExpanded(itemExpanded.contains(sqlPosition)); + holder.itemView.setOnClickListener(view -> { + if (holder.isExpanded()) { + holder.collapse(); + itemExpanded.remove(sqlPosition); + } else { + holder.expand(); + itemExpanded.add(sqlPosition); + } + }); + holder.appName.setText(entry.appName); + holder.action.setText(entry.action ? R.string.grant : R.string.deny); + holder.command.setText(entry.command); + holder.fromPid.setText(String.valueOf(entry.fromPid)); + holder.toUid.setText(String.valueOf(entry.toUid)); + holder.time.setText(entry.getTimeString()); + } + + public void notifyDBChanged() { + if (suLogCursor != null) + suLogCursor.close(); + suLogCursor = suDB.getLogCursor(); + logEntryList = suDB.getLogStructure(); + itemExpanded.clear(); + sectionExpanded.clear(); + sectionExpanded.add(0); + notifyDataSetChanged(); + } + + static class SectionHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.date) TextView date; + @BindView(R.id.arrow) ImageView arrow; + + SectionHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + static class LogViewHolder extends RecyclerView.ViewHolder implements ExpandableView { + + @BindView(R.id.app_name) TextView appName; + @BindView(R.id.action) TextView action; + @BindView(R.id.time) TextView time; + @BindView(R.id.fromPid) TextView fromPid; + @BindView(R.id.toUid) TextView toUid; + @BindView(R.id.command) TextView command; + @BindView(R.id.expand_layout) ViewGroup expandLayout; + + private Container container = new Container(); + + LogViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + container.expandLayout = expandLayout; + setupExpandable(); + } + + @Override + public Container getContainer() { + return container; + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/adapters/TabFragmentAdapter.java b/app/src/full/java/com/topjohnwu/magisk/adapters/TabFragmentAdapter.java new file mode 100644 index 000000000..c7daba71f --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/adapters/TabFragmentAdapter.java @@ -0,0 +1,41 @@ +package com.topjohnwu.magisk.adapters; + + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class TabFragmentAdapter extends FragmentPagerAdapter { + + private List fragmentList; + private List titleList; + + public TabFragmentAdapter(FragmentManager fm) { + super(fm); + fragmentList = new ArrayList<>(); + titleList = new ArrayList<>(); + } + + @Override + public Fragment getItem(int position) { + return fragmentList.get(position); + } + + @Override + public int getCount() { + return fragmentList.size(); + } + + @Override + public CharSequence getPageTitle(int position) { + return titleList.get(position); + } + + public void addTab(Fragment fragment, String title) { + fragmentList.add(fragment); + titleList.add(title); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java new file mode 100644 index 000000000..9fb81bbac --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java @@ -0,0 +1,84 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.ISafetyNetHelper; +import com.topjohnwu.magisk.utils.WebService; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; + +import dalvik.system.DexClassLoader; + +public class CheckSafetyNet extends ParallelTask { + + public static final File dexPath = + new File(MagiskManager.get().getFilesDir().getParent() + "/snet", "snet.apk"); + private ISafetyNetHelper helper; + + public CheckSafetyNet(Activity activity) { + super(activity); + } + + private void dlSnet() throws Exception { + Shell.Sync.sh("rm -rf " + dexPath.getParent()); + dexPath.getParentFile().mkdir(); + HttpURLConnection conn = WebService.request(Const.Url.SNET_URL, null); + try ( + OutputStream out = new BufferedOutputStream(new FileOutputStream(dexPath)); + InputStream in = new BufferedInputStream(conn.getInputStream())) { + ShellUtils.pump(in, out); + } finally { + conn.disconnect(); + } + } + + private void dyload() throws Exception { + DexClassLoader loader = new DexClassLoader(dexPath.getPath(), dexPath.getParent(), + null, ISafetyNetHelper.class.getClassLoader()); + Class clazz = loader.loadClass("com.topjohnwu.snet.SafetyNetHelper"); + helper = (ISafetyNetHelper) clazz.getConstructors()[0] + .newInstance(getActivity(), (ISafetyNetHelper.Callback) + code -> MagiskManager.get().safetyNetDone.publish(false, code)); + if (helper.getVersion() != Const.SNET_VER) { + throw new Exception(); + } + } + + @Override + protected Exception doInBackground(Void... voids) { + try { + try { + dyload(); + } catch (Exception e) { + // If dynamic load failed, try re-downloading and reload + dlSnet(); + dyload(); + } + } catch (Exception e) { + return e; + } + + return null; + } + + @Override + protected void onPostExecute(Exception e) { + if (e == null) { + helper.attest(); + } else { + e.printStackTrace(); + MagiskManager.get().safetyNetDone.publish(false, -1); + } + super.onPostExecute(e); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java new file mode 100644 index 000000000..b8e6495a4 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java @@ -0,0 +1,70 @@ +package com.topjohnwu.magisk.asyncs; + +import com.topjohnwu.magisk.BuildConfig; +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.ShowUI; +import com.topjohnwu.magisk.utils.WebService; + +import org.json.JSONException; +import org.json.JSONObject; + +public class CheckUpdates extends ParallelTask { + + private boolean showNotification; + + public CheckUpdates() { + this(false); + } + + public CheckUpdates(boolean b) { + showNotification = b; + } + + @Override + protected Void doInBackground(Void... voids) { + MagiskManager mm = MagiskManager.get(); + String jsonStr = ""; + switch (mm.updateChannel) { + case Const.Value.STABLE_CHANNEL: + jsonStr = WebService.getString(Const.Url.STABLE_URL); + break; + case Const.Value.BETA_CHANNEL: + jsonStr = WebService.getString(Const.Url.BETA_URL); + break; + case Const.Value.CUSTOM_CHANNEL: + jsonStr = WebService.getString(mm.prefs.getString(Const.Key.CUSTOM_CHANNEL, "")); + break; + } + try { + JSONObject json = new JSONObject(jsonStr); + JSONObject magisk = json.getJSONObject("magisk"); + mm.remoteMagiskVersionString = magisk.getString("version"); + mm.remoteMagiskVersionCode = magisk.getInt("versionCode"); + mm.magiskLink = magisk.getString("link"); + mm.magiskNoteLink = magisk.getString("note"); + JSONObject manager = json.getJSONObject("app"); + mm.remoteManagerVersionString = manager.getString("version"); + mm.remoteManagerVersionCode = manager.getInt("versionCode"); + mm.managerLink = manager.getString("link"); + mm.managerNoteLink = manager.getString("note"); + JSONObject uninstaller = json.getJSONObject("uninstaller"); + mm.uninstallerLink = uninstaller.getString("link"); + } catch (JSONException ignored) {} + return null; + } + + @Override + protected void onPostExecute(Void v) { + MagiskManager mm = MagiskManager.get(); + if (showNotification) { + if (BuildConfig.VERSION_CODE < mm.remoteManagerVersionCode) { + ShowUI.managerUpdateNotification(); + } else if (mm.magiskVersionCode < mm.remoteMagiskVersionCode) { + ShowUI.magiskUpdateNotification(); + } + } + mm.updateCheckDone.publish(); + super.onPostExecute(v); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/FlashZip.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/FlashZip.java new file mode 100644 index 000000000..85255425a --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/FlashZip.java @@ -0,0 +1,109 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.net.Uri; +import android.text.TextUtils; +import android.view.View; + +import com.topjohnwu.magisk.FlashActivity; +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.ZipUtils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +public class FlashZip extends ParallelTask { + + private Uri mUri; + private File mCachedFile; + private List console, logs; + + public FlashZip(Activity context, Uri uri, List console, List logs) { + super(context); + mUri = uri; + this.console = console; + this.logs = logs; + mCachedFile = new File(context.getCacheDir(), "install.zip"); + } + + private boolean unzipAndCheck() throws Exception { + ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android", true); + return ShellUtils.fastCmdResult("grep -q '#MAGISK' " + new File(mCachedFile.getParentFile(), "updater-script")); + } + + @Override + protected Integer doInBackground(Void... voids) { + MagiskManager mm = MagiskManager.get(); + try { + console.add("- Copying zip to temp directory"); + + mCachedFile.delete(); + try ( + InputStream in = mm.getContentResolver().openInputStream(mUri); + OutputStream out = new BufferedOutputStream(new FileOutputStream(mCachedFile)) + ) { + if (in == null) throw new FileNotFoundException(); + InputStream buf= new BufferedInputStream(in); + ShellUtils.pump(buf, out); + } catch (FileNotFoundException e) { + console.add("! Invalid Uri"); + throw e; + } catch (IOException e) { + console.add("! Cannot copy to cache"); + throw e; + } + if (!unzipAndCheck()) return 0; + console.add("- Installing " + Utils.getNameFromUri(mm, mUri)); + Shell.Sync.su(console, logs, + "cd " + mCachedFile.getParent(), + "BOOTMODE=true sh update-binary dummy 1 " + mCachedFile + " || echo 'Failed!'" + ); + + if (TextUtils.equals(console.get(console.size() - 1), "Failed!")) + return -1; + + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + console.add("- All done!"); + return 1; + } + + // -1 = error, manual install; 0 = invalid zip; 1 = success + @Override + protected void onPostExecute(Integer result) { + FlashActivity activity = (FlashActivity) getActivity(); + Shell.Async.su( + "rm -rf " + mCachedFile.getParent(), + "rm -rf " + Const.TMP_FOLDER_PATH + ); + switch (result) { + case -1: + console.add("! Installation failed"); + SnackbarMaker.showUri(getActivity(), mUri); + break; + case 0: + console.add("! This zip is not a Magisk Module!"); + break; + case 1: + // Success + new LoadModules().exec(); + break; + } + activity.reboot.setVisibility(result > 0 ? View.VISIBLE : View.GONE); + activity.buttonPanel.setVisibility(View.VISIBLE); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/HideManager.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/HideManager.java new file mode 100644 index 000000000..92c7bf8a6 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/HideManager.java @@ -0,0 +1,95 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.widget.Toast; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.PatchAPK; +import com.topjohnwu.magisk.utils.RootUtils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileOutputStream; + +import java.io.FileNotFoundException; +import java.security.SecureRandom; + +public class HideManager extends ParallelTask { + + private ProgressDialog dialog; + + public HideManager(Activity activity) { + super(activity); + } + + private String genPackageName(String prefix, int length) { + StringBuilder builder = new StringBuilder(length); + builder.append(prefix); + length -= prefix.length(); + SecureRandom random = new SecureRandom(); + String base = "abcdefghijklmnopqrstuvwxyz"; + String alpha = base + base.toUpperCase(); + String full = alpha + "0123456789.........."; + char next, prev = '\0'; + for (int i = 0; i < length; ++i) { + if (prev == '.' || i == length - 1 || i == 0) { + next = alpha.charAt(random.nextInt(alpha.length())); + } else { + next = full.charAt(random.nextInt(full.length())); + } + builder.append(next); + prev = next; + } + return builder.toString(); + } + + @Override + protected void onPreExecute() { + dialog = ProgressDialog.show(getActivity(), + getActivity().getString(R.string.hide_manager_toast), + getActivity().getString(R.string.hide_manager_toast2)); + } + + @Override + protected Boolean doInBackground(Void... voids) { + MagiskManager mm = MagiskManager.get(); + + // Generate a new app with random package name + SuFile repack = new SuFile("/data/local/tmp/repack.apk"); + String pkg = genPackageName("com.", Const.ORIG_PKG_NAME.length()); + + try { + if (!PatchAPK.patchPackageID( + mm.getPackageCodePath(), + new SuFileOutputStream(repack), + Const.ORIG_PKG_NAME, pkg)) + return false; + } catch (FileNotFoundException e) { + return false; + } + + // Install the application + if (!ShellUtils.fastCmdResult(Shell.getShell(), "pm install " + repack)) + return false; + + repack.delete(); + + mm.mDB.setStrings(Const.Key.SU_MANAGER, pkg); + mm.dumpPrefs(); + RootUtils.uninstallPkg(Const.ORIG_PKG_NAME); + + return true; + } + + @Override + protected void onPostExecute(Boolean b) { + dialog.dismiss(); + if (!b) { + MagiskManager.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG); + } + super.onPostExecute(b); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/InstallMagisk.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/InstallMagisk.java new file mode 100644 index 000000000..988d37632 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/InstallMagisk.java @@ -0,0 +1,327 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.view.View; +import android.widget.Toast; + +import com.topjohnwu.magisk.FlashActivity; +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.container.TarEntry; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.ZipUtils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFileInputStream; +import com.topjohnwu.utils.SignBoot; + +import org.kamranzafar.jtar.TarInputStream; +import org.kamranzafar.jtar.TarOutputStream; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.List; + +public class InstallMagisk extends ParallelTask { + + private static final int PATCH_MODE = 0; + public static final int DIRECT_MODE = 1; + private static final int FIX_ENV_MODE = 2; + public static final int SECOND_SLOT_MODE = 3; + + private Uri bootUri, mZip; + private List console, logs; + private String mBoot; + private int mode; + private File installDir; + private ProgressDialog dialog; + private MagiskManager mm; + + public InstallMagisk(Activity context, Uri zip) { + super(context); + mZip = zip; + mm = MagiskManager.get(); + mode = FIX_ENV_MODE; + } + + public InstallMagisk(Activity context, List console, List logs, Uri zip, int mode) { + this(context, zip); + this.console = console; + this.logs = logs; + this.mode = mode; + } + + public InstallMagisk(FlashActivity context, List console, List logs, Uri zip, Uri boot) { + this(context, console, logs, zip, PATCH_MODE); + bootUri = boot; + } + + @Override + protected void onPreExecute() { + if (mode == FIX_ENV_MODE) { + Activity a = getActivity(); + dialog = ProgressDialog.show(a, a.getString(R.string.setup_title), a.getString(R.string.setup_msg)); + console = new NOPList<>(); + } + } + + private void extractFiles(String arch) throws IOException { + console.add("- Extracting files"); + try (InputStream in = mm.getContentResolver().openInputStream(mZip)) { + if (in == null) throw new FileNotFoundException(); + BufferedInputStream buf = new BufferedInputStream(in); + buf.mark(Integer.MAX_VALUE); + ZipUtils.unzip(buf, installDir, arch + "/", true); + buf.reset(); + ZipUtils.unzip(buf, installDir, "common/", true); + buf.reset(); + ZipUtils.unzip(buf, installDir, "chromeos/", false); + buf.reset(); + ZipUtils.unzip(buf, installDir, "META-INF/com/google/android/update-binary", true); + buf.close(); + } catch (FileNotFoundException e) { + console.add("! Invalid Uri"); + throw e; + } catch (IOException e) { + console.add("! Cannot unzip zip"); + throw e; + } + Shell.Sync.sh(Utils.fmt("chmod -R 755 %s/*; %s/magiskinit -x magisk %s/magisk", + installDir, installDir, installDir)); + } + + private boolean dumpBoot() { + console.add("- Copying image locally"); + // Copy boot image to local + try (InputStream in = mm.getContentResolver().openInputStream(bootUri); + OutputStream out = new FileOutputStream(mBoot) + ) { + if (in == null) + throw new FileNotFoundException(); + + InputStream src; + if (Utils.getNameFromUri(mm, bootUri).endsWith(".tar")) { + // Extract boot.img from tar + TarInputStream tar = new TarInputStream(new BufferedInputStream(in)); + org.kamranzafar.jtar.TarEntry entry; + while ((entry = tar.getNextEntry()) != null) { + if (entry.getName().equals("boot.img")) + break; + } + src = tar; + } else { + // Direct copy raw image + src = new BufferedInputStream(in); + } + ShellUtils.pump(src, out); + } catch (FileNotFoundException e) { + console.add("! Invalid Uri"); + return false; + } catch (IOException e) { + console.add("! Copy failed"); + return false; + } + return true; + } + + private File patchBoot() throws IOException { + boolean isSigned; + try (InputStream in = new SuFileInputStream(mBoot)) { + isSigned = SignBoot.verifySignature(in, null); + if (isSigned) { + console.add("- Boot image is signed with AVB 1.0"); + } + } catch (IOException e) { + console.add("! Unable to check signature"); + throw e; + } + + // Patch boot image + Shell.Sync.sh(console, logs, + "cd " + installDir, + Utils.fmt("KEEPFORCEENCRYPT=%b KEEPVERITY=%b sh update-binary indep " + + "boot_patch.sh %s || echo 'Failed!'", + mm.keepEnc, mm.keepVerity, mBoot)); + + if (TextUtils.equals(console.get(console.size() - 1), "Failed!")) + return null; + + Shell.Sync.sh("mv bin/busybox busybox", + "rm -rf magisk.apk bin boot.img update-binary", + "cd /"); + + File patched = new File(installDir, "new-boot.img"); + if (isSigned) { + console.add("- Signing boot image with test keys"); + File signed = new File(installDir, "signed.img"); + try (InputStream in = new SuFileInputStream(patched); + OutputStream out = new BufferedOutputStream(new FileOutputStream(signed)) + ) { + SignBoot.doSignature("/boot", in, out, null, null); + } + Shell.Sync.su("mv -f " + signed + " " + patched); + } + return patched; + } + + private void outputBoot(File patched) throws IOException { + switch (mode) { + case PATCH_MODE: + File dest = new File(Const.EXTERNAL_PATH, "patched_boot" + mm.bootFormat); + dest.getParentFile().mkdirs(); + OutputStream out; + switch (mm.bootFormat) { + case ".img.tar": + out = new TarOutputStream(new BufferedOutputStream(new FileOutputStream(dest))); + ((TarOutputStream) out).putNextEntry(new TarEntry(patched, "boot.img")); + break; + default: + case ".img": + out = new BufferedOutputStream(new FileOutputStream(dest)); + break; + } + try (InputStream in = new SuFileInputStream(patched)) { + ShellUtils.pump(in, out); + out.close(); + } + Shell.Sync.su("rm -f " + patched); + console.add(""); + console.add("****************************"); + console.add(" Patched image is placed in "); + console.add(" " + dest + " "); + console.add("****************************"); + break; + case SECOND_SLOT_MODE: + case DIRECT_MODE: + Shell.Sync.sh(console, logs, + Utils.fmt("direct_install %s %s %s", patched, mBoot, installDir)); + if (!mm.keepVerity) + Shell.Sync.sh(console, logs, "find_dtbo_image", "patch_dtbo_image"); + break; + } + } + + @Override + protected Boolean doInBackground(Void... voids) { + if (mode == FIX_ENV_MODE) { + installDir = new File("/data/adb/magisk"); + Shell.Sync.sh("rm -rf /data/adb/magisk/*"); + } else { + installDir = new File( + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? + mm.createDeviceProtectedStorageContext() : mm) + .getFilesDir().getParent() + , "install"); + Shell.Sync.sh("rm -rf " + installDir); + installDir.mkdirs(); + } + + switch (mode) { + case PATCH_MODE: + mBoot = new File(installDir, "boot.img").getAbsolutePath(); + if (!dumpBoot()) + return false; + break; + case DIRECT_MODE: + console.add("- Detecting target image"); + mBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\""); + break; + case SECOND_SLOT_MODE: + console.add("- Detecting target image"); + char slot[] = ShellUtils.fastCmd("echo $SLOT").toCharArray(); + if (slot[1] == 'a') slot[1] = 'b'; + else slot[1] = 'a'; + mBoot = ShellUtils.fastCmd("SLOT=" + String.valueOf(slot), + "find_boot_image", "echo \"$BOOTIMAGE\""); + Shell.Async.su("mount_partitions"); + break; + case FIX_ENV_MODE: + mBoot = ""; + break; + } + if (mBoot == null) { + console.add("! Unable to detect target image"); + return false; + } + + console.add("- Target image: " + mBoot); + + List abis = Arrays.asList(Build.SUPPORTED_ABIS); + String arch; + + if (mm.remoteMagiskVersionCode >= Const.MAGISK_VER.SEPOL_REFACTOR) { + // 32-bit only + if (abis.contains("x86")) arch = "x86"; + else arch = "arm"; + } else { + if (abis.contains("x86_64")) arch = "x64"; + else if (abis.contains("arm64-v8a")) arch = "arm64"; + else if (abis.contains("x86")) arch = "x86"; + else arch = "arm"; + } + + console.add("- Device platform: " + Build.SUPPORTED_ABIS[0]); + + try { + extractFiles(arch); + if (mode == FIX_ENV_MODE) { + Shell.Sync.sh("fix_env"); + } else { + File patched = patchBoot(); + if (patched == null) + return false; + outputBoot(patched); + console.add("- All done!"); + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + if (mode == FIX_ENV_MODE) { + dialog.dismiss(); + MagiskManager.toast(result ? R.string.setup_done : R.string.setup_fail, Toast.LENGTH_LONG); + } else { + // Running in FlashActivity + FlashActivity activity = (FlashActivity) getActivity(); + if (!result) { + Shell.Async.sh("rm -rf " + installDir); + console.add("! Installation failed"); + activity.reboot.setVisibility(View.GONE); + } + activity.buttonPanel.setVisibility(View.VISIBLE); + } + } + + private static class NOPList extends AbstractList { + @Override + public E get(int index) { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public void add(int index, E element) {} + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/LoadModules.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/LoadModules.java new file mode 100644 index 000000000..06c95df49 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/LoadModules.java @@ -0,0 +1,36 @@ +package com.topjohnwu.magisk.asyncs; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.container.Module; +import com.topjohnwu.magisk.container.ValueSortedMap; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.superuser.Shell; + +import java.util.List; + +public class LoadModules extends ParallelTask { + + private List getModList() { + String command = "ls -d " + Const.MAGISK_PATH + "/* | grep -v lost+found"; + return Shell.Sync.su(command); + } + + @Override + protected Void doInBackground(Void... voids) { + MagiskManager mm = MagiskManager.get(); + mm.moduleMap = new ValueSortedMap<>(); + + for (String path : getModList()) { + Module module = new Module(path); + mm.moduleMap.put(module.getId(), module); + } + + return null; + } + + @Override + protected void onPostExecute(Void v) { + MagiskManager.get().moduleLoadDone.publish(); + super.onPostExecute(v); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/MarkDownWindow.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/MarkDownWindow.java new file mode 100644 index 000000000..06a31ef92 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/MarkDownWindow.java @@ -0,0 +1,86 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.webkit.WebView; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.WebService; +import com.topjohnwu.superuser.ShellUtils; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class MarkDownWindow extends ParallelTask { + + private String mTitle; + private String mUrl; + private InputStream is; + + + public MarkDownWindow(Activity context, String title, String url) { + super(context); + mTitle = title; + mUrl = url; + } + + public MarkDownWindow(Activity context, String title, InputStream in) { + super(context); + mTitle = title; + is = in; + } + + @Override + protected String doInBackground(Void... voids) { + MagiskManager mm = MagiskManager.get(); + String md; + if (mUrl != null) { + md = WebService.getString(mUrl); + } else { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ShellUtils.pump(is, out); + md = out.toString(); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + } + String css; + try ( + InputStream in = mm.getResources().openRawResource( + mm.isDarkTheme ? R.raw.dark : R.raw.light); + ByteArrayOutputStream out = new ByteArrayOutputStream() + ) { + ShellUtils.pump(in, out); + css = out.toString(); + in.close(); + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + Parser parser = Parser.builder().build(); + HtmlRenderer renderer = HtmlRenderer.builder().build(); + Node doc = parser.parse(md); + return String.format("%s", css, renderer.render(doc)); + } + + @Override + protected void onPostExecute(String html) { + AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); + alert.setTitle(mTitle); + + WebView wv = new WebView(getActivity()); + wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null); + + alert.setView(wv); + alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss()); + alert.show(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/ParallelTask.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/ParallelTask.java new file mode 100644 index 000000000..bd94e6365 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/ParallelTask.java @@ -0,0 +1,39 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.os.AsyncTask; + +import java.lang.ref.WeakReference; + +public abstract class ParallelTask extends AsyncTask { + + private WeakReference weakActivity; + + private Runnable callback = null; + + public ParallelTask() {} + + public ParallelTask(Activity context) { + weakActivity = new WeakReference<>(context); + } + + protected Activity getActivity() { + return weakActivity.get(); + } + + @SuppressWarnings("unchecked") + public ParallelTask exec(Params... params) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + return this; + } + + @Override + protected void onPostExecute(Result result) { + if (callback != null) callback.run(); + } + + public ParallelTask setCallBack(Runnable next) { + callback = next; + return this; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java new file mode 100644 index 000000000..f0f564b68 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java @@ -0,0 +1,199 @@ +package com.topjohnwu.magisk.asyncs; + +import android.Manifest; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.widget.Toast; + +import com.topjohnwu.magisk.FlashActivity; +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.WebService; +import com.topjohnwu.magisk.utils.ZipUtils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; + +public class ProcessRepoZip extends ParallelTask { + + private ProgressDialog progressDialog; + private boolean mInstall; + private String mLink; + private File mFile; + private int progress = 0, total = -1; + private Handler mHandler; + + public ProcessRepoZip(Activity context, String link, String filename, boolean install) { + super(context); + mLink = link; + mFile = new File(Const.EXTERNAL_PATH, filename); + mInstall = install; + mHandler = new Handler(); + } + + private void removeTopFolder(File input, File output) throws IOException { + JarEntry entry; + try ( + JarInputStream in = new JarInputStream(new BufferedInputStream(new FileInputStream(input))); + JarOutputStream out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(output))) + ) { + String path; + while ((entry = in.getNextJarEntry()) != null) { + // Remove the top directory from the path + path = entry.getName().substring(entry.getName().indexOf("/") + 1); + // If it's the top folder, ignore it + if (path.isEmpty()) { + continue; + } + // Don't include placeholder + if (path.equals("system/placeholder")) { + continue; + } + out.putNextEntry(new JarEntry(path)); + ShellUtils.pump(in, out); + } + } + } + + @Override + protected void onPreExecute() { + Activity activity = getActivity(); + mFile.getParentFile().mkdirs(); + progressDialog = ProgressDialog.show(activity, activity.getString(R.string.zip_download_title), activity.getString(R.string.zip_download_msg, 0)); + } + + @Override + protected Boolean doInBackground(Void... params) { + Activity activity = getActivity(); + if (activity == null) return null; + try { + // Request zip from Internet + HttpURLConnection conn; + do { + conn = WebService.request(mLink, null); + total = conn.getContentLength(); + if (total < 0) + conn.disconnect(); + else + break; + } while (true); + + // Temp files + File temp1 = new File(activity.getCacheDir(), "1.zip"); + File temp2 = new File(temp1.getParentFile(), "2.zip"); + temp1.getParentFile().mkdir(); + + // First download the zip, Web -> temp1 + try ( + InputStream in = new BufferedInputStream(new ProgressInputStream(conn.getInputStream())); + OutputStream out = new BufferedOutputStream(new FileOutputStream(temp1)) + ) { + ShellUtils.pump(in, out); + in.close(); + } + conn.disconnect(); + + mHandler.post(() -> { + progressDialog.setTitle(R.string.zip_process_title); + progressDialog.setMessage(getActivity().getString(R.string.zip_process_msg)); + }); + + // First remove top folder in Github source zip, temp1 -> temp2 + removeTopFolder(temp1, temp2); + + // Then sign the zip + ZipUtils.signZip(temp2, mFile); + + // Delete temp files + temp1.delete(); + temp2.delete(); + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + Activity activity = getActivity(); + if (activity == null) return; + progressDialog.dismiss(); + if (result) { + Uri uri = Uri.fromFile(mFile); + if (Shell.rootAccess() && mInstall) { + Intent intent = new Intent(activity, FlashActivity.class); + intent.setData(uri).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP); + activity.startActivity(intent); + } else { + SnackbarMaker.showUri(activity, uri); + } + } else { + MagiskManager.toast(R.string.process_error, Toast.LENGTH_LONG); + } + super.onPostExecute(result); + } + + @Override + public ParallelTask exec(Void... voids) { + com.topjohnwu.magisk.components.Activity.runWithPermission( + getActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, + () -> super.exec(voids)); + return this; + } + + private class ProgressInputStream extends FilterInputStream { + + ProgressInputStream(InputStream in) { + super(in); + } + + private void updateDlProgress(int step) { + progress += step; + progressDialog.setMessage(getActivity().getString(R.string.zip_download_msg, (int) (100 * (double) progress / total + 0.5))); + } + + @Override + public synchronized int read() throws IOException { + int b = super.read(); + if (b > 0) { + mHandler.post(() -> updateDlProgress(1)); + } + return b; + } + + @Override + public int read(@NonNull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException { + int read = super.read(b, off, len); + if (read > 0) { + mHandler.post(() -> updateDlProgress(read)); + } + return read; + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/RestoreImages.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/RestoreImages.java new file mode 100644 index 000000000..fb28a2dd5 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/RestoreImages.java @@ -0,0 +1,39 @@ +package com.topjohnwu.magisk.asyncs; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.widget.Toast; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.superuser.ShellUtils; + +public class RestoreImages extends ParallelTask { + + private ProgressDialog dialog; + + public RestoreImages(Activity activity) { + super(activity); + } + + @Override + protected void onPreExecute() { + Activity a = getActivity(); + dialog = ProgressDialog.show(a, a.getString(R.string.restore_img), a.getString(R.string.restore_img_msg)); + } + + @Override + protected Boolean doInBackground(Void... voids) { + return ShellUtils.fastCmdResult("restore_imgs"); + } + + @Override + protected void onPostExecute(Boolean result) { + dialog.cancel(); + if (result) { + MagiskManager.toast(R.string.restore_done, Toast.LENGTH_SHORT); + } else { + MagiskManager.toast(R.string.restore_fail, Toast.LENGTH_LONG); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/asyncs/UpdateRepos.java b/app/src/full/java/com/topjohnwu/magisk/asyncs/UpdateRepos.java new file mode 100644 index 000000000..55b7685f5 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/asyncs/UpdateRepos.java @@ -0,0 +1,210 @@ +package com.topjohnwu.magisk.asyncs; + +import android.database.Cursor; +import android.os.AsyncTask; +import android.text.TextUtils; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.ReposFragment; +import com.topjohnwu.magisk.container.Repo; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Logger; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebService; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +public class UpdateRepos extends ParallelTask { + + private static final int CHECK_ETAG = 0; + private static final int LOAD_NEXT = 1; + private static final int LOAD_PREV = 2; + private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + + private MagiskManager mm; + private List etags, newEtags = new LinkedList<>(); + private Set cached; + private boolean forceUpdate; + private AtomicInteger taskCount = new AtomicInteger(0); + final private Object allDone = new Object(); + + public UpdateRepos(boolean force) { + mm = MagiskManager.get(); + mm.repoLoadDone.reset(); + forceUpdate = force; + } + + private void queueTask(Runnable task) { + // Thread pool's queue has an upper bound, batch it with 64 tasks + while (taskCount.get() >= 64) { + waitTasks(); + } + taskCount.incrementAndGet(); + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + task.run(); + if (taskCount.decrementAndGet() == 0) { + synchronized (allDone) { + allDone.notify(); + } + } + }); + } + + private void waitTasks() { + if (taskCount.get() == 0) + return; + synchronized (allDone) { + try { + allDone.wait(); + } catch (InterruptedException e) { + // Wait again + waitTasks(); + } + } + } + + private boolean loadJSON(String jsonString) throws JSONException, ParseException { + JSONArray jsonArray = new JSONArray(jsonString); + + // Empty page, halt + if (jsonArray.length() == 0) + return false; + + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject rawRepo = jsonArray.getJSONObject(i); + String id = rawRepo.getString("description"); + String name = rawRepo.getString("name"); + Date date = dateFormat.parse(rawRepo.getString("pushed_at")); + Set set = Collections.synchronizedSet(cached); + queueTask(() -> { + Repo repo = mm.repoDB.getRepo(id); + try { + if (repo == null) + repo = new Repo(name); + else + set.remove(id); + repo.update(date); + mm.repoDB.addRepo(repo); + publishProgress(); + } catch (Repo.IllegalRepoException e) { + Logger.debug(e.getMessage()); + mm.repoDB.removeRepo(id); + } + }); + } + return true; + } + + private boolean loadPage(int page, int mode) { + Map header = new HashMap<>(); + if (mode == CHECK_ETAG && page < etags.size()) + header.put(Const.Key.IF_NONE_MATCH, etags.get(page)); + String url = Utils.fmt(Const.Url.REPO_URL, page + 1); + + try { + HttpURLConnection conn = WebService.request(url, header); + if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + // Current page is not updated, check the next page + return loadPage(page + 1, CHECK_ETAG); + } + if (!loadJSON(WebService.getString(conn))) + return mode != CHECK_ETAG; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + /* If one page is updated, we force update all pages */ + + // Update ETAG + String etag = header.get(Const.Key.ETAG_KEY); + etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1); + if (mode == LOAD_PREV) { + // We are loading a previous page, push the new tag to the front + newEtags.add(0, etag); + } else { + newEtags.add(etag); + } + + String links = header.get(Const.Key.LINK_KEY); + if (links != null) { + for (String s : links.split(", ")) { + if (mode != LOAD_PREV && s.contains("next")) { + // Force load all next pages + loadPage(page + 1, LOAD_NEXT); + } + if (mode != LOAD_NEXT && s.contains("prev")) { + // Back propagation + loadPage(page - 1, LOAD_PREV); + } + } + } + return true; + } + + @Override + protected void onProgressUpdate(Void... values) { + if (ReposFragment.adapter != null) + ReposFragment.adapter.notifyDBChanged(); + } + + @Override + protected void onPreExecute() { + mm.repoLoadDone.setPending(); + } + + @Override + protected Void doInBackground(Void... voids) { + etags = Arrays.asList(mm.prefs.getString(Const.Key.ETAG_KEY, "").split(",")); + cached = mm.repoDB.getRepoIDSet(); + + if (loadPage(0, CHECK_ETAG)) { + waitTasks(); + + // The leftover cached means they are removed from online repo + mm.repoDB.removeRepo(cached); + + // Update ETag + mm.prefs.edit().putString(Const.Key.ETAG_KEY, TextUtils.join(",", newEtags)).apply(); + } else if (forceUpdate) { + Cursor c = mm.repoDB.getRawCursor(); + while (c.moveToNext()) { + Repo repo = new Repo(c); + queueTask(() -> { + try { + repo.update(); + mm.repoDB.addRepo(repo); + } catch (Repo.IllegalRepoException e) { + Logger.debug(e.getMessage()); + mm.repoDB.removeRepo(repo); + } + }); + } + waitTasks(); + } + return null; + } + + @Override + protected void onPostExecute(Void v) { + mm.repoLoadDone.publish(); + super.onPostExecute(v); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/components/AboutCardRow.java b/app/src/full/java/com/topjohnwu/magisk/components/AboutCardRow.java new file mode 100644 index 000000000..db022cc97 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/AboutCardRow.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016 dvdandroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.topjohnwu.magisk.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; + +/** + * @author dvdandroid + */ +public class AboutCardRow extends LinearLayout { + + private final String title; + private final Drawable icon; + + private final TextView mTitle; + private final TextView mSummary; + private final ImageView mIcon; + + private final View mView; + + public AboutCardRow(Context context) { + this(context, null); + } + + public AboutCardRow(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AboutCardRow(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + LayoutInflater.from(context).inflate(R.layout.info_item_row, this); + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AboutCardRow, 0, 0); + + try { + title = a.getString(R.styleable.AboutCardRow_text); + icon = a.getDrawable(R.styleable.AboutCardRow_icon); + } finally { + a.recycle(); + } + + mView = findViewById(R.id.container); + + mTitle = (TextView) findViewById(android.R.id.title); + mSummary = (TextView) findViewById(android.R.id.summary); + mIcon = (ImageView) findViewById(android.R.id.icon); + + mTitle.setText(title); + mIcon.setImageDrawable(icon); + } + + @Override + public void setOnClickListener(OnClickListener l) { + super.setOnClickListener(l); + + mView.setOnClickListener(l); + } + + public void setSummary(String s) { + mSummary.setText(s); + } + + public void removeSummary() { + mSummary.setVisibility(GONE); + } +} \ No newline at end of file diff --git a/app/src/full/java/com/topjohnwu/magisk/components/AlertDialogBuilder.java b/app/src/full/java/com/topjohnwu/magisk/components/AlertDialogBuilder.java new file mode 100644 index 000000000..568d69444 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/AlertDialogBuilder.java @@ -0,0 +1,153 @@ +package com.topjohnwu.magisk.components; + +import android.app.Activity; +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.StyleRes; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class AlertDialogBuilder extends AlertDialog.Builder { + + @BindView(R.id.button_panel) LinearLayout buttons; + @BindView(R.id.message_panel) LinearLayout messagePanel; + + @BindView(R.id.negative) Button negative; + @BindView(R.id.positive) Button positive; + @BindView(R.id.neutral) Button neutral; + @BindView(R.id.message) TextView messageView; + + private DialogInterface.OnClickListener positiveListener; + private DialogInterface.OnClickListener negativeListener; + private DialogInterface.OnClickListener neutralListener; + + private AlertDialog dialog; + + public AlertDialogBuilder(@NonNull Activity context) { + super(context); + setup(); + } + + public AlertDialogBuilder(@NonNull Activity context, @StyleRes int themeResId) { + super(context, themeResId); + setup(); + } + + private void setup() { + View v = LayoutInflater.from(getContext()).inflate(R.layout.alert_dialog, null); + ButterKnife.bind(this, v); + super.setView(v); + negative.setVisibility(View.GONE); + positive.setVisibility(View.GONE); + neutral.setVisibility(View.GONE); + buttons.setVisibility(View.GONE); + messagePanel.setVisibility(View.GONE); + } + + @Override + public AlertDialog.Builder setTitle(int titleId) { + return super.setTitle(titleId); + } + + @Override + public AlertDialog.Builder setView(int layoutResId) { return this; } + + @Override + public AlertDialog.Builder setView(View view) { return this; } + + @Override + public AlertDialog.Builder setMessage(@Nullable CharSequence message) { + messageView.setText(message); + messagePanel.setVisibility(View.VISIBLE); + return this; + } + + @Override + public AlertDialog.Builder setMessage(@StringRes int messageId) { + return setMessage(getContext().getString(messageId)); + } + + @Override + public AlertDialog.Builder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) { + buttons.setVisibility(View.VISIBLE); + positive.setVisibility(View.VISIBLE); + positive.setText(text); + positiveListener = listener; + positive.setOnClickListener((v) -> { + if (positiveListener != null) { + positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + } + dialog.dismiss(); + }); + return this; + } + + @Override + public AlertDialog.Builder setPositiveButton(@StringRes int textId, DialogInterface.OnClickListener listener) { + return setPositiveButton(getContext().getString(textId), listener); + } + + @Override + public AlertDialog.Builder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) { + buttons.setVisibility(View.VISIBLE); + negative.setVisibility(View.VISIBLE); + negative.setText(text); + negativeListener = listener; + negative.setOnClickListener((v) -> { + if (negativeListener != null) { + negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE); + } + dialog.dismiss(); + }); + return this; + } + + @Override + public AlertDialog.Builder setNegativeButton(@StringRes int textId, DialogInterface.OnClickListener listener) { + return setNegativeButton(getContext().getString(textId), listener); + } + + @Override + public AlertDialog.Builder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) { + buttons.setVisibility(View.VISIBLE); + neutral.setVisibility(View.VISIBLE); + neutral.setText(text); + neutralListener = listener; + neutral.setOnClickListener((v) -> { + if (neutralListener != null) { + neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL); + } + dialog.dismiss(); + }); + return this; + } + + @Override + public AlertDialog.Builder setNeutralButton(@StringRes int textId, DialogInterface.OnClickListener listener) { + return setNeutralButton(getContext().getString(textId), listener); + } + + @Override + public AlertDialog create() { + dialog = super.create(); + return dialog; + } + + @Override + public AlertDialog show() { + create(); + dialog.show(); + return dialog; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/components/ExpandableView.java b/app/src/full/java/com/topjohnwu/magisk/components/ExpandableView.java new file mode 100644 index 000000000..121611eab --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/ExpandableView.java @@ -0,0 +1,84 @@ +package com.topjohnwu.magisk.components; + +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +public interface ExpandableView { + + class Container { + public ViewGroup expandLayout; + ValueAnimator expandAnimator, collapseAnimator; + boolean mExpanded = false; + int expandHeight = 0; + } + + // Provide state info + Container getContainer(); + + default void setupExpandable() { + Container container = getContainer(); + container.expandLayout.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + if (container.expandHeight == 0) { + final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + container.expandLayout.measure(widthSpec, heightSpec); + container.expandHeight = container.expandLayout.getMeasuredHeight(); + } + + container.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this); + container.expandLayout.setVisibility(View.GONE); + container.expandAnimator = slideAnimator(0, container.expandHeight); + container.collapseAnimator = slideAnimator(container.expandHeight, 0); + return true; + } + + }); + } + + default boolean isExpanded() { + return getContainer().mExpanded; + } + + default void setExpanded(boolean expanded) { + Container container = getContainer(); + container.mExpanded = expanded; + ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams(); + layoutParams.height = expanded ? container.expandHeight : 0; + container.expandLayout.setLayoutParams(layoutParams); + container.expandLayout.setVisibility(expanded ? View.VISIBLE : View.GONE); + } + + default void expand() { + Container container = getContainer(); + if (container.mExpanded) return; + container.expandLayout.setVisibility(View.VISIBLE); + container.expandAnimator.start(); + container.mExpanded = true; + } + + default void collapse() { + Container container = getContainer(); + if (!container.mExpanded) return; + container.collapseAnimator.start(); + container.mExpanded = false; + } + + default ValueAnimator slideAnimator(int start, int end) { + Container container = getContainer(); + ValueAnimator animator = ValueAnimator.ofInt(start, end); + + animator.addUpdateListener(valueAnimator -> { + int value = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams(); + layoutParams.height = value; + container.expandLayout.setLayoutParams(layoutParams); + }); + return animator; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/components/FlavorActivity.java b/app/src/full/java/com/topjohnwu/magisk/components/FlavorActivity.java new file mode 100644 index 000000000..7b7f307db --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/FlavorActivity.java @@ -0,0 +1,108 @@ +package com.topjohnwu.magisk.components; + +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.Keep; +import android.support.annotation.Nullable; +import android.support.annotation.StyleRes; +import android.support.v7.app.AppCompatActivity; +import android.view.WindowManager; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Topic; + +public abstract class FlavorActivity extends AppCompatActivity { + + private AssetManager swappedAssetManager = null; + private Resources swappedResources = null; + private Resources.Theme backupTheme = null; + + @StyleRes + public int getDarkTheme() { + return -1; + } + + public MagiskManager getMagiskManager() { + return (MagiskManager) super.getApplication(); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (this instanceof Topic.Subscriber) { + ((Topic.Subscriber) this).subscribeTopics(); + } + + if (getMagiskManager().isDarkTheme && getDarkTheme() != -1) { + setTheme(getDarkTheme()); + } + } + + @Override + protected void onDestroy() { + if (this instanceof Topic.Subscriber) { + ((Topic.Subscriber) this).unsubscribeTopics(); + } + super.onDestroy(); + } + + protected void setFloating() { + boolean isTablet = getResources().getBoolean(R.bool.isTablet); + if (isTablet) { + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.height = getResources().getDimensionPixelSize(R.dimen.floating_height); + params.width = getResources().getDimensionPixelSize(R.dimen.floating_width); + params.alpha = 1.0f; + params.dimAmount = 0.6f; + params.flags |= 2; + getWindow().setAttributes(params); + setFinishOnTouchOutside(true); + } + } + + @Override + public Resources.Theme getTheme() { + return backupTheme == null ? super.getTheme() : backupTheme; + } + + @Override + public AssetManager getAssets() { + return swappedAssetManager == null ? super.getAssets() : swappedAssetManager; + } + + private AssetManager getAssets(String apk) { + try { + AssetManager asset = AssetManager.class.newInstance(); + AssetManager.class.getMethod("addAssetPath", String.class).invoke(asset, apk); + return asset; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + @Override + public Resources getResources() { + return swappedResources == null ? super.getResources() : swappedResources; + } + + @Keep + public void swapResources(String dexPath) { + AssetManager asset = getAssets(dexPath); + if (asset != null) { + backupTheme = super.getTheme(); + Resources res = super.getResources(); + swappedResources = new Resources(asset, res.getDisplayMetrics(), res.getConfiguration()); + swappedAssetManager = asset; + } + } + + @Keep + public void restoreResources() { + swappedAssetManager = null; + swappedResources = null; + backupTheme = null; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/components/Fragment.java b/app/src/full/java/com/topjohnwu/magisk/components/Fragment.java new file mode 100644 index 000000000..3ca503821 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/Fragment.java @@ -0,0 +1,43 @@ +package com.topjohnwu.magisk.components; + +import android.content.Intent; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; + +public class Fragment extends android.support.v4.app.Fragment { + + public MagiskManager getApplication() { + return Utils.getMagiskManager(getActivity()); + } + + @Override + public void onResume() { + super.onResume(); + if (this instanceof Topic.Subscriber) { + ((Topic.Subscriber) this).subscribeTopics(); + } + } + + @Override + public void onPause() { + if (this instanceof Topic.Subscriber) { + ((Topic.Subscriber) this).unsubscribeTopics(); + } + super.onPause(); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + startActivityForResult(intent, requestCode, this::onActivityResult); + } + + public void startActivityForResult(Intent intent, int requestCode, Activity.ActivityResultListener listener) { + ((Activity) getActivity()).startActivityForResult(intent, requestCode, listener); + } + + public void runWithPermission(String[] permissions, Runnable callback) { + Activity.runWithPermission(getActivity(), permissions, callback); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/components/SnackbarMaker.java b/app/src/full/java/com/topjohnwu/magisk/components/SnackbarMaker.java new file mode 100644 index 000000000..bf873790f --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/components/SnackbarMaker.java @@ -0,0 +1,47 @@ +package com.topjohnwu.magisk.components; + +import android.app.Activity; +import android.net.Uri; +import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; +import android.view.View; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Utils; + +public class SnackbarMaker { + + public static Snackbar make(Activity activity, CharSequence text, int duration) { + View view = activity.findViewById(android.R.id.content); + return make(view, text, duration); + } + + public static Snackbar make(Activity activity, @StringRes int resId, int duration) { + return make(activity, activity.getString(resId), duration); + } + + public static Snackbar make(View view, CharSequence text, int duration) { + Snackbar snack = Snackbar.make(view, text, duration); + setup(snack); + return snack; + } + + public static Snackbar make(View view, @StringRes int resId, int duration) { + Snackbar snack = Snackbar.make(view, resId, duration); + setup(snack); + return snack; + } + + private static void setup(Snackbar snack) { + TextView text = snack.getView().findViewById(android.support.design.R.id.snackbar_text); + text.setMaxLines(Integer.MAX_VALUE); + } + + public static void showUri(Activity activity, Uri uri) { + make(activity, activity.getString(R.string.internal_storage, + "/MagiskManager/" + Utils.getNameFromUri(activity, uri)), + Snackbar.LENGTH_LONG) + .setAction(R.string.ok, (v)->{}).show(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/BaseModule.java b/app/src/full/java/com/topjohnwu/magisk/container/BaseModule.java new file mode 100644 index 000000000..1aeea8c4c --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/BaseModule.java @@ -0,0 +1,121 @@ +package com.topjohnwu.magisk.container; + + +import android.content.ContentValues; +import android.database.Cursor; +import android.support.annotation.NonNull; + +import java.util.List; + +public abstract class BaseModule implements Comparable { + + private String mId = null, mName, mVersion, mAuthor, mDescription; + private int mVersionCode = -1, minMagiskVersion = -1; + + protected BaseModule() {} + + protected BaseModule(Cursor c) { + mId = c.getString(c.getColumnIndex("id")); + mName = c.getString(c.getColumnIndex("name")); + mVersion = c.getString(c.getColumnIndex("version")); + mVersionCode = c.getInt(c.getColumnIndex("versionCode")); + mAuthor = c.getString(c.getColumnIndex("author")); + mDescription = c.getString(c.getColumnIndex("description")); + minMagiskVersion = c.getInt(c.getColumnIndex("minMagisk")); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put("id", mId); + values.put("name", mName); + values.put("version", mVersion); + values.put("versionCode", mVersionCode); + values.put("author", mAuthor); + values.put("description", mDescription); + values.put("minMagisk", minMagiskVersion); + return values; + } + + protected void parseProps(List props) { parseProps(props.toArray(new String[0])); } + + protected void parseProps(String[] props) throws NumberFormatException { + for (String line : props) { + String[] prop = line.split("=", 2); + if (prop.length != 2) + continue; + + String key = prop[0].trim(); + String value = prop[1].trim(); + if (key.isEmpty() || key.charAt(0) == '#') + continue; + + switch (key) { + case "id": + mId = value; + break; + case "name": + mName = value; + break; + case "version": + mVersion = value; + break; + case "versionCode": + mVersionCode = Integer.parseInt(value); + break; + case "author": + mAuthor = value; + break; + case "description": + mDescription = value; + break; + case "minMagisk": + case "template": + minMagiskVersion = Integer.parseInt(value); + break; + default: + break; + } + } + } + + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public String getVersion() { + return mVersion; + } + + public String getAuthor() { + return mAuthor; + } + + public String getId() { + return mId; + } + + public void setId(String id) { + mId = id; + } + + public String getDescription() { + return mDescription; + } + + public int getVersionCode() { + return mVersionCode; + } + + public int getMinMagiskVersion() { + return minMagiskVersion; + } + + @Override + public int compareTo(@NonNull BaseModule module) { + return this.getName().toLowerCase().compareTo(module.getName().toLowerCase()); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/Module.java b/app/src/full/java/com/topjohnwu/magisk/container/Module.java new file mode 100644 index 000000000..399854cf9 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/Module.java @@ -0,0 +1,67 @@ +package com.topjohnwu.magisk.container; + +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.io.SuFile; + +public class Module extends BaseModule { + + private SuFile mRemoveFile, mDisableFile, mUpdateFile; + private boolean mEnable, mRemove, mUpdated; + + public Module(String path) { + + try { + parseProps(Shell.Sync.su("dos2unix < " + path + "/module.prop")); + } catch (NumberFormatException ignored) {} + + mRemoveFile = new SuFile(path + "/remove"); + mDisableFile = new SuFile(path + "/disable"); + mUpdateFile = new SuFile(path + "/update"); + + if (getId() == null) { + int sep = path.lastIndexOf('/'); + setId(path.substring(sep + 1)); + } + + if (getName() == null) { + setName(getId()); + } + + mEnable = !mDisableFile.exists(); + mRemove = mRemoveFile.exists(); + mUpdated = mUpdateFile.exists(); + } + + public void createDisableFile() { + mEnable = false; + mDisableFile.createNewFile(); + } + + public void removeDisableFile() { + mEnable = true; + mDisableFile.delete(); + } + + public boolean isEnabled() { + return mEnable; + } + + public void createRemoveFile() { + mRemove = true; + mRemoveFile.createNewFile(); + } + + public void deleteRemoveFile() { + mRemove = false; + mRemoveFile.delete(); + } + + public boolean willBeRemoved() { + return mRemove; + } + + public boolean isUpdated() { + return mUpdated; + } + +} \ No newline at end of file diff --git a/app/src/full/java/com/topjohnwu/magisk/container/Policy.java b/app/src/full/java/com/topjohnwu/magisk/container/Policy.java new file mode 100644 index 000000000..3c3cf60e5 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/Policy.java @@ -0,0 +1,59 @@ +package com.topjohnwu.magisk.container; + +import android.content.ContentValues; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.support.annotation.NonNull; + + +public class Policy implements Comparable{ + public static final int INTERACTIVE = 0; + public static final int DENY = 1; + public static final int ALLOW = 2; + + public int uid, policy = INTERACTIVE; + public long until; + public boolean logging = true, notification = true; + public String packageName, appName; + public ApplicationInfo info; + + public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException { + String[] pkgs = pm.getPackagesForUid(uid); + if (pkgs == null || pkgs.length == 0) + throw new PackageManager.NameNotFoundException(); + this.uid = uid; + packageName = pkgs[0]; + info = pm.getApplicationInfo(packageName, 0); + appName = info.loadLabel(pm).toString(); + } + + public Policy(Cursor c, PackageManager pm) throws PackageManager.NameNotFoundException { + uid = c.getInt(c.getColumnIndex("uid")); + packageName = c.getString(c.getColumnIndex("package_name")); + policy = c.getInt(c.getColumnIndex("policy")); + until = c.getLong(c.getColumnIndex("until")); + logging = c.getInt(c.getColumnIndex("logging")) != 0; + notification = c.getInt(c.getColumnIndex("notification")) != 0; + info = pm.getApplicationInfo(packageName, 0); + if (info.uid != uid) + throw new PackageManager.NameNotFoundException(); + appName = info.loadLabel(pm).toString(); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put("uid", uid); + values.put("package_name", packageName); + values.put("policy", policy); + values.put("until", until); + values.put("logging", logging ? 1 : 0); + values.put("notification", notification ? 1 : 0); + return values; + } + + @Override + public int compareTo(@NonNull Policy policy) { + return appName.toLowerCase().compareTo(policy.appName.toLowerCase()); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/Repo.java b/app/src/full/java/com/topjohnwu/magisk/container/Repo.java new file mode 100644 index 000000000..a422983a6 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/Repo.java @@ -0,0 +1,92 @@ +package com.topjohnwu.magisk.container; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Logger; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebService; + +import java.text.DateFormat; +import java.util.Date; + +public class Repo extends BaseModule { + + private String repoName; + private Date mLastUpdate; + + public Repo(String name) { + repoName = name; + } + + public Repo(Cursor c) { + super(c); + repoName = c.getString(c.getColumnIndex("repo_name")); + mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update"))); + } + + public void update() throws IllegalRepoException { + String props[] = Utils.dos2unix(WebService.getString(getManifestUrl())).split("\\n"); + try { + parseProps(props); + } catch (NumberFormatException e) { + throw new IllegalRepoException("Repo [" + repoName + "] parse error: " + e.getMessage()); + } + + if (getId() == null) { + throw new IllegalRepoException("Repo [" + repoName + "] does not contain id"); + } + if (getVersionCode() < 0) { + throw new IllegalRepoException("Repo [" + repoName + "] does not contain versionCode"); + } + if (getMinMagiskVersion() < Const.MIN_MODULE_VER()) { + Logger.debug("Repo [" + repoName + "] is outdated"); + } + } + + public void update(Date lastUpdate) throws IllegalRepoException { + mLastUpdate = lastUpdate; + update(); + } + + @Override + public ContentValues getContentValues() { + ContentValues values = super.getContentValues(); + values.put("repo_name", repoName); + values.put("last_update", mLastUpdate.getTime()); + return values; + } + + public String getRepoName() { + return repoName; + } + + public String getZipUrl() { + return String.format(Const.Url.ZIP_URL, repoName); + } + + public String getManifestUrl() { + return String.format(Const.Url.FILE_URL, repoName, "module.prop"); + } + + public String getDetailUrl() { + return String.format(Const.Url.FILE_URL, repoName, "README.md"); + } + + public String getLastUpdateString() { + return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, + MagiskManager.locale).format(mLastUpdate); + } + + public Date getLastUpdate() { + return mLastUpdate; + } + + public class IllegalRepoException extends Exception { + IllegalRepoException(String message) { + super(message); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/SuLogEntry.java b/app/src/full/java/com/topjohnwu/magisk/container/SuLogEntry.java new file mode 100644 index 000000000..4b3518468 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/SuLogEntry.java @@ -0,0 +1,56 @@ +package com.topjohnwu.magisk.container; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.topjohnwu.magisk.MagiskManager; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class SuLogEntry { + + public int fromUid, toUid, fromPid; + public String packageName, appName, command; + public boolean action; + public Date date; + + public SuLogEntry(Policy policy) { + fromUid = policy.uid; + packageName = policy.packageName; + appName = policy.appName; + } + + public SuLogEntry(Cursor c) { + fromUid = c.getInt(c.getColumnIndex("from_uid")); + fromPid = c.getInt(c.getColumnIndex("from_pid")); + toUid = c.getInt(c.getColumnIndex("to_uid")); + packageName = c.getString(c.getColumnIndex("package_name")); + appName = c.getString(c.getColumnIndex("app_name")); + command = c.getString(c.getColumnIndex("command")); + action = c.getInt(c.getColumnIndex("action")) != 0; + date = new Date(c.getLong(c.getColumnIndex("time"))); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put("from_uid", fromUid); + values.put("package_name", packageName); + values.put("app_name", appName); + values.put("from_pid", fromPid); + values.put("command", command); + values.put("to_uid", toUid); + values.put("action", action ? 1 : 0); + values.put("time", date.getTime()); + return values; + } + + public String getDateString() { + return DateFormat.getDateInstance(DateFormat.MEDIUM, MagiskManager.locale).format(date); + } + + public String getTimeString() { + return new SimpleDateFormat("h:mm a", MagiskManager.locale).format(date); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/TarEntry.java b/app/src/full/java/com/topjohnwu/magisk/container/TarEntry.java new file mode 100644 index 000000000..2a09ddd39 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/TarEntry.java @@ -0,0 +1,64 @@ +package com.topjohnwu.magisk.container; + +import org.kamranzafar.jtar.TarHeader; + +import java.io.File; +import java.util.Arrays; + +public class TarEntry extends org.kamranzafar.jtar.TarEntry { + + public TarEntry(File file, String entryName) { + super(file, entryName); + } + + /* + * Workaround missing java.nio.file.attribute.PosixFilePermission + * Simply just assign a default permission to the file + * */ + + @Override + public void extractTarHeader(String entryName) { + int permissions = file.isDirectory() ? 000755 : 000644; + header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory(), permissions); + header.userName = new StringBuffer(""); + header.groupName = header.userName; + } + + /* + * Rewrite the header to GNU format + * */ + + @Override + public void writeEntryHeader(byte[] outbuf) { + super.writeEntryHeader(outbuf); + + System.arraycopy("ustar \0".getBytes(), 0, outbuf, 257, TarHeader.USTAR_MAGICLEN); + getOctalBytes(header.mode, outbuf, 100, TarHeader.MODELEN); + getOctalBytes(header.userId, outbuf, 108, TarHeader.UIDLEN); + getOctalBytes(header.groupId, outbuf, 116, TarHeader.GIDLEN); + getOctalBytes(header.size, outbuf, 124, TarHeader.SIZELEN); + getOctalBytes(header.modTime, outbuf, 136, TarHeader.MODTIMELEN); + Arrays.fill(outbuf, 148, 148 + TarHeader.CHKSUMLEN, (byte) ' '); + Arrays.fill(outbuf, 329, 329 + TarHeader.USTAR_DEVLEN, (byte) '\0'); + Arrays.fill(outbuf, 337, 337 + TarHeader.USTAR_DEVLEN, (byte) '\0'); + + // Recalculate checksum + getOctalBytes(computeCheckSum(outbuf), outbuf, 148, TarHeader.CHKSUMLEN); + } + + /* + * Proper octal to ASCII conversion + * */ + + private void getOctalBytes(long value, byte[] buf, int offset, int length) { + int idx = length - 1; + + buf[offset + idx] = 0; + --idx; + + for (long val = value; idx >= 0; --idx) { + buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7)); + val = val >> 3; + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/container/ValueSortedMap.java b/app/src/full/java/com/topjohnwu/magisk/container/ValueSortedMap.java new file mode 100644 index 000000000..dccd6105c --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/container/ValueSortedMap.java @@ -0,0 +1,43 @@ +package com.topjohnwu.magisk.container; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ValueSortedMap> extends HashMap { + + private List sorted = new ArrayList<>(); + + @NonNull + @Override + public Collection values() { + if (sorted.isEmpty()) { + sorted.addAll(super.values()); + Collections.sort(sorted); + } + return sorted; + } + + @Override + public V put(K key, V value) { + sorted.clear(); + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + sorted.clear(); + super.putAll(m); + } + + @Override + public V remove(Object key) { + sorted.clear(); + return super.remove(key); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/database/MagiskDatabaseHelper.java b/app/src/full/java/com/topjohnwu/magisk/database/MagiskDatabaseHelper.java new file mode 100644 index 000000000..36548119f --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/database/MagiskDatabaseHelper.java @@ -0,0 +1,301 @@ +package com.topjohnwu.magisk.database; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; +import android.os.Process; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.widget.Toast; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.container.Policy; +import com.topjohnwu.magisk.container.SuLogEntry; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.io.SuFile; + +import java.io.File; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +public class MagiskDatabaseHelper { + + private static final int DATABASE_VER = 5; + private static final String POLICY_TABLE = "policies"; + private static final String LOG_TABLE = "logs"; + private static final String SETTINGS_TABLE = "settings"; + private static final String STRINGS_TABLE = "strings"; + + private PackageManager pm; + private SQLiteDatabase db; + + @NonNull + public static MagiskDatabaseHelper getInstance(MagiskManager mm) { + try { + return new MagiskDatabaseHelper(mm); + } catch (Exception e) { + // Let's cleanup everything and try again + Shell.Sync.su("db_clean '*'"); + return new MagiskDatabaseHelper(mm); + } + } + + private MagiskDatabaseHelper(MagiskManager mm) { + pm = mm.getPackageManager(); + db = openDatabase(mm); + db.disableWriteAheadLogging(); + int version = db.getVersion(); + if (version < DATABASE_VER) { + onUpgrade(db, version); + } else if (version > DATABASE_VER) { + onDowngrade(db); + } + db.setVersion(DATABASE_VER); + clearOutdated(); + } + + private SQLiteDatabase openDatabase(MagiskManager mm) { + final File DB_FILE = new File(Utils.fmt("/sbin/.core/db-%d/magisk.db", Const.USER_ID)); + Context de = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + ? mm.createDeviceProtectedStorageContext() : mm; + if (!DB_FILE.canWrite()) { + if (!Shell.rootAccess()) { + // We don't want the app to crash, create a db and return + return mm.openOrCreateDatabase("su.db", Context.MODE_PRIVATE, null); + } + mm.loadMagiskInfo(); + // Cleanup + Shell.Sync.su("db_clean " + Const.USER_ID); + if (mm.magiskVersionCode < Const.MAGISK_VER.FBE_AWARE) { + // Super old legacy mode + return mm.openOrCreateDatabase("su.db", Context.MODE_PRIVATE, null); + } else if (mm.magiskVersionCode < Const.MAGISK_VER.HIDDEN_PATH) { + // Legacy mode with FBE aware + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + de.moveDatabaseFrom(mm, "su.db"); + } + return de.openOrCreateDatabase("su.db", Context.MODE_PRIVATE, null); + } else { + // Global database + final SuFile GLOBAL_DB = new SuFile("/data/adb/magisk.db"); + mm.deleteDatabase("su.db"); + de.deleteDatabase("su.db"); + if (mm.magiskVersionCode < Const.MAGISK_VER.SEPOL_REFACTOR) { + // We need some additional policies on old versions + Shell.Sync.su("db_sepatch"); + } + if (!GLOBAL_DB.exists()) { + Shell.Sync.su("db_init"); + SQLiteDatabase.openOrCreateDatabase(GLOBAL_DB, null).close(); + Shell.Sync.su("db_restore"); + } + } + Shell.Sync.su("db_setup " + Process.myUid()); + } + // Not using legacy mode, open the mounted global DB + return SQLiteDatabase.openOrCreateDatabase(DB_FILE, null); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion) { + if (oldVersion == 0) { + createTables(db); + oldVersion = 3; + } + if (oldVersion == 1) { + // We're dropping column app_name, rename and re-construct table + db.execSQL(Utils.fmt("ALTER TABLE %s RENAME TO %s_old", POLICY_TABLE)); + + // Create the new tables + createTables(db); + + // Migrate old data to new tables + db.execSQL(Utils.fmt("INSERT INTO %s SELECT " + + "uid, package_name, policy, until, logging, notification FROM %s_old", + POLICY_TABLE, POLICY_TABLE)); + db.execSQL(Utils.fmt("DROP TABLE %s_old", POLICY_TABLE)); + + MagiskManager.get().deleteDatabase("sulog.db"); + ++oldVersion; + } + if (oldVersion == 2) { + db.execSQL(Utils.fmt("UPDATE %s SET time=time*1000", LOG_TABLE)); + ++oldVersion; + } + if (oldVersion == 3) { + db.execSQL(Utils.fmt("CREATE TABLE IF NOT EXISTS %s (key TEXT, value TEXT, PRIMARY KEY(key))", STRINGS_TABLE)); + ++oldVersion; + } + if (oldVersion == 4) { + db.execSQL(Utils.fmt("UPDATE %s SET uid=uid%%100000", POLICY_TABLE)); + ++oldVersion; + } + } + + // Remove everything, we do not support downgrade + public void onDowngrade(SQLiteDatabase db) { + MagiskManager.toast(R.string.su_db_corrupt, Toast.LENGTH_LONG); + db.execSQL("DROP TABLE IF EXISTS " + POLICY_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + LOG_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + SETTINGS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + STRINGS_TABLE); + onUpgrade(db, 0); + } + + private void createTables(SQLiteDatabase db) { + // Policies + db.execSQL( + "CREATE TABLE IF NOT EXISTS " + POLICY_TABLE + " " + + "(uid INT, package_name TEXT, policy INT, " + + "until INT, logging INT, notification INT, " + + "PRIMARY KEY(uid))"); + + // Logs + db.execSQL( + "CREATE TABLE IF NOT EXISTS " + LOG_TABLE + " " + + "(from_uid INT, package_name TEXT, app_name TEXT, from_pid INT, " + + "to_uid INT, action INT, time INT, command TEXT)"); + + // Settings + db.execSQL( + "CREATE TABLE IF NOT EXISTS " + SETTINGS_TABLE + " " + + "(key TEXT, value INT, PRIMARY KEY(key))"); + } + + public void clearOutdated() { + // Clear outdated policies + db.delete(POLICY_TABLE, Utils.fmt("until > 0 AND until < %d", System.currentTimeMillis() / 1000), null); + // Clear outdated logs + db.delete(LOG_TABLE, Utils.fmt("time < %d", System.currentTimeMillis() - MagiskManager.get().suLogTimeout * 86400000), null); + } + + public void deletePolicy(Policy policy) { + deletePolicy(policy.uid); + } + + public void deletePolicy(String pkg) { + db.delete(POLICY_TABLE, "package_name=?", new String[] { pkg }); + } + + public void deletePolicy(int uid) { + db.delete(POLICY_TABLE, Utils.fmt("uid=%d", uid), null); + } + + public Policy getPolicy(int uid) { + Policy policy = null; + try (Cursor c = db.query(POLICY_TABLE, null, Utils.fmt("uid=%d", uid), null, null, null, null)) { + if (c.moveToNext()) { + policy = new Policy(c, pm); + } + } catch (PackageManager.NameNotFoundException e) { + deletePolicy(uid); + return null; + } + return policy; + } + + public void addPolicy(Policy policy) { + db.replace(POLICY_TABLE, null, policy.getContentValues()); + } + + public void updatePolicy(Policy policy) { + db.update(POLICY_TABLE, policy.getContentValues(), Utils.fmt("uid=%d", policy.uid), null); + } + + public List getPolicyList(PackageManager pm) { + try (Cursor c = db.query(POLICY_TABLE, null, Utils.fmt("uid/100000=%d", Const.USER_ID), + null, null, null, null)) { + List ret = new ArrayList<>(c.getCount()); + while (c.moveToNext()) { + try { + Policy policy = new Policy(c, pm); + ret.add(policy); + } catch (PackageManager.NameNotFoundException e) { + // The app no longer exist, remove from DB + deletePolicy(c.getInt(c.getColumnIndex("uid"))); + } + } + Collections.sort(ret); + return ret; + } + } + + public List> getLogStructure() { + try (Cursor c = db.query(LOG_TABLE, new String[] { "time" }, Utils.fmt("from_uid/100000=%d", Const.USER_ID), + null, null, null, "time DESC")) { + List> ret = new ArrayList<>(); + List list = null; + String dateString = null, newString; + while (c.moveToNext()) { + Date date = new Date(c.getLong(c.getColumnIndex("time"))); + newString = DateFormat.getDateInstance(DateFormat.MEDIUM, MagiskManager.locale).format(date); + if (!TextUtils.equals(dateString, newString)) { + dateString = newString; + list = new ArrayList<>(); + ret.add(list); + } + list.add(c.getPosition()); + } + return ret; + } + } + + public Cursor getLogCursor() { + return db.query(LOG_TABLE, null, Utils.fmt("from_uid/100000=%d", Const.USER_ID), + null, null, null, "time DESC"); + } + + public void addLog(SuLogEntry log) { + db.insert(LOG_TABLE, null, log.getContentValues()); + } + + public void clearLogs() { + db.delete(LOG_TABLE, null, null); + } + + public void setSettings(String key, int value) { + ContentValues data = new ContentValues(); + data.put("key", key); + data.put("value", value); + db.replace(SETTINGS_TABLE, null, data); + } + + public int getSettings(String key, int defaultValue) { + int value = defaultValue; + try (Cursor c = db.query(SETTINGS_TABLE, null, "key=?",new String[] { key }, null, null, null)) { + if (c.moveToNext()) { + value = c.getInt(c.getColumnIndex("value")); + } + } + return value; + } + + public void setStrings(String key, String value) { + if (value == null) { + db.delete(STRINGS_TABLE, "key=?", new String[] { key }); + } else { + ContentValues data = new ContentValues(); + data.put("key", key); + data.put("value", value); + db.replace(STRINGS_TABLE, null, data); + } + } + + public String getStrings(String key, String defaultValue) { + String value = defaultValue; + try (Cursor c = db.query(STRINGS_TABLE, null, "key=?",new String[] { key }, null, null, null)) { + if (c.moveToNext()) { + value = c.getString(c.getColumnIndex("value")); + } + } + return value; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/database/RepoDatabaseHelper.java b/app/src/full/java/com/topjohnwu/magisk/database/RepoDatabaseHelper.java new file mode 100644 index 000000000..acc4fecb0 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/database/RepoDatabaseHelper.java @@ -0,0 +1,124 @@ +package com.topjohnwu.magisk.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.container.Repo; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; + +import java.util.HashSet; +import java.util.Set; + +public class RepoDatabaseHelper extends SQLiteOpenHelper { + + private static final int DATABASE_VER = 3; + private static final String TABLE_NAME = "repos"; + + private SQLiteDatabase mDb; + private MagiskManager mm; + + public RepoDatabaseHelper(Context context) { + super(context, "repo.db", null, DATABASE_VER); + mm = Utils.getMagiskManager(context); + mDb = getWritableDatabase(); + + // Remove outdated repos + mDb.delete(TABLE_NAME, "minMagisk list) { + for (String id : list) { + if (id == null) continue; + mDb.delete(TABLE_NAME, "id=?", new String[] { id }); + } + } + + public void addRepo(Repo repo) { + mDb.replace(TABLE_NAME, null, repo.getContentValues()); + } + + public Repo getRepo(String id) { + try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) { + if (c.moveToNext()) { + return new Repo(c); + } + } + return null; + } + + public Cursor getRawCursor() { + return mDb.query(TABLE_NAME, null, null, null, null, null, null); + } + + public Cursor getRepoCursor() { + String orderBy = null; + switch (mm.repoOrder) { + case Const.Value.ORDER_NAME: + orderBy = "name COLLATE NOCASE"; + break; + case Const.Value.ORDER_DATE: + orderBy = "last_update DESC"; + } + return mDb.query(TABLE_NAME, null, "minMagisk<=? AND minMagisk>=?", + new String[] { String.valueOf(mm.magiskVersionCode), String.valueOf(Const.MIN_MODULE_VER()) }, + null, null, orderBy); + } + + public Set getRepoIDSet() { + HashSet set = new HashSet<>(300); + try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) { + while (c.moveToNext()) { + set.add(c.getString(c.getColumnIndex("id"))); + } + } + return set; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/receivers/BootReceiver.java b/app/src/full/java/com/topjohnwu/magisk/receivers/BootReceiver.java new file mode 100644 index 000000000..6dd45497f --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/receivers/BootReceiver.java @@ -0,0 +1,21 @@ +package com.topjohnwu.magisk.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import com.topjohnwu.magisk.services.OnBootIntentService; + +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(new Intent(context, OnBootIntentService.class)); + } else { + context.startService(new Intent(context, OnBootIntentService.class)); + } + } + +} diff --git a/app/src/full/java/com/topjohnwu/magisk/receivers/ManagerUpdate.java b/app/src/full/java/com/topjohnwu/magisk/receivers/ManagerUpdate.java new file mode 100644 index 000000000..5dd8e520f --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/receivers/ManagerUpdate.java @@ -0,0 +1,47 @@ +package com.topjohnwu.magisk.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; + +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.PatchAPK; +import com.topjohnwu.magisk.utils.Utils; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +public class ManagerUpdate extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Utils.dlAndReceive( + context, new PatchedInstall(), + intent.getStringExtra(Const.Key.INTENT_SET_LINK), + intent.getStringExtra(Const.Key.INTENT_SET_FILENAME) + ); + } + + private static class PatchedInstall extends ManagerInstall { + @Override + public void onDownloadDone(Context context, Uri uri) { + if (!context.getPackageName().equals(Const.ORIG_PKG_NAME)) { + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + String o = uri.getPath(); + String p = o.substring(0, o.lastIndexOf('.')) + "-patched.apk"; + try { + PatchAPK.patchPackageID(o, new BufferedOutputStream(new FileOutputStream(p)), + Const.ORIG_PKG_NAME, context.getPackageName()); + } catch (FileNotFoundException ignored) { } + super.onDownloadDone(context, Uri.fromFile(new File(p))); + }); + } else { + super.onDownloadDone(context, uri); + } + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/receivers/PackageReceiver.java b/app/src/full/java/com/topjohnwu/magisk/receivers/PackageReceiver.java new file mode 100644 index 000000000..0fb4cfb01 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/receivers/PackageReceiver.java @@ -0,0 +1,32 @@ +package com.topjohnwu.magisk.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; + +public class PackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + MagiskManager mm = Utils.getMagiskManager(context); + + String pkg = intent.getData().getEncodedSchemeSpecificPart(); + + switch (intent.getAction()) { + case Intent.ACTION_PACKAGE_REPLACED: + // This will only work pre-O + if (mm.prefs.getBoolean(Const.Key.SU_REAUTH, false)) { + mm.mDB.deletePolicy(pkg); + } + break; + case Intent.ACTION_PACKAGE_FULLY_REMOVED: + mm.mDB.deletePolicy(pkg); + Shell.Async.su("magiskhide --rm " + pkg); + break; + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/receivers/RebootReceiver.java b/app/src/full/java/com/topjohnwu/magisk/receivers/RebootReceiver.java new file mode 100644 index 000000000..44794e461 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/receivers/RebootReceiver.java @@ -0,0 +1,14 @@ +package com.topjohnwu.magisk.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.topjohnwu.superuser.Shell; + +public class RebootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Shell.Async.su("/system/bin/reboot"); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/receivers/ShortcutReceiver.java b/app/src/full/java/com/topjohnwu/magisk/receivers/ShortcutReceiver.java new file mode 100644 index 000000000..5ae2b2357 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/receivers/ShortcutReceiver.java @@ -0,0 +1,87 @@ +package com.topjohnwu.magisk.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.text.TextUtils; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.SplashActivity; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; + +import java.util.ArrayList; + +public class ShortcutReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + MagiskManager mm = Utils.getMagiskManager(context); + ShortcutManager manager = context.getSystemService(ShortcutManager.class); + if (TextUtils.equals(intent.getAction(), Intent.ACTION_LOCALE_CHANGED)) { + // It is triggered with locale change, manual load Magisk info + mm.loadMagiskInfo(); + } + manager.setDynamicShortcuts(getShortCuts(mm)); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + private ArrayList getShortCuts(MagiskManager mm) { + ArrayList shortCuts = new ArrayList<>(); + if (Shell.rootAccess() && + !(Const.USER_ID > 0 && + mm.multiuserMode == Const.Value.MULTIUSER_MODE_OWNER_MANAGED)) { + shortCuts.add(new ShortcutInfo.Builder(mm, "superuser") + .setShortLabel(mm.getString(R.string.superuser)) + .setIntent(new Intent(mm, SplashActivity.class) + .putExtra(Const.Key.OPEN_SECTION, "superuser") + .setAction(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)) + .setIcon(Icon.createWithResource(mm, R.drawable.sc_superuser)) + .setRank(0) + .build()); + } + if (Shell.rootAccess() && mm.magiskVersionCode >= Const.MAGISK_VER.UNIFIED + && mm.prefs.getBoolean(Const.Key.MAGISKHIDE, false)) { + shortCuts.add(new ShortcutInfo.Builder(mm, "magiskhide") + .setShortLabel(mm.getString(R.string.magiskhide)) + .setIntent(new Intent(mm, SplashActivity.class) + .putExtra(Const.Key.OPEN_SECTION, "magiskhide") + .setAction(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)) + .setIcon(Icon.createWithResource(mm, R.drawable.sc_magiskhide)) + .setRank(1) + .build()); + } + if (!mm.prefs.getBoolean(Const.Key.COREONLY, false) && + Shell.rootAccess() && mm.magiskVersionCode >= 0) { + shortCuts.add(new ShortcutInfo.Builder(mm, "modules") + .setShortLabel(mm.getString(R.string.modules)) + .setIntent(new Intent(mm, SplashActivity.class) + .putExtra(Const.Key.OPEN_SECTION, "modules") + .setAction(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)) + .setIcon(Icon.createWithResource(mm, R.drawable.sc_extension)) + .setRank(3) + .build()); + shortCuts.add(new ShortcutInfo.Builder(mm, "downloads") + .setShortLabel(mm.getString(R.string.download)) + .setIntent(new Intent(mm, SplashActivity.class) + .putExtra(Const.Key.OPEN_SECTION, "downloads") + .setAction(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)) + .setIcon(Icon.createWithResource(mm, R.drawable.sc_cloud_download)) + .setRank(2) + .build()); + } + return shortCuts; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/services/OnBootIntentService.java b/app/src/full/java/com/topjohnwu/magisk/services/OnBootIntentService.java new file mode 100644 index 000000000..a172736a5 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/services/OnBootIntentService.java @@ -0,0 +1,44 @@ +package com.topjohnwu.magisk.services; + +import android.app.IntentService; +import android.content.Intent; +import android.os.Build; +import android.support.v4.app.NotificationCompat; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.RootUtils; + +public class OnBootIntentService extends IntentService { + + public OnBootIntentService() { + super("OnBootIntentService"); + } + + @Override + public void onCreate() { + super.onCreate(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(Const.ID.ONBOOT_NOTIFICATION_ID, + new NotificationCompat.Builder(this, Const.ID.NOTIFICATION_CHANNEL) + .setSmallIcon(R.drawable.ic_magisk_outline) + .setContentTitle("Startup Operations") + .setContentText("Running startup operations...") + .build()); + } + } + + @Override + protected void onHandleIntent(Intent intent) { + /* Pixel 2 (XL) devices will need to patch dtbo.img. + * However, that is not possible if Magisk is installed by + * patching boot image with Magisk Manager and fastboot flash + * the boot image, since at that time we do not have root. + * Check for dtbo status every boot time, and prompt user + * to reboot if dtbo wasn't patched and patched by Magisk Manager. + * */ + MagiskManager.get().loadMagiskInfo(); + RootUtils.patchDTBO(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/services/UpdateCheckService.java b/app/src/full/java/com/topjohnwu/magisk/services/UpdateCheckService.java new file mode 100644 index 000000000..89a958fc2 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/services/UpdateCheckService.java @@ -0,0 +1,23 @@ +package com.topjohnwu.magisk.services; + +import android.app.job.JobParameters; +import android.app.job.JobService; + +import com.topjohnwu.magisk.asyncs.CheckUpdates; +import com.topjohnwu.magisk.utils.Utils; + +public class UpdateCheckService extends JobService { + + @Override + public boolean onStartJob(JobParameters params) { + Utils.getMagiskManager(this).loadMagiskInfo(); + new CheckUpdates(true) + .setCallBack(() -> jobFinished(params, false)).exec(); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return true; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/superuser/RequestActivity.java b/app/src/full/java/com/topjohnwu/magisk/superuser/RequestActivity.java new file mode 100644 index 000000000..10bb42c68 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/superuser/RequestActivity.java @@ -0,0 +1,300 @@ +package com.topjohnwu.magisk.superuser; + +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.FileObserver; +import android.text.TextUtils; +import android.view.View; +import android.view.Window; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.asyncs.ParallelTask; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.container.Policy; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.FingerprintHelper; +import com.topjohnwu.magisk.utils.Utils; + +import java.io.DataInputStream; +import java.io.IOException; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class RequestActivity extends Activity { + + @BindView(R.id.su_popup) LinearLayout suPopup; + @BindView(R.id.timeout) Spinner timeout; + @BindView(R.id.app_icon) ImageView appIcon; + @BindView(R.id.app_name) TextView appNameView; + @BindView(R.id.package_name) TextView packageNameView; + @BindView(R.id.grant_btn) Button grant_btn; + @BindView(R.id.deny_btn) Button deny_btn; + @BindView(R.id.fingerprint) ImageView fingerprintImg; + @BindView(R.id.warning) TextView warning; + + private String socketPath; + private LocalSocket socket; + private PackageManager pm; + private MagiskManager mm; + + private boolean hasTimeout; + private Policy policy; + private CountDownTimer timer; + private FingerprintHelper fingerprintHelper; + + @Override + public int getDarkTheme() { + return R.style.SuRequest_Dark; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + supportRequestWindowFeature(Window.FEATURE_NO_TITLE); + + pm = getPackageManager(); + mm = Utils.getMagiskManager(this); + mm.mDB.clearOutdated(); + + Intent intent = getIntent(); + socketPath = intent.getStringExtra("socket"); + hasTimeout = intent.getBooleanExtra("timeout", true); + + new FileObserver(socketPath) { + @Override + public void onEvent(int fileEvent, String path) { + if (fileEvent == FileObserver.DELETE_SELF) { + finish(); + } + } + }.startWatching(); + + new SocketManager(this).exec(); + } + + @Override + public void finish() { + if (timer != null) + timer.cancel(); + if (fingerprintHelper != null) + fingerprintHelper.cancel(); + super.finish(); + } + + private boolean cancelTimeout() { + timer.cancel(); + deny_btn.setText(getString(R.string.deny)); + return false; + } + + private void showRequest() { + switch (mm.suResponseType) { + case Const.Value.SU_AUTO_DENY: + handleAction(Policy.DENY, 0); + return; + case Const.Value.SU_AUTO_ALLOW: + handleAction(Policy.ALLOW, 0); + return; + case Const.Value.SU_PROMPT: + default: + } + + // If not interactive, response directly + if (policy.policy != Policy.INTERACTIVE) { + handleAction(); + return; + } + + setContentView(R.layout.activity_request); + ButterKnife.bind(this); + + appIcon.setImageDrawable(policy.info.loadIcon(pm)); + appNameView.setText(policy.appName); + packageNameView.setText(policy.packageName); + + ArrayAdapter adapter = ArrayAdapter.createFromResource(this, + R.array.allow_timeout, android.R.layout.simple_spinner_item); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + timeout.setAdapter(adapter); + + timer = new CountDownTimer(mm.suRequestTimeout * 1000, 1000) { + @Override + public void onTick(long millisUntilFinished) { + deny_btn.setText(getString(R.string.deny_with_str, "(" + millisUntilFinished / 1000 + ")")); + } + @Override + public void onFinish() { + deny_btn.setText(getString(R.string.deny_with_str, "(0)")); + handleAction(Policy.DENY); + } + }; + + boolean useFingerprint = mm.prefs.getBoolean(Const.Key.SU_FINGERPRINT, false) && FingerprintHelper.canUseFingerprint(); + + if (useFingerprint) { + try { + fingerprintHelper = new FingerprintHelper() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + warning.setText(errString); + } + + @Override + public void onAuthenticationHelp(int helpCode, CharSequence helpString) { + warning.setText(helpString); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + handleAction(Policy.ALLOW); + } + + @Override + public void onAuthenticationFailed() { + warning.setText(R.string.auth_fail); + } + }; + fingerprintHelper.startAuth(); + } catch (Exception e) { + e.printStackTrace(); + useFingerprint = false; + } + } + + if (!useFingerprint) { + grant_btn.setOnClickListener(v -> { + handleAction(Policy.ALLOW); + timer.cancel(); + }); + grant_btn.requestFocus(); + } + + grant_btn.setVisibility(useFingerprint ? View.GONE : View.VISIBLE); + fingerprintImg.setVisibility(useFingerprint ? View.VISIBLE : View.GONE); + + deny_btn.setOnClickListener(v -> { + handleAction(Policy.DENY); + timer.cancel(); + }); + suPopup.setOnClickListener(v -> cancelTimeout()); + timeout.setOnTouchListener((v, event) -> cancelTimeout()); + + if (hasTimeout) { + timer.start(); + } else { + cancelTimeout(); + } + } + + @Override + public void onBackPressed() { + if (policy != null) { + handleAction(Policy.DENY); + } else { + finish(); + } + } + + void handleAction() { + String response; + if (policy.policy == Policy.ALLOW) { + response = "socket:ALLOW"; + } else { + response = "socket:DENY"; + } + try { + socket.getOutputStream().write((response).getBytes()); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + finish(); + } + + void handleAction(int action) { + handleAction(action, Const.Value.timeoutList[timeout.getSelectedItemPosition()]); + } + + void handleAction(int action, int time) { + policy.policy = action; + if (time >= 0) { + policy.until = (time == 0) ? 0 : (System.currentTimeMillis() / 1000 + time * 60); + mm.mDB.addPolicy(policy); + } + handleAction(); + } + + private class SocketManager extends ParallelTask { + + SocketManager(Activity context) { + super(context); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + socket = new LocalSocket(); + socket.connect(new LocalSocketAddress(socketPath, LocalSocketAddress.Namespace.FILESYSTEM)); + + DataInputStream is = new DataInputStream(socket.getInputStream()); + ContentValues payload = new ContentValues(); + + while (true) { + int nameLen = is.readInt(); + byte[] nameBytes = new byte[nameLen]; + is.readFully(nameBytes); + String name = new String(nameBytes); + if (TextUtils.equals(name, "eof")) + break; + + int dataLen = is.readInt(); + byte[] dataBytes = new byte[dataLen]; + is.readFully(dataBytes); + String data = new String(dataBytes); + payload.put(name, data); + } + + if (payload.getAsInteger("uid") == null) { + return false; + } + + int uid = payload.getAsInteger("uid"); + policy = mm.mDB.getPolicy(uid); + if (policy == null) { + policy = new Policy(uid, pm); + } + + /* Never allow com.topjohnwu.magisk (could be malware) */ + if (TextUtils.equals(policy.packageName, Const.ORIG_PKG_NAME)) + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + showRequest(); + } else { + finish(); + } + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/superuser/SuReceiver.java b/app/src/full/java/com/topjohnwu/magisk/superuser/SuReceiver.java new file mode 100644 index 000000000..d39d1f4be --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/superuser/SuReceiver.java @@ -0,0 +1,90 @@ +package com.topjohnwu.magisk.superuser; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Process; +import android.widget.Toast; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.container.Policy; +import com.topjohnwu.magisk.container.SuLogEntry; +import com.topjohnwu.magisk.utils.Const; +import com.topjohnwu.magisk.utils.Utils; + +import java.util.Date; + +public class SuReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + int fromUid, toUid, pid, mode; + String command, action; + Policy policy; + + MagiskManager mm = Utils.getMagiskManager(context); + + if (intent == null) return; + + mode = intent.getIntExtra("mode", -1); + if (mode < 0) return; + + if (mode == Const.Value.NOTIFY_USER_TO_OWNER) { + MagiskManager.toast(R.string.multiuser_hint_owner_request, Toast.LENGTH_LONG); + return; + } + + fromUid = intent.getIntExtra("from.uid", -1); + if (fromUid < 0) return; + if (fromUid == Process.myUid()) return; // Don't show anything if it's Magisk Manager + + action = intent.getStringExtra("action"); + if (action == null) return; + + policy = mm.mDB.getPolicy(fromUid); + if (policy == null) { + try { + policy = new Policy(fromUid, context.getPackageManager()); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return; + } + } + + SuLogEntry log = new SuLogEntry(policy); + + String message; + switch (action) { + case "allow": + message = context.getString(R.string.su_allow_toast, policy.appName); + log.action = true; + break; + case "deny": + message = context.getString(R.string.su_deny_toast, policy.appName); + log.action = false; + break; + default: + return; + } + + if (policy.notification && mm.suNotificationType == Const.Value.NOTIFICATION_TOAST) { + MagiskManager.toast(message, Toast.LENGTH_SHORT); + } + + if (mode == Const.Value.NOTIFY_NORMAL_LOG && policy.logging) { + toUid = intent.getIntExtra("to.uid", -1); + if (toUid < 0) return; + pid = intent.getIntExtra("pid", -1); + if (pid < 0) return; + command = intent.getStringExtra("command"); + if (command == null) return; + log.toUid = toUid; + log.fromPid = pid; + log.command = command; + log.date = new Date(); + mm.mDB.addLog(log); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/BootSigner.java b/app/src/full/java/com/topjohnwu/magisk/utils/BootSigner.java new file mode 100644 index 000000000..82204912d --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/BootSigner.java @@ -0,0 +1,47 @@ +package com.topjohnwu.magisk.utils; + +import android.support.annotation.Keep; + +import com.topjohnwu.utils.SignBoot; + +import java.io.FileInputStream; +import java.io.InputStream; + +public class BootSigner { + + @Keep + public static void main(String[] args) throws Exception { + if (args.length > 0 && "-verify".equals(args[0])) { + String certPath = ""; + if (args.length >= 2) { + /* args[1] is the path to a public key certificate */ + certPath = args[1]; + } + boolean signed = SignBoot.verifySignature(System.in, + certPath.isEmpty() ? null : new FileInputStream(certPath)); + System.exit(signed ? 0 : 1); + } else if (args.length > 0 && "-sign".equals(args[0])) { + InputStream cert = null; + InputStream key = null; + + if (args.length >= 3) { + cert = new FileInputStream(args[1]); + key = new FileInputStream(args[2]); + } + + boolean success = SignBoot.doSignature("/boot", System.in, System.out, cert, key); + System.exit(success ? 0 : 1); + } else { + System.err.println( + "BootSigner [args]\n" + + "Input from stdin, outputs to stdout\n" + + "\n" + + "Actions:\n" + + " -verify [x509.pem]\n" + + " verify image, cert is optional\n" + + " -sign [x509.pem] [pk8]\n" + + " sign image, cert and key pair is optional\n" + ); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/FingerprintHelper.java b/app/src/full/java/com/topjohnwu/magisk/utils/FingerprintHelper.java new file mode 100644 index 000000000..1808f06e5 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/FingerprintHelper.java @@ -0,0 +1,112 @@ +package com.topjohnwu.magisk.utils; + +import android.annotation.TargetApi; +import android.app.KeyguardManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.CancellationSignal; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; + +import com.topjohnwu.magisk.MagiskManager; + +import java.security.KeyStore; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +@TargetApi(Build.VERSION_CODES.M) +public abstract class FingerprintHelper { + + private FingerprintManager manager; + private Cipher cipher; + private CancellationSignal cancel; + + public static boolean canUseFingerprint() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return false; + MagiskManager mm = MagiskManager.get(); + KeyguardManager km = mm.getSystemService(KeyguardManager.class); + FingerprintManager fm = mm.getSystemService(FingerprintManager.class); + return km.isKeyguardSecure() && fm != null && fm.isHardwareDetected() && fm.hasEnrolledFingerprints(); + } + + protected FingerprintHelper() throws Exception { + MagiskManager mm = MagiskManager.get(); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + manager = mm.getSystemService(FingerprintManager.class); + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(Const.SU_KEYSTORE_KEY, null); + if (key == null) { + key = generateKey(); + } + try { + cipher.init(Cipher.ENCRYPT_MODE, key); + } catch (KeyPermanentlyInvalidatedException e) { + // Only happens on Marshmallow + key = generateKey(); + cipher.init(Cipher.ENCRYPT_MODE, key); + } + } + + public abstract void onAuthenticationError(int errorCode, CharSequence errString); + + public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString); + + public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result); + + public abstract void onAuthenticationFailed(); + + public void startAuth() { + cancel = new CancellationSignal(); + FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher); + manager.authenticate(cryptoObject, cancel, 0, new FingerprintManager.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + FingerprintHelper.this.onAuthenticationError(errorCode, errString); + } + + @Override + public void onAuthenticationHelp(int helpCode, CharSequence helpString) { + FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + FingerprintHelper.this.onAuthenticationSucceeded(result); + } + + @Override + public void onAuthenticationFailed() { + FingerprintHelper.this.onAuthenticationFailed(); + } + }, null); + } + + public void cancel() { + if (cancel != null) + cancel.cancel(); + } + + private SecretKey generateKey() throws Exception { + KeyGenerator keygen = KeyGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( + Const.SU_KEYSTORE_KEY, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setInvalidatedByBiometricEnrollment(false); + } + keygen.init(builder.build()); + return keygen.generateKey(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/ISafetyNetHelper.java b/app/src/full/java/com/topjohnwu/magisk/utils/ISafetyNetHelper.java new file mode 100644 index 000000000..e4c329ec4 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/ISafetyNetHelper.java @@ -0,0 +1,22 @@ +package com.topjohnwu.magisk.utils; + +import android.support.annotation.Keep; + +public interface ISafetyNetHelper { + + int CAUSE_SERVICE_DISCONNECTED = 0x01; + int CAUSE_NETWORK_LOST = 0x02; + int RESPONSE_ERR = 0x04; + int CONNECTION_FAIL = 0x08; + + int BASIC_PASS = 0x10; + int CTS_PASS = 0x20; + + void attest(); + int getVersion(); + + interface Callback { + @Keep + void onResponse(int responseCode); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/Logger.java b/app/src/full/java/com/topjohnwu/magisk/utils/Logger.java new file mode 100644 index 000000000..b3f05c111 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/Logger.java @@ -0,0 +1,27 @@ +package com.topjohnwu.magisk.utils; + +import android.util.Log; + +import com.topjohnwu.magisk.BuildConfig; + +import java.util.Locale; + +public class Logger { + + public static void debug(String line) { + if (BuildConfig.DEBUG) + Log.d(Const.DEBUG_TAG, "DEBUG: " + line); + } + + public static void debug(String fmt, Object... args) { + debug(Utils.fmt(fmt, args)); + } + + public static void error(String line) { + Log.e(Const.DEBUG_TAG, "ERROR: " + line); + } + + public static void error(String fmt, Object... args) { + error(Utils.fmt(fmt, args)); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/PatchAPK.java b/app/src/full/java/com/topjohnwu/magisk/utils/PatchAPK.java new file mode 100644 index 000000000..516ed365b --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/PatchAPK.java @@ -0,0 +1,77 @@ +package com.topjohnwu.magisk.utils; + +import com.topjohnwu.utils.JarMap; + +import java.io.OutputStream; +import java.util.jar.JarEntry; + +public class PatchAPK { + + private static int findOffset(byte buf[], byte pattern[]) { + int offset = -1; + for (int i = 0; i < buf.length - pattern.length; ++i) { + boolean match = true; + for (int j = 0; j < pattern.length; ++j) { + if (buf[i + j] != pattern[j]) { + match = false; + break; + } + } + if (match) { + offset = i; + break; + } + } + return offset; + } + + /* It seems that AAPT sometimes generate another type of string format */ + private static boolean fallbackPatch(byte xml[], String from, String to) { + + byte[] target = new byte[from.length() * 2 + 2]; + for (int i = 0; i < from.length(); ++i) { + target[i * 2] = (byte) from.charAt(i); + } + int offset = findOffset(xml, target); + if (offset < 0) + return false; + byte[] dest = new byte[target.length - 2]; + for (int i = 0; i < to.length(); ++i) { + dest[i * 2] = (byte) to.charAt(i); + } + System.arraycopy(dest, 0, xml, offset, dest.length); + return true; + } + + private static boolean findAndPatch(byte xml[], String from, String to) { + byte target[] = (from + '\0').getBytes(); + int offset = findOffset(xml, target); + if (offset < 0) + return fallbackPatch(xml, from, to); + System.arraycopy(to.getBytes(), 0, xml, offset, to.length()); + return true; + } + + public static boolean patchPackageID(String fileName, OutputStream out, String from, String to) { + try { + JarMap apk = new JarMap(fileName); + JarEntry je = apk.getJarEntry(Const.ANDROID_MANIFEST); + byte xml[] = apk.getRawData(je); + + if (!findAndPatch(xml, from, to)) + return false; + if (!findAndPatch(xml, from + ".provider", to + ".provider")) + return false; + + // Write in changes + apk.getOutputStream(je).write(xml); + + // Sign the APK + ZipUtils.signZip(apk, out); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/RootUtils.java b/app/src/full/java/com/topjohnwu/magisk/utils/RootUtils.java new file mode 100644 index 000000000..00729a04d --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/RootUtils.java @@ -0,0 +1,38 @@ +package com.topjohnwu.magisk.utils; + +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFile; + +public class RootUtils { + + public static void init() { + if (Shell.rootAccess()) { + Const.MAGISK_DISABLE_FILE = new SuFile("/cache/.disable_magisk"); + SuFile file = new SuFile("/sbin/.core/img"); + if (file.exists()) { + Const.MAGISK_PATH = file; + } else if ((file = new SuFile("/dev/magisk/img")).exists()) { + Const.MAGISK_PATH = file; + } else { + Const.MAGISK_PATH = new SuFile("/magisk"); + } + Const.MAGISK_HOST_FILE = new SuFile(Const.MAGISK_PATH + "/.core/hosts"); + } + } + + public static void uninstallPkg(String pkg) { + Shell.Sync.su("db_clean " + Const.USER_ID, "pm uninstall " + pkg); + } + + public static void patchDTBO() { + if (Shell.rootAccess()) { + MagiskManager mm = MagiskManager.get(); + if (mm.magiskVersionCode >= Const.MAGISK_VER.DTBO_SUPPORT) { + if (Boolean.parseBoolean(ShellUtils.fastCmd("mm_patch_dtbo"))) + ShowUI.dtboPatchedNotification(); + } + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/ShellInitializer.java b/app/src/full/java/com/topjohnwu/magisk/utils/ShellInitializer.java new file mode 100644 index 000000000..317e2aa14 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/ShellInitializer.java @@ -0,0 +1,36 @@ +package com.topjohnwu.magisk.utils; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.superuser.BusyBox; +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.ShellUtils; + +import java.io.File; +import java.io.InputStream; + +public class ShellInitializer extends Shell.Initializer { + + static { + BusyBox.BB_PATH = new File(Const.BUSYBOX_PATH); + } + + @Override + public boolean onRootShellInit(Context context, @NonNull Shell shell) throws Exception { + try (InputStream magiskUtils = context.getResources().openRawResource(R.raw.util_functions); + InputStream managerUtils = context.getResources().openRawResource(R.raw.utils) + ) { + shell.execTask((in, err, out) -> { + ShellUtils.pump(magiskUtils, in); + ShellUtils.pump(managerUtils, in); + }); + } + shell.run(null, null, + "mount_partitions", + "get_flags", + "run_migrations"); + return true; + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/ShowUI.java b/app/src/full/java/com/topjohnwu/magisk/utils/ShowUI.java new file mode 100644 index 000000000..3add96f34 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/ShowUI.java @@ -0,0 +1,273 @@ +package com.topjohnwu.magisk.utils; + +import android.Manifest; +import android.app.Activity; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.widget.Toast; + +import com.topjohnwu.magisk.FlashActivity; +import com.topjohnwu.magisk.MagiskManager; +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.SplashActivity; +import com.topjohnwu.magisk.asyncs.InstallMagisk; +import com.topjohnwu.magisk.asyncs.MarkDownWindow; +import com.topjohnwu.magisk.asyncs.RestoreImages; +import com.topjohnwu.magisk.components.AlertDialogBuilder; +import com.topjohnwu.magisk.components.SnackbarMaker; +import com.topjohnwu.magisk.receivers.DownloadReceiver; +import com.topjohnwu.magisk.receivers.ManagerUpdate; +import com.topjohnwu.magisk.receivers.RebootReceiver; +import com.topjohnwu.superuser.Shell; + +import java.util.ArrayList; +import java.util.List; + +public class ShowUI { + + public static void magiskUpdateNotification() { + MagiskManager mm = MagiskManager.get(); + + Intent intent = new Intent(mm, SplashActivity.class); + intent.putExtra(Const.Key.OPEN_SECTION, "magisk"); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(mm); + stackBuilder.addParentStack(SplashActivity.class); + stackBuilder.addNextIntent(intent); + PendingIntent pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.NOTIFICATION_CHANNEL); + builder.setSmallIcon(R.drawable.ic_magisk_outline) + .setContentTitle(mm.getString(R.string.magisk_update_title)) + .setContentText(mm.getString(R.string.magisk_update_available, mm.remoteMagiskVersionString)) + .setVibrate(new long[]{0, 100, 100, 100}) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build()); + } + + public static void managerUpdateNotification() { + MagiskManager mm = MagiskManager.get(); + String filename = Utils.fmt("MagiskManager-v%s(%d).apk", + mm.remoteManagerVersionString, mm.remoteManagerVersionCode); + + Intent intent = new Intent(mm, ManagerUpdate.class); + intent.putExtra(Const.Key.INTENT_SET_LINK, mm.managerLink); + intent.putExtra(Const.Key.INTENT_SET_FILENAME, filename); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mm, + Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.NOTIFICATION_CHANNEL); + builder.setSmallIcon(R.drawable.ic_magisk_outline) + .setContentTitle(mm.getString(R.string.manager_update_title)) + .setContentText(mm.getString(R.string.manager_download_install)) + .setVibrate(new long[]{0, 100, 100, 100}) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build()); + } + + public static void dtboPatchedNotification() { + MagiskManager mm = MagiskManager.get(); + + Intent intent = new Intent(mm, RebootReceiver.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mm, + Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.NOTIFICATION_CHANNEL); + builder.setSmallIcon(R.drawable.ic_magisk_outline) + .setContentTitle(mm.getString(R.string.dtbo_patched_title)) + .setContentText(mm.getString(R.string.dtbo_patched_reboot)) + .setVibrate(new long[]{0, 100, 100, 100}) + .addAction(R.drawable.ic_refresh, mm.getString(R.string.reboot), pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build()); + } + + public static void envFixDialog(Activity activity) { + MagiskManager mm = Utils.getMagiskManager(activity); + String filename = Utils.fmt("Magisk-v%s(%d).zip", + mm.remoteMagiskVersionString, mm.remoteMagiskVersionCode); + new AlertDialogBuilder(activity) + .setTitle(R.string.env_fix_title) + .setMessage(R.string.env_fix_msg) + .setCancelable(true) + .setPositiveButton(R.string.yes, (d, i) -> { + Utils.dlAndReceive(activity, new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + new InstallMagisk(activity, uri).exec(); + } + }, mm.magiskLink, filename); + }) + .setNegativeButton(R.string.no_thanks, null) + .show(); + } + + public static void magiskInstallDialog(Activity activity) { + MagiskManager mm = Utils.getMagiskManager(activity); + String filename = Utils.fmt("Magisk-v%s(%d).zip", + mm.remoteMagiskVersionString, mm.remoteMagiskVersionCode); + AlertDialog.Builder b = new AlertDialogBuilder(activity) + .setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.magisk))) + .setMessage(mm.getString(R.string.repo_install_msg, filename)) + .setCancelable(true) + .setPositiveButton(R.string.install, (d, i) -> { + List options = new ArrayList<>(); + options.add(mm.getString(R.string.download_zip_only)); + options.add(mm.getString(R.string.patch_boot_file)); + if (Shell.rootAccess()) { + options.add(mm.getString(R.string.direct_install)); + } + new AlertDialog.Builder(activity) + .setTitle(R.string.select_method) + .setItems( + options.toArray(new String [0]), + (dialog, idx) -> { + DownloadReceiver receiver = null; + switch (idx) { + case 1: + if (mm.remoteMagiskVersionCode < 1400) { + MagiskManager.toast(R.string.no_boot_file_patch_support, Toast.LENGTH_LONG); + return; + } + MagiskManager.toast(R.string.boot_file_patch_msg, Toast.LENGTH_LONG); + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + ((com.topjohnwu.magisk.components.Activity) activity) + .startActivityForResult(intent, Const.ID.SELECT_BOOT, + (requestCode, resultCode, data) -> { + if (requestCode == Const.ID.SELECT_BOOT + && resultCode == Activity.RESULT_OK && data != null) { + Utils.dlAndReceive( + activity, + new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + Intent intent = new Intent(mm, FlashActivity.class); + intent.setData(uri) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Const.Key.FLASH_SET_BOOT, data.getData()) + .putExtra(Const.Key.FLASH_ACTION, Const.Value.PATCH_BOOT); + mm.startActivity(intent); + } + }, + mm.magiskLink, + filename + ); + } + }); + return; + case 0: + receiver = new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + SnackbarMaker.showUri(activity, uri); + } + }; + break; + case 2: + receiver = new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + Intent intent = new Intent(mm, FlashActivity.class); + intent.setData(uri).putExtra(Const.Key.FLASH_ACTION, + Const.Value.FLASH_MAGISK); + activity.startActivity(intent); + } + }; + break; + case 3: + receiver = new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + Intent intent = new Intent(mm, FlashActivity.class); + intent.setData(uri).putExtra(Const.Key.FLASH_ACTION, + Const.Value.FLASH_SECOND_SLOT); + activity.startActivity(intent); + } + }; + default: + } + Utils.dlAndReceive(activity, receiver, mm.magiskLink, filename); + } + ).show(); + }) + .setNegativeButton(R.string.no_thanks, null); + if (!TextUtils.isEmpty(mm.magiskNoteLink)) { + b.setNeutralButton(R.string.release_notes, (d, i) -> { + if (mm.magiskNoteLink.contains("forum.xda-developers")) { + // Open forum links in browser + Intent openLink = new Intent(Intent.ACTION_VIEW, Uri.parse(mm.magiskNoteLink)); + openLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mm.startActivity(openLink); + } else { + new MarkDownWindow(activity, null, mm.magiskNoteLink).exec(); + } + }); + } + b.show(); + } + + public static void managerInstallDialog(Activity activity) { + MagiskManager mm = Utils.getMagiskManager(activity); + String filename = Utils.fmt("MagiskManager-v%s(%d).apk", + mm.remoteManagerVersionString, mm.remoteManagerVersionCode); + AlertDialog.Builder b = new AlertDialogBuilder(activity) + .setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.app_name))) + .setMessage(mm.getString(R.string.repo_install_msg, filename)) + .setCancelable(true) + .setPositiveButton(R.string.install, (d, i) -> { + com.topjohnwu.magisk.components.Activity.runWithPermission(activity, + new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, () -> { + Intent intent = new Intent(mm, ManagerUpdate.class); + intent.putExtra(Const.Key.INTENT_SET_LINK, mm.managerLink); + intent.putExtra(Const.Key.INTENT_SET_FILENAME, filename); + mm.sendBroadcast(intent); + }); + }) + .setNegativeButton(R.string.no_thanks, null); + if (!TextUtils.isEmpty(mm.managerNoteLink)) { + b.setNeutralButton(R.string.app_changelog, (d, i) -> + new MarkDownWindow(activity, null, mm.managerNoteLink).exec()); + } + b.show(); + } + + public static void uninstallDialog(Activity activity) { + MagiskManager mm = Utils.getMagiskManager(activity); + AlertDialog.Builder b = new AlertDialogBuilder(activity) + .setTitle(R.string.uninstall_magisk_title) + .setMessage(R.string.uninstall_magisk_msg) + .setNeutralButton(R.string.restore_img, (d, i) -> new RestoreImages(activity).exec()); + if (!TextUtils.isEmpty(mm.uninstallerLink)) { + b.setPositiveButton(R.string.complete_uninstall, (d, i) -> + Utils.dlAndReceive(activity, new DownloadReceiver() { + @Override + public void onDownloadDone(Context context, Uri uri) { + Intent intent = new Intent(context, FlashActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setData(uri) + .putExtra(Const.Key.FLASH_ACTION, Const.Value.UNINSTALL); + context.startActivity(intent); + } + }, mm.uninstallerLink, "magisk-uninstaller.zip")); + } + b.show(); + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/Topic.java b/app/src/full/java/com/topjohnwu/magisk/utils/Topic.java new file mode 100644 index 000000000..d174c576a --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/Topic.java @@ -0,0 +1,99 @@ +package com.topjohnwu.magisk.utils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class Topic { + + private static final int NON_INIT = 0; + private static final int PENDING = 1; + private static final int PUBLISHED = 2; + + private int state = NON_INIT; + private List> subscribers; + private Object[] results; + + public Topic() { + subscribers = new SyncArrayList<>(); + } + + public synchronized void subscribe(Subscriber sub) { + subscribers.add(new WeakReference<>(sub)); + } + + public synchronized void unsubscribe() { + subscribers = new SyncArrayList<>(); + } + + public synchronized void unsubscribe(Subscriber sub) { + List> subs = subscribers; + subscribers = new ArrayList<>(); + for (WeakReference subscriber : subs) { + if (subscriber.get() != null && subscriber.get() != sub) + subscribers.add(subscriber); + } + } + + public void reset() { + state = NON_INIT; + results = null; + } + + public boolean isPublished() { + return state == PUBLISHED; + } + + public void publish() { + publish(true); + } + + public void publish(boolean record, Object... results) { + if (record) + state = PUBLISHED; + this.results = results; + // Snapshot + List> subs = subscribers; + for (WeakReference subscriber : subs) { + if (subscriber != null && subscriber.get() != null) + subscriber.get().onTopicPublished(this); + } + } + + public Object[] getResults() { + return results; + } + + public boolean isPending() { + return state == PENDING; + } + + public void setPending() { + state = PENDING; + } + + public interface Subscriber { + default void subscribeTopics() { + for (Topic topic : getSubscription()) { + if (topic.isPublished()) { + onTopicPublished(topic); + } + topic.subscribe(this); + } + } + default void unsubscribeTopics() { + for (Topic event : getSubscription()) { + event.unsubscribe(this); + } + } + void onTopicPublished(Topic topic); + Topic[] getSubscription(); + } + + private static class SyncArrayList extends ArrayList { + @Override + public synchronized boolean add(E e) { + return super.add(e); + } + } +} diff --git a/app/src/full/java/com/topjohnwu/magisk/utils/ZipUtils.java b/app/src/full/java/com/topjohnwu/magisk/utils/ZipUtils.java new file mode 100644 index 000000000..f18eb7017 --- /dev/null +++ b/app/src/full/java/com/topjohnwu/magisk/utils/ZipUtils.java @@ -0,0 +1,69 @@ +package com.topjohnwu.magisk.utils; + +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileOutputStream; +import com.topjohnwu.utils.JarMap; +import com.topjohnwu.utils.SignAPK; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ZipUtils { + + public static void unzip(File zip, File folder, String path, boolean junkPath) throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(zip)); + unzip(in, folder, path, junkPath); + in.close(); + } + + public static void unzip(InputStream zip, File folder, String path, boolean junkPath) throws IOException { + try { + ZipInputStream zipfile = new ZipInputStream(zip); + ZipEntry entry; + while ((entry = zipfile.getNextEntry()) != null) { + if (!entry.getName().startsWith(path) || entry.isDirectory()){ + // Ignore directories, only create files + continue; + } + String name; + if (junkPath) { + name = entry.getName().substring(entry.getName().lastIndexOf('/') + 1); + } else { + name = entry.getName(); + } + File dest = new File(folder, name); + if (!dest.getParentFile().exists() && !dest.getParentFile().mkdirs()) { + dest = new SuFile(folder, name); + dest.getParentFile().mkdirs(); + } + try (OutputStream out = new SuFileOutputStream(dest)) { + ShellUtils.pump(zipfile, out); + } + } + } + catch(IOException e) { + e.printStackTrace(); + throw e; + } + } + + public static void signZip(File input, File output) throws Exception { + try (JarMap map = new JarMap(input, false); + BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(output))) { + signZip(map, out); + } + } + + public static void signZip(JarMap input, OutputStream output) throws Exception { + SignAPK.signZip(null, null, input, output); + } +} diff --git a/app/src/full/res/drawable-nodpi/logo.png b/app/src/full/res/drawable-nodpi/logo.png new file mode 100644 index 000000000..94f74a0ca Binary files /dev/null and b/app/src/full/res/drawable-nodpi/logo.png differ diff --git a/app/src/full/res/drawable-v26/sc_cloud_download.xml b/app/src/full/res/drawable-v26/sc_cloud_download.xml new file mode 100644 index 000000000..734f23865 --- /dev/null +++ b/app/src/full/res/drawable-v26/sc_cloud_download.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable-v26/sc_extension.xml b/app/src/full/res/drawable-v26/sc_extension.xml new file mode 100644 index 000000000..a7a98ef76 --- /dev/null +++ b/app/src/full/res/drawable-v26/sc_extension.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable-v26/sc_magiskhide.xml b/app/src/full/res/drawable-v26/sc_magiskhide.xml new file mode 100644 index 000000000..02a59ce4e --- /dev/null +++ b/app/src/full/res/drawable-v26/sc_magiskhide.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable-v26/sc_superuser.xml b/app/src/full/res/drawable-v26/sc_superuser.xml new file mode 100644 index 000000000..505088495 --- /dev/null +++ b/app/src/full/res/drawable-v26/sc_superuser.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_add.xml b/app/src/full/res/drawable/ic_add.xml new file mode 100644 index 000000000..0258249cc --- /dev/null +++ b/app/src/full/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_archive.xml b/app/src/full/res/drawable/ic_archive.xml new file mode 100644 index 000000000..8b18a9d56 --- /dev/null +++ b/app/src/full/res/drawable/ic_archive.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_arrow.xml b/app/src/full/res/drawable/ic_arrow.xml new file mode 100644 index 000000000..46fd51124 --- /dev/null +++ b/app/src/full/res/drawable/ic_arrow.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_attach_money.xml b/app/src/full/res/drawable/ic_attach_money.xml new file mode 100644 index 000000000..4fb28d7f4 --- /dev/null +++ b/app/src/full/res/drawable/ic_attach_money.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_bug_report.xml b/app/src/full/res/drawable/ic_bug_report.xml new file mode 100644 index 000000000..0ac39ddc1 --- /dev/null +++ b/app/src/full/res/drawable/ic_bug_report.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_cancel.xml b/app/src/full/res/drawable/ic_cancel.xml new file mode 100644 index 000000000..e6545bf8a --- /dev/null +++ b/app/src/full/res/drawable/ic_cancel.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_check_circle.xml b/app/src/full/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..45d1b3076 --- /dev/null +++ b/app/src/full/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_cloud_download.xml b/app/src/full/res/drawable/ic_cloud_download.xml new file mode 100644 index 000000000..4aaf5ebc1 --- /dev/null +++ b/app/src/full/res/drawable/ic_cloud_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_delete.xml b/app/src/full/res/drawable/ic_delete.xml new file mode 100644 index 000000000..9b7a8c7e5 --- /dev/null +++ b/app/src/full/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_device_information.xml b/app/src/full/res/drawable/ic_device_information.xml new file mode 100644 index 000000000..2c4e5c7f4 --- /dev/null +++ b/app/src/full/res/drawable/ic_device_information.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_extension.xml b/app/src/full/res/drawable/ic_extension.xml new file mode 100644 index 000000000..549fdce23 --- /dev/null +++ b/app/src/full/res/drawable/ic_extension.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_file_download_black.xml b/app/src/full/res/drawable/ic_file_download_black.xml new file mode 100644 index 000000000..d05655222 --- /dev/null +++ b/app/src/full/res/drawable/ic_file_download_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_fingerprint.xml b/app/src/full/res/drawable/ic_fingerprint.xml new file mode 100644 index 000000000..f650f7445 --- /dev/null +++ b/app/src/full/res/drawable/ic_fingerprint.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_github.xml b/app/src/full/res/drawable/ic_github.xml new file mode 100644 index 000000000..83be6c04e --- /dev/null +++ b/app/src/full/res/drawable/ic_github.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_help.xml b/app/src/full/res/drawable/ic_help.xml new file mode 100644 index 000000000..98d384f8b --- /dev/null +++ b/app/src/full/res/drawable/ic_help.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_history.xml b/app/src/full/res/drawable/ic_history.xml new file mode 100644 index 000000000..6b9f91a37 --- /dev/null +++ b/app/src/full/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_info_outline.xml b/app/src/full/res/drawable/ic_info_outline.xml new file mode 100644 index 000000000..2d66b0bf6 --- /dev/null +++ b/app/src/full/res/drawable/ic_info_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_language.xml b/app/src/full/res/drawable/ic_language.xml new file mode 100644 index 000000000..85b37743a --- /dev/null +++ b/app/src/full/res/drawable/ic_language.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_magisk_outline.xml b/app/src/full/res/drawable/ic_magisk_outline.xml new file mode 100644 index 000000000..7bbc6e822 --- /dev/null +++ b/app/src/full/res/drawable/ic_magisk_outline.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/full/res/drawable/ic_magiskhide.xml b/app/src/full/res/drawable/ic_magiskhide.xml new file mode 100644 index 000000000..6a3ce181d --- /dev/null +++ b/app/src/full/res/drawable/ic_magiskhide.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/full/res/drawable/ic_menu_overflow_material.xml b/app/src/full/res/drawable/ic_menu_overflow_material.xml new file mode 100644 index 000000000..4ab8f2f1d --- /dev/null +++ b/app/src/full/res/drawable/ic_menu_overflow_material.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_more.xml b/app/src/full/res/drawable/ic_more.xml new file mode 100644 index 000000000..da83afdb1 --- /dev/null +++ b/app/src/full/res/drawable/ic_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_notifications.xml b/app/src/full/res/drawable/ic_notifications.xml new file mode 100644 index 000000000..be9f8368d --- /dev/null +++ b/app/src/full/res/drawable/ic_notifications.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_person.xml b/app/src/full/res/drawable/ic_person.xml new file mode 100644 index 000000000..234d73d8d --- /dev/null +++ b/app/src/full/res/drawable/ic_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_refresh.xml b/app/src/full/res/drawable/ic_refresh.xml new file mode 100644 index 000000000..635d77eb9 --- /dev/null +++ b/app/src/full/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_safetynet.xml b/app/src/full/res/drawable/ic_safetynet.xml new file mode 100644 index 000000000..a380b9bb4 --- /dev/null +++ b/app/src/full/res/drawable/ic_safetynet.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_save.xml b/app/src/full/res/drawable/ic_save.xml new file mode 100644 index 000000000..194ffafd4 --- /dev/null +++ b/app/src/full/res/drawable/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_settings.xml b/app/src/full/res/drawable/ic_settings.xml new file mode 100644 index 000000000..ace746c40 --- /dev/null +++ b/app/src/full/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_sort.xml b/app/src/full/res/drawable/ic_sort.xml new file mode 100644 index 000000000..0fd497923 --- /dev/null +++ b/app/src/full/res/drawable/ic_sort.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_splash_activity.xml b/app/src/full/res/drawable/ic_splash_activity.xml new file mode 100644 index 000000000..7b0082a31 --- /dev/null +++ b/app/src/full/res/drawable/ic_splash_activity.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_su_warning.xml b/app/src/full/res/drawable/ic_su_warning.xml new file mode 100644 index 000000000..87e5f4ac4 --- /dev/null +++ b/app/src/full/res/drawable/ic_su_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_superuser.xml b/app/src/full/res/drawable/ic_superuser.xml new file mode 100644 index 000000000..01fb6a803 --- /dev/null +++ b/app/src/full/res/drawable/ic_superuser.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/ic_undelete.xml b/app/src/full/res/drawable/ic_undelete.xml new file mode 100644 index 000000000..8546fea72 --- /dev/null +++ b/app/src/full/res/drawable/ic_undelete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_update.xml b/app/src/full/res/drawable/ic_update.xml new file mode 100644 index 000000000..b2a2c14f0 --- /dev/null +++ b/app/src/full/res/drawable/ic_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/full/res/drawable/ic_xda.xml b/app/src/full/res/drawable/ic_xda.xml new file mode 100644 index 000000000..918a5497b --- /dev/null +++ b/app/src/full/res/drawable/ic_xda.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/sc_cloud_download.xml b/app/src/full/res/drawable/sc_cloud_download.xml new file mode 100644 index 000000000..1a7dae048 --- /dev/null +++ b/app/src/full/res/drawable/sc_cloud_download.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/sc_extension.xml b/app/src/full/res/drawable/sc_extension.xml new file mode 100644 index 000000000..21e823a39 --- /dev/null +++ b/app/src/full/res/drawable/sc_extension.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/sc_magiskhide.xml b/app/src/full/res/drawable/sc_magiskhide.xml new file mode 100644 index 000000000..b388a2f3f --- /dev/null +++ b/app/src/full/res/drawable/sc_magiskhide.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/drawable/sc_superuser.xml b/app/src/full/res/drawable/sc_superuser.xml new file mode 100644 index 000000000..0e6c58937 --- /dev/null +++ b/app/src/full/res/drawable/sc_superuser.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/layout/activity_about.xml b/app/src/full/res/layout/activity_about.xml new file mode 100644 index 000000000..2d77018df --- /dev/null +++ b/app/src/full/res/layout/activity_about.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/layout/activity_flash.xml b/app/src/full/res/layout/activity_flash.xml new file mode 100644 index 000000000..d33e6e726 --- /dev/null +++ b/app/src/full/res/layout/activity_flash.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + +