diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..055a8c836 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ +###### Include the following: + - Xposed Version: `85` + - [ ] Xposed Systemless + - Device OS version: `6.0.1` + - Device Manufacturer: `LG` + - Device Name: `Nexus 5` + - Material Xposed Installer version: `14/09/2016` + +###### Reproduction Steps + +1. +2. +3. + +###### Expected Result + + +###### Actual Result diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..cdca707c0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ +_If your pull request is a translation update, then the title of this pull must contains 'string' or 'translation'_ diff --git a/.gitignore b/.gitignore index b3f9dc9c7..e3ae87a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,11 @@ -# Built application files *.apk *.ap_ *.iml - -# Files for the Dalvik VM *.dex - -# Java class files *.class - -# Generated files -bin/ -gen/ - -# User-specific stuff: .idea/ - -# Gradle files .gradle/ build/ - -# Local configuration file (sdk path, etc) local.properties - -# Proguard folder generated by Eclipse -proguard/ - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation/ \ No newline at end of file +app/lint.xml +app/proguard-project.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..b358f0919 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: android +jdk: oraclejdk8 +android: + components: + - tools + - platform-tools + - tools + - build-tools-28.0.3 + - android-28 + - extra-android-support + - extra-android-m2repository + - extra-google-m2repository diff --git a/README.md b/README.md new file mode 100644 index 000000000..1845b6897 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +XposedInstaller +=============== +[![Build Status](https://travis-ci.org/DVDAndroid/XposedInstaller.svg?branch=material)](https://travis-ci.org/DVDAndroid/XposedInstaller) + +This is a materialised version of Xposed Installer + +[Show this project on XDA](http://forum.xda-developers.com/xposed/material-design-xposed-installer-t3137758) + +Credits +------- + +[rovo89](https://github.com/rovo89) for original XposedInstaller +[BioHaZard1](https://github.com/BioHaZard1) for graphics changes + +[afollestad](https://github.com/afollestad) for his [material-dialogs](https://github.com/afollestad/material-dialogs) library diff --git a/app/Xposed-Disabler-Recovery.zip b/app/Xposed-Disabler-Recovery.zip new file mode 100644 index 000000000..4e45bd4cc Binary files /dev/null and b/app/Xposed-Disabler-Recovery.zip differ diff --git a/app/Xposed-Installer-Recovery.zip b/app/Xposed-Installer-Recovery.zip new file mode 100644 index 000000000..f9069944c Binary files /dev/null and b/app/Xposed-Installer-Recovery.zip differ diff --git a/app/build.gradle b/app/build.gradle index dae73c5a4..fbc019216 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,30 +1,50 @@ apply plugin: 'com.android.application' +ext { + VERSION_DATE = '18/05/19' + SUPPORT_LIBRARY_VERSION = '27.0.2' + BUILD_TOOLS = "28.0.3" + APP_VERSION = '"1558200000000"' +} + android { - compileSdkVersion 22 - buildToolsVersion "23.0.0 rc3" + compileSdkVersion 27 + buildToolsVersion BUILD_TOOLS + + lintOptions { + abortOnError false + } defaultConfig { applicationId "de.robv.android.xposed.installer" - minSdkVersion 14 - targetSdkVersion 22 - versionCode 37 - versionName "3.0 alpha4" + minSdkVersion 16 + //noinspection OldTargetApi + targetSdkVersion 25 + versionCode 42 + versionName "3.1.5 by dvdandroid - " + VERSION_DATE + project.ext.set("archivesBaseName", "XposedInstaller_by_dvdandroid_" + VERSION_DATE.replaceAll("/", "_")) + + buildConfigField "String", "APP_VERSION", APP_VERSION } buildTypes { release { - minifyEnabled true - proguardFiles 'proguard-project.txt' + minifyEnabled false } } } dependencies { - compile 'com.android.support:cardview-v7:22.2.1' - compile 'com.android.support:recyclerview-v7:22.2.1' - compile 'com.android.support:design:22.2.1' - compile 'com.github.machinarius:preferencefragment:0.1.1' - compile 'se.emilsjolander:stickylistheaders:2.7.0' - compile fileTree(dir: 'libs', include: ['*.jar']) + implementation "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" + implementation "com.android.support:design:$SUPPORT_LIBRARY_VERSION" + implementation "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION" + implementation 'com.afollestad.material-dialogs:commons:0.9.0.2' + implementation 'com.github.machinarius:preferencefragment:0.1.1' + implementation 'com.github.mtotschnig:StickyListHeaders:2.7.1' + implementation 'eu.chainfire:libsuperuser:1.0.0.201608240809' + implementation 'com.squareup.picasso:picasso:2.5.2' + implementation 'de.psdev.licensesdialog:licensesdialog:1.8.3' + implementation 'com.annimon:stream:1.1.9' + implementation 'com.google.code.gson:gson:2.8.2' + implementation fileTree(include: ['*.jar'], dir: 'libs') } diff --git a/app/libs/libsuperuser-185868.jar b/app/libs/libsuperuser-185868.jar deleted file mode 100644 index a80ac02c3..000000000 Binary files a/app/libs/libsuperuser-185868.jar and /dev/null differ diff --git a/app/lint.xml b/app/lint.xml index 38567987d..b8e8d4ccc 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -1,10 +1,10 @@ - - + + - + \ No newline at end of file diff --git a/app/proguard-project.txt b/app/proguard-project.txt index 510d188fa..c361a7195 100644 --- a/app/proguard-project.txt +++ b/app/proguard-project.txt @@ -1,7 +1,7 @@ -dontobfuscate # These are mostly picked from proguard-android-optimize.txt --optimizations !code/allocation/variable,!code/simplification/cast,!field/*,!class/merging/*,!method/propagation/returnvalue,!method/inlining/* +-optimizations !code/allocation/variable,!code/simplification/cast,!field/*,!class/merging/* -optimizationpasses 5 -allowaccessmodification -dontpreverify @@ -28,3 +28,4 @@ # These are ok as well -dontwarn android.os.FileUtils -dontwarn com.emilsjolander.components.stickylistheaders.** +-dontwarn com.squareup.picasso.** \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 63b1f1a06..b4cae7ec8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,77 +1,233 @@ - + - - - - - - + + + + + + + - + tools:ignore="ManifestResource" > + android:exported="true" + android:label="@string/app_name"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:exported="true" + android:theme="@style/Theme.XposedInstaller.Transparent"> - - - + + + + + - - - - + + + + + + - + + - + - + + + + + + + - + + + + + + + - + - - - - + + + + + - - + android:permission="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"> - + + + + + + + + + + + + + + + + - - + \ No newline at end of file diff --git a/app/src/main/assets/XposedBridge.jar b/app/src/main/assets/XposedBridge.jar new file mode 100644 index 000000000..21f6f5f17 Binary files /dev/null and b/app/src/main/assets/XposedBridge.jar differ diff --git a/app/src/main/assets/arm/app_process_xposed_sdk15 b/app/src/main/assets/arm/app_process_xposed_sdk15 new file mode 100644 index 000000000..243cb98e3 Binary files /dev/null and b/app/src/main/assets/arm/app_process_xposed_sdk15 differ diff --git a/app/src/main/assets/arm/app_process_xposed_sdk16 b/app/src/main/assets/arm/app_process_xposed_sdk16 new file mode 100644 index 000000000..6e0e90860 Binary files /dev/null and b/app/src/main/assets/arm/app_process_xposed_sdk16 differ diff --git a/app/src/main/assets/arm/app_process_xposed_sdk19 b/app/src/main/assets/arm/app_process_xposed_sdk19 new file mode 100644 index 000000000..ed0ac7692 Binary files /dev/null and b/app/src/main/assets/arm/app_process_xposed_sdk19 differ diff --git a/app/src/main/assets/arm/busybox-xposed b/app/src/main/assets/arm/busybox-xposed index d83f9dbfa..e24b68e87 100644 Binary files a/app/src/main/assets/arm/busybox-xposed and b/app/src/main/assets/arm/busybox-xposed differ diff --git a/app/src/main/assets/x86/app_process_xposed_sdk15 b/app/src/main/assets/x86/app_process_xposed_sdk15 new file mode 100644 index 000000000..846676f90 Binary files /dev/null and b/app/src/main/assets/x86/app_process_xposed_sdk15 differ diff --git a/app/src/main/assets/x86/app_process_xposed_sdk16 b/app/src/main/assets/x86/app_process_xposed_sdk16 new file mode 100644 index 000000000..ec3957544 Binary files /dev/null and b/app/src/main/assets/x86/app_process_xposed_sdk16 differ diff --git a/app/src/main/assets/x86/app_process_xposed_sdk19 b/app/src/main/assets/x86/app_process_xposed_sdk19 new file mode 100644 index 000000000..ec3957544 Binary files /dev/null and b/app/src/main/assets/x86/app_process_xposed_sdk19 differ diff --git a/app/src/main/assets/x86/busybox-xposed b/app/src/main/assets/x86/busybox-xposed index fcc0b27ac..7825113fd 100644 Binary files a/app/src/main/assets/x86/busybox-xposed and b/app/src/main/assets/x86/busybox-xposed differ diff --git a/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java b/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java index f79484458..6884dcac6 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/AboutActivity.java @@ -1,83 +1,170 @@ package de.robv.android.xposed.installer; +import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.text.method.LinkMovementMethod; +import android.text.Html; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.afollestad.materialdialogs.MaterialDialog; + +import de.psdev.licensesdialog.LicensesDialog; +import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20; +import de.psdev.licensesdialog.licenses.MITLicense; +import de.psdev.licensesdialog.model.Notice; +import de.psdev.licensesdialog.model.Notices; +import de.robv.android.xposed.installer.util.NavUtil; import de.robv.android.xposed.installer.util.ThemeUtil; -import de.robv.android.xposed.installer.util.UIUtil; + +import static android.content.Intent.ACTION_SEND; +import static android.content.Intent.EXTRA_TEXT; +import static de.robv.android.xposed.installer.XposedApp.darkenColor; public class AboutActivity extends XposedBaseActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ThemeUtil.setTheme(this); - setContentView(R.layout.activity_container); - - if (UIUtil.isLollipop()) { - this.getWindow().setStatusBarColor(this.getResources().getColor(R.color.colorPrimaryDark)); - } - - Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - - mToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setTitle(R.string.nav_item_about); - ab.setDisplayHomeAsUpEnabled(true); - } - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .add(R.id.container, new AboutFragment()) - .commit(); - } - } - - public static class AboutFragment extends Fragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.tab_about, container, false); - - try { - String packageName = getActivity().getPackageName(); - String version = getActivity().getPackageManager().getPackageInfo(packageName, 0).versionName; - ((TextView) v.findViewById(R.id.version)).setText(version); - } catch (NameNotFoundException e) { - // should not happen - } - - ((TextView) v.findViewById(R.id.about_developers)).setMovementMethod(LinkMovementMethod.getInstance()); - ((TextView) v.findViewById(R.id.about_libraries)).setMovementMethod(LinkMovementMethod.getInstance()); - - String translator = getResources().getString(R.string.translator); - if (translator.isEmpty()) { - v.findViewById(R.id.about_translator_label).setVisibility(View.GONE); - v.findViewById(R.id.about_translator).setVisibility(View.GONE); - } else { - ((TextView) v.findViewById(R.id.about_translator)).setMovementMethod(LinkMovementMethod.getInstance()); - } - - return v; - } - } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.nav_item_about); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, R.string.details); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction().add(R.id.container, new AboutFragment()).commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_about, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Intent sharingIntent = new Intent(ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(EXTRA_TEXT, getString(R.string.share_app_text, getString(R.string.support_material_xda))); + startActivity(Intent.createChooser(sharingIntent, getString(R.string.share))); + + return super.onOptionsItemSelected(item); + } + + public void openLink(View view) { + NavUtil.startURL(this, view.getTag().toString()); + } + + public static class AboutFragment extends Fragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + if (Build.VERSION.SDK_INT >= 21) + getActivity().getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(getActivity()), 0.85f)); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_about, container, false); + + View changelogView = v.findViewById(R.id.changelogView); + View licensesView = v.findViewById(R.id.licensesView); + View translatorsView = v.findViewById(R.id.translatorsView); + View sourceCodeView = v.findViewById(R.id.sourceCodeView); + + String packageName = getActivity().getPackageName(); + String translator = getResources().getString(R.string.translator); + + SharedPreferences prefs = getContext().getSharedPreferences(packageName + "_preferences", MODE_PRIVATE); + + final String changes = prefs.getString("changelog_" + BuildConfig.APP_VERSION, null); + + if (changes == null) { + changelogView.setVisibility(View.GONE); + } else { + changelogView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new MaterialDialog.Builder(getContext()) + .title(R.string.changes) + .content(Html.fromHtml(changes)) + .positiveText(android.R.string.ok).show(); + } + }); + } + + try { + String version = getActivity().getPackageManager().getPackageInfo(packageName, 0).versionName; + ((TextView) v.findViewById(R.id.app_version)).setText(version); + } catch (NameNotFoundException ignored) { + } + + licensesView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createLicenseDialog(); + } + }); + + sourceCodeView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), getString(R.string.about_source)); + } + }); + + if (translator.isEmpty()) { + translatorsView.setVisibility(View.GONE); + } + + return v; + } + + private void createLicenseDialog() { + Notices notices = new Notices(); + notices.addNotice(new Notice("material-dialogs", "https://github.com/afollestad/material-dialogs", "Copyright (c) 2014-2016 Aidan Michael Follestad", new MITLicense())); + notices.addNotice(new Notice("StickyListHeaders", "https://github.com/emilsjolander/StickyListHeaders", "Emil Sjölander", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("PreferenceFragment-Compat", "https://github.com/Machinarius/PreferenceFragment-Compat", "machinarius", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("libsuperuser", "https://github.com/Chainfire/libsuperuser", "Copyright (C) 2012-2015 Jorrit \"Chainfire\" Jongma", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("picasso", "https://github.com/square/picasso", "Copyright 2013 Square, Inc.", new ApacheSoftwareLicense20())); + + new LicensesDialog.Builder(getActivity()) + .setNotices(notices) + .setIncludeOwnLicense(true) + .build() + .show(); + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java index 4c0a831cb..b51fd6dd4 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsActivity.java @@ -1,9 +1,13 @@ package de.robv.android.xposed.installer; -import java.util.List; - +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -12,11 +16,15 @@ import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.text.TextUtils; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.TextView; + +import java.util.List; + import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; @@ -24,204 +32,313 @@ import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.RepoLoader.RepoListener; import de.robv.android.xposed.installer.util.ThemeUtil; -import de.robv.android.xposed.installer.util.UIUtil; +import static de.robv.android.xposed.installer.XposedApp.TAG; +import static de.robv.android.xposed.installer.XposedApp.darkenColor; public class DownloadDetailsActivity extends XposedBaseActivity implements RepoListener, ModuleListener { - private ViewPager mPager; - private String mPackageName; - private static RepoLoader sRepoLoader = RepoLoader.getInstance(); - private static ModuleUtil sModuleUtil = ModuleUtil.getInstance(); - private Module mModule; - private InstalledModule mInstalledModule; - private Toolbar mToolbar; - private TabLayout mTabLayout; - - public static final int DOWNLOAD_DESCRIPTION = 0; - public static final int DOWNLOAD_VERSIONS = 1; - public static final int DOWNLOAD_SETTINGS = 2; - - @Override - public void onCreate(Bundle savedInstanceState) { - ThemeUtil.setTheme(this); - - mPackageName = getModulePackageName(); - mModule = sRepoLoader.getModule(mPackageName); - - mInstalledModule = ModuleUtil.getInstance().getModule(mPackageName); - - super.onCreate(savedInstanceState); - sRepoLoader.addListener(this, false); - sModuleUtil.addListener(this); - - if (mModule != null) { - setContentView(R.layout.activity_download_details); - - if (UIUtil.isLollipop()) { - this.getWindow().setStatusBarColor(this.getResources().getColor(R.color.colorPrimaryDark)); - } - - mToolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - - mToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setTitle(R.string.nav_item_download); - ab.setDisplayHomeAsUpEnabled(true); - } - - ((TextView) findViewById(android.R.id.title)).setText(mModule.name); - - setupTabs(); - - // Updates available => start on the versions page - if (mInstalledModule != null && mInstalledModule.isUpdate(sRepoLoader.getLatestVersion(mModule))) - mPager.setCurrentItem(DOWNLOAD_VERSIONS); - - } else { - setContentView(R.layout.activity_download_details_not_found); - - TextView txtMessage = (TextView) findViewById(android.R.id.message); - txtMessage.setText(getResources().getString(R.string.download_details_not_found, mPackageName)); - - findViewById(R.id.reload).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - v.setEnabled(false); - sRepoLoader.triggerReload(true); - } - }); - } - } - - private void setupTabs() { - mPager = (ViewPager) findViewById(R.id.download_pager); - mPager.setAdapter(new SwipeFragmentPagerAdapter(getSupportFragmentManager())); - mTabLayout = (TabLayout) findViewById(R.id.sliding_tabs); - mTabLayout.setupWithViewPager(mPager); - } - - - private String getModulePackageName() { - Uri uri = getIntent().getData(); - if (uri == null) - return null; - - String scheme = uri.getScheme(); - if (TextUtils.isEmpty(scheme)) { - return null; - } else if (scheme.equals("package")) { - return uri.getSchemeSpecificPart(); - } else if (scheme.equals("http")) { - List segments = uri.getPathSegments(); - if (segments.size() > 1) - return segments.get(1); - } - return null; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - sRepoLoader.removeListener(this); - sModuleUtil.removeListener(this); - } - - public Module getModule() { - return mModule; - } - - public InstalledModule getInstalledModule() { - return mInstalledModule; - } - - public void gotoPage(int page) { - mPager.setCurrentItem(page); - } - - private void reload() { - runOnUiThread(new Runnable() { - @Override - public void run() { - recreate(); - } - }); - } - - @Override - public void onRepoReloaded(RepoLoader loader) { - reload(); - } - - @Override - public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { - reload(); - } - - @Override - public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { - if (packageName.equals(mPackageName)) - reload(); - } - - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_download_details, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_refresh: - RepoLoader.getInstance().triggerReload(true); - return true; - } - return super.onOptionsItemSelected(item); - } - - class SwipeFragmentPagerAdapter extends FragmentPagerAdapter { - final int PAGE_COUNT = 3; - private String tabTitles[] = new String[]{getString(R.string.download_details_page_description), - getString(R.string.download_details_page_versions), - getString(R.string.download_details_page_settings), - }; - - public SwipeFragmentPagerAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public int getCount() { - return PAGE_COUNT; - } - - @Override - public Fragment getItem(int position) { - switch (position) { - case DOWNLOAD_DESCRIPTION: - return new DownloadDetailsFragment(); - case DOWNLOAD_VERSIONS: - return new DownloadDetailsVersionsFragment(); - case DOWNLOAD_SETTINGS: - return new DownloadDetailsSettingsFragment(); - default: - return null; - } - } - - @Override - public CharSequence getPageTitle(int position) { - // Generate title based on item position - return tabTitles[position]; - } - } + public static final int DOWNLOAD_DESCRIPTION = 0; + public static final int DOWNLOAD_VERSIONS = 1; + public static final int DOWNLOAD_SETTINGS = 2; + private static RepoLoader sRepoLoader = RepoLoader.getInstance(); + private static ModuleUtil sModuleUtil = ModuleUtil.getInstance(); + private ViewPager mPager; + private String mPackageName; + private Module mModule; + private InstalledModule mInstalledModule; + private MenuItem mItemBookmark; + private boolean changeIcon = false; + + @Override + public void onCreate(Bundle savedInstanceState) { + ThemeUtil.setTheme(this); + + mPackageName = getModulePackageName(); + try { + mModule = sRepoLoader.getModule(mPackageName); + } catch (Exception e) { + Log.i(TAG, "DownloadDetailsActivity -> " + e.getMessage()); + + mModule = null; + } + + mInstalledModule = ModuleUtil.getInstance().getModule(mPackageName); + + super.onCreate(savedInstanceState); + sRepoLoader.addListener(this, false); + sModuleUtil.addListener(this); + + if (mModule != null) { + setContentView(R.layout.activity_download_details); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + + if (ab != null) { + ab.setTitle(R.string.nav_item_download); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + if (changeIcon) { + toolbar.setNavigationIcon(R.drawable.ic_close); + } + + setupTabs(); + + Boolean directDownload = getIntent().getBooleanExtra("direct_download", false); + // Updates available => start on the versions page + if (mInstalledModule != null && mInstalledModule.isUpdate(sRepoLoader.getLatestVersion(mModule)) || directDownload) + mPager.setCurrentItem(DOWNLOAD_VERSIONS); + + if (Build.VERSION.SDK_INT >= 21) + findViewById(R.id.fake_elevation).setVisibility(View.GONE); + + } else { + setContentView(R.layout.activity_download_details_not_found); + + TextView txtMessage = findViewById(android.R.id.message); + txtMessage.setText(getResources().getString(R.string.download_details_not_found, mPackageName)); + + findViewById(R.id.reload).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + v.setEnabled(false); + sRepoLoader.triggerReload(true); + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (Build.VERSION.SDK_INT >= 21) + getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(this), 0.85f)); + + } + + private void setupTabs() { + mPager = findViewById(R.id.download_pager); + mPager.setAdapter(new SwipeFragmentPagerAdapter(getSupportFragmentManager())); + TabLayout mTabLayout = findViewById(R.id.sliding_tabs); + mTabLayout.setupWithViewPager(mPager); + mTabLayout.setBackgroundColor(XposedApp.getColor(this)); + } + + private String getModulePackageName() { + Uri uri = getIntent().getData(); + if (uri == null) + return null; + + String scheme = uri.getScheme(); + if (TextUtils.isEmpty(scheme)) { + return null; + } else switch (scheme) { + case "xposed": + changeIcon = true; + case "package": + return uri.getSchemeSpecificPart().replace("//", ""); + case "http": + List segments = uri.getPathSegments(); + if (segments.size() > 1) + return segments.get(1); + break; + } + return null; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + sRepoLoader.removeListener(this); + sModuleUtil.removeListener(this); + } + + public Module getModule() { + return mModule; + } + + public InstalledModule getInstalledModule() { + return mInstalledModule; + } + + public void gotoPage(int page) { + mPager.setCurrentItem(page); + } + + private void reload() { + runOnUiThread(new Runnable() { + @Override + public void run() { + recreate(); + } + }); + } + + @Override + public void onRepoReloaded(RepoLoader loader) { + reload(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reload(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + if (packageName.equals(mPackageName)) + reload(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_download_details, menu); + + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + if (updateIgnorePreference) { + SharedPreferences prefs = getSharedPreferences("update_ignored", Context.MODE_PRIVATE); + + boolean ignored = prefs.getBoolean(mModule.packageName, false); + menu.findItem(R.id.ignoreUpdate).setChecked(ignored); + } else { + menu.removeItem(R.id.ignoreUpdate); + } + + mItemBookmark = menu.findItem(R.id.menu_bookmark); + setupBookmark(false); + return true; + } + + private void setupBookmark(boolean clicked) { + SharedPreferences myPref = getSharedPreferences("bookmarks", MODE_PRIVATE); + + boolean saved = myPref.getBoolean(mModule.packageName, false); + boolean newValue; + + if (clicked) { + newValue = !saved; + myPref.edit().putBoolean(mModule.packageName, newValue).apply(); + + int msg = newValue ? R.string.bookmark_added : R.string.bookmark_removed; + + Snackbar.make(findViewById(android.R.id.content), msg, Snackbar.LENGTH_SHORT).show(); + } + + saved = myPref.getBoolean(mModule.packageName, false); + + if (saved) { + mItemBookmark.setTitle(R.string.remove_bookmark); + mItemBookmark.setIcon(R.drawable.ic_bookmark); + } else { + mItemBookmark.setTitle(R.string.add_bookmark); + mItemBookmark.setIcon(R.drawable.ic_bookmark_outline); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_bookmark: + setupBookmark(true); + break; + case R.id.menu_refresh: + RepoLoader.getInstance().triggerReload(true); + return true; + case R.id.menu_share: + String text = mModule.name + " - "; + + if (isPackageInstalled(mPackageName, this)) { + String s = getPackageManager().getInstallerPackageName(mPackageName); + boolean playStore; + + try { + playStore = s.equals(ModulesFragment.PLAY_STORE_PACKAGE); + } catch (NullPointerException e) { + playStore = false; + } + + if (playStore) { + text += String.format(ModulesFragment.PLAY_STORE_LINK, mPackageName); + } else { + text += String.format(ModulesFragment.XPOSED_REPO_LINK, mPackageName); + } + } else { + text += String.format(ModulesFragment.XPOSED_REPO_LINK, + mPackageName); + } + + Intent sharingIntent = new Intent(Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(Intent.EXTRA_TEXT, text); + startActivity(Intent.createChooser(sharingIntent, getString(R.string.share))); + return true; + case R.id.ignoreUpdate: + SharedPreferences prefs = getSharedPreferences("update_ignored", Context.MODE_PRIVATE); + + boolean ignored = prefs.getBoolean(mModule.packageName, false); + prefs.edit().putBoolean(mModule.packageName, !ignored).apply(); + item.setChecked(!ignored); + break; + } + return super.onOptionsItemSelected(item); + } + + private boolean isPackageInstalled(String packagename, Context context) { + PackageManager pm = context.getPackageManager(); + try { + pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + class SwipeFragmentPagerAdapter extends FragmentPagerAdapter { + final int PAGE_COUNT = 3; + private String tabTitles[] = new String[]{getString(R.string.download_details_page_description), getString(R.string.download_details_page_versions), getString(R.string.download_details_page_settings),}; + + public SwipeFragmentPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public int getCount() { + return PAGE_COUNT; + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case DOWNLOAD_DESCRIPTION: + return new DownloadDetailsFragment(); + case DOWNLOAD_VERSIONS: + return new DownloadDetailsVersionsFragment(); + case DOWNLOAD_SETTINGS: + return new DownloadDetailsSettingsFragment(); + default: + return null; + } + } + + @Override + public CharSequence getPageTitle(int position) { + // Generate title based on item position + return tabTitles[position]; + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java index dfbeedf57..177c601ef 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsFragment.java @@ -1,80 +1,85 @@ package de.robv.android.xposed.installer; import android.app.Activity; -import android.support.v4.app.Fragment; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.text.method.LinkMovementMethod; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.repo.RepoParser; import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.chrome.LinkTransformationMethod; public class DownloadDetailsFragment extends Fragment { - private DownloadDetailsActivity mActivity; + private DownloadDetailsActivity mActivity; - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mActivity = (DownloadDetailsActivity) activity; - } + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final Module module = mActivity.getModule(); - if (module == null) - return null; + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Module module = mActivity.getModule(); + if (module == null) + return null; - final View view = inflater.inflate(R.layout.download_details, container, false); + final View view = inflater.inflate(R.layout.download_details, container, false); - TextView title = (TextView) view.findViewById(R.id.download_title); - title.setText(module.name); + TextView title = view.findViewById(R.id.download_title); + title.setText(module.name); + title.setTextIsSelectable(true); - TextView author = (TextView) view.findViewById(R.id.download_author); - if (module.author != null && !module.author.isEmpty()) - author.setText(getString(R.string.download_author, module.author)); - else - author.setText(R.string.download_unknown_author); + TextView author = view.findViewById(R.id.download_author); + if (module.author != null && !module.author.isEmpty()) + author.setText(getString(R.string.download_author, module.author)); + else + author.setText(R.string.download_unknown_author); - TextView description = (TextView) view.findViewById(R.id.download_description); - if (module.description != null) { - if (module.descriptionIsHtml) { - description.setText(RepoParser.parseSimpleHtml(module.description)); - description.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - description.setText(module.description); - } - } else { - description.setVisibility(View.GONE); - } + TextView description = view.findViewById(R.id.download_description); + if (module.description != null) { + if (module.descriptionIsHtml) { + description.setText(RepoParser.parseSimpleHtml(getActivity(), module.description, description)); + description.setTransformationMethod(new LinkTransformationMethod(getActivity())); + description.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + description.setText(module.description); + } + description.setTextIsSelectable(true); + } else { + description.setVisibility(View.GONE); + } - ViewGroup moreInfoContainer = (ViewGroup) view.findViewById(R.id.download_moreinfo_container); - for (Pair moreInfoEntry : module.moreInfo) { - View moreInfoView = inflater.inflate(R.layout.download_moreinfo, moreInfoContainer, false); - TextView txtTitle = (TextView) moreInfoView.findViewById(android.R.id.title); - TextView txtValue = (TextView) moreInfoView.findViewById(android.R.id.message); + ViewGroup moreInfoContainer = view.findViewById(R.id.download_moreinfo_container); + for (Pair moreInfoEntry : module.moreInfo) { + View moreInfoView = inflater.inflate(R.layout.download_moreinfo, moreInfoContainer, false); + TextView txtTitle = moreInfoView.findViewById(android.R.id.title); + TextView txtValue = moreInfoView.findViewById(android.R.id.message); - txtTitle.setText(moreInfoEntry.first + ":"); - txtValue.setText(moreInfoEntry.second); + txtTitle.setText(moreInfoEntry.first + ":"); + txtValue.setText(moreInfoEntry.second); - final Uri link = NavUtil.parseURL(moreInfoEntry.second); - if (link != null) { - txtValue.setTextColor(txtValue.getLinkTextColors()); - moreInfoView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - NavUtil.startURL(getActivity(), link); - } - }); - } + final Uri link = NavUtil.parseURL(moreInfoEntry.second); + if (link != null) { + txtValue.setTextColor(txtValue.getLinkTextColors()); + moreInfoView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), link); + } + }); + } - moreInfoContainer.addView(moreInfoView); - } + moreInfoContainer.addView(moreInfoView); + } - return view; - } -} + return view; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java index 28a05c21f..49b1d2527 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsSettingsFragment.java @@ -1,45 +1,66 @@ package de.robv.android.xposed.installer; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceManager; + import com.github.machinarius.preferencefragment.PreferenceFragment; + +import java.util.Map; + import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.util.PrefixedSharedPreferences; import de.robv.android.xposed.installer.util.RepoLoader; public class DownloadDetailsSettingsFragment extends PreferenceFragment { - private DownloadDetailsActivity mActivity; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mActivity = (DownloadDetailsActivity) activity; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Module module = mActivity.getModule(); - if (module == null) - return; - - final String packageName = module.packageName; - - PreferenceManager prefManager = getPreferenceManager(); - prefManager.setSharedPreferencesName("module_settings"); - PrefixedSharedPreferences.injectToPreferenceManager(prefManager, module.packageName); - addPreferencesFromResource(R.xml.module_prefs); - - findPreference("release_type").setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue); - return true; - } - }); - } -} + private DownloadDetailsActivity mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Module module = mActivity.getModule(); + if (module == null) + return; + + final String packageName = module.packageName; + + PreferenceManager prefManager = getPreferenceManager(); + prefManager.setSharedPreferencesName("module_settings"); + PrefixedSharedPreferences.injectToPreferenceManager(prefManager, module.packageName); + addPreferencesFromResource(R.xml.module_prefs); + + SharedPreferences prefs = getActivity().getSharedPreferences("module_settings", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + if (prefs.getBoolean("no_global", true)) { + for (Map.Entry k : prefs.getAll().entrySet()) { + if (prefs.getString(k.getKey(), "").equals("global")) { + editor.putString(k.getKey(), "").apply(); + } + } + + editor.putBoolean("no_global", false).apply(); + } + + findPreference("release_type").setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue); + return true; + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java index d0104dc1b..20ba8de72 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadDetailsVersionsFragment.java @@ -1,15 +1,9 @@ package de.robv.android.xposed.installer; -import java.io.File; -import java.text.DateFormat; -import java.util.Date; - import android.app.Activity; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.text.method.LinkMovementMethod; @@ -21,230 +15,248 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import android.widget.Toast; + +import java.io.File; +import java.text.DateFormat; +import java.util.Date; + import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.repo.ModuleVersion; import de.robv.android.xposed.installer.repo.ReleaseType; import de.robv.android.xposed.installer.repo.RepoParser; import de.robv.android.xposed.installer.util.DownloadsUtil; import de.robv.android.xposed.installer.util.HashUtil; +import de.robv.android.xposed.installer.util.InstallApkUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.ThemeUtil; +import de.robv.android.xposed.installer.util.chrome.LinkTransformationMethod; import de.robv.android.xposed.installer.widget.DownloadView; +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; + public class DownloadDetailsVersionsFragment extends ListFragment { - private DownloadDetailsActivity mActivity; - private static VersionsAdapter sAdapter; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mActivity = (DownloadDetailsActivity) activity; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final Module module = mActivity.getModule(); - if (module == null) - return; - - if (module.versions.isEmpty()) { - setEmptyText(getString(R.string.download_no_versions)); - setListShown(true); - } else { - RepoLoader repoLoader = RepoLoader.getInstance(); - if (!repoLoader.isVersionShown(module.versions.get(0))) { - TextView txtHeader = new TextView(getActivity()); - txtHeader.setText(R.string.download_test_version_not_shown); - txtHeader.setTextColor(getResources().getColor(R.color.warning)); - txtHeader.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mActivity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS); - } - }); - getListView().addHeaderView(txtHeader); - } - - sAdapter = new VersionsAdapter(mActivity, mActivity.getInstalledModule()); - for (ModuleVersion version : module.versions) { - if (repoLoader.isVersionShown(version)) - sAdapter.add(version); - } - setListAdapter(sAdapter); - } - - DisplayMetrics metrics = getResources().getDisplayMetrics(); - int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); - int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); - getListView().setDivider(null); - getListView().setDividerHeight(sixDp); - getListView().setPadding(eightDp, eightDp, eightDp, eightDp); - getListView().setClipToPadding(false); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - setListAdapter(null); - } - - static class ViewHolder { - TextView txtStatus; - TextView txtVersion; - TextView txtRelType; - TextView txtUploadDate; - DownloadView downloadView; - TextView txtChangesTitle; - TextView txtChanges; - } - - private class VersionsAdapter extends ArrayAdapter { - private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); - private final int mColorRelTypeStable; - private final int mColorRelTypeOthers; - private final int mColorInstalled; - private final int mColorUpdateAvailable; - private final String mTextInstalled; - private final String mTextUpdateAvailable; - private final int mInstalledVersionCode; - - public VersionsAdapter(Context context, InstalledModule installed) { - super(context, R.layout.list_item_version); - mColorRelTypeStable = ThemeUtil.getThemeColor(context, android.R.attr.textColorTertiary); - mColorRelTypeOthers = getResources().getColor(R.color.warning); - mColorInstalled = ThemeUtil.getThemeColor(context, R.attr.download_status_installed); - mColorUpdateAvailable = getResources().getColor(R.color.download_status_update_available); - mTextInstalled = getString(R.string.download_section_installed) + ":"; - mTextUpdateAvailable = getString(R.string.download_section_update_available) + ":"; - mInstalledVersionCode = (installed != null) ? installed.versionCode : -1; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(R.layout.list_item_version, null, true); - ViewHolder viewHolder = new ViewHolder(); - viewHolder.txtStatus = (TextView) view.findViewById(R.id.txtStatus); - viewHolder.txtVersion = (TextView) view.findViewById(R.id.txtVersion); - viewHolder.txtRelType = (TextView) view.findViewById(R.id.txtRelType); - viewHolder.txtUploadDate = (TextView) view.findViewById(R.id.txtUploadDate); - viewHolder.downloadView = (DownloadView) view.findViewById(R.id.downloadView); - viewHolder.txtChangesTitle = (TextView) view.findViewById(R.id.txtChangesTitle); - viewHolder.txtChanges = (TextView) view.findViewById(R.id.txtChanges); - view.setTag(viewHolder); - } - - ViewHolder holder = (ViewHolder) view.getTag(); - ModuleVersion item = (ModuleVersion) getItem(position); - - holder.txtVersion.setText(item.name); - holder.txtRelType.setText(item.relType.getTitleId()); - holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE ? mColorRelTypeStable : mColorRelTypeOthers); - - if (item.uploaded > 0) { - holder.txtUploadDate.setText(mDateFormatter.format(new Date(item.uploaded))); - holder.txtUploadDate.setVisibility(View.VISIBLE); - } else { - holder.txtUploadDate.setVisibility(View.GONE); - } - - if (item.code <= 0 || mInstalledVersionCode <= 0 || item.code < mInstalledVersionCode) { - holder.txtStatus.setVisibility(View.GONE); - } else if (item.code == mInstalledVersionCode) { - holder.txtStatus.setText(mTextInstalled); - holder.txtStatus.setTextColor(mColorInstalled); - holder.txtStatus.setVisibility(View.VISIBLE); - } else { // item.code > mInstalledVersionCode - holder.txtStatus.setText(mTextUpdateAvailable); - holder.txtStatus.setTextColor(mColorUpdateAvailable); - holder.txtStatus.setVisibility(View.VISIBLE); - } - - holder.downloadView.setUrl(item.downloadLink); - holder.downloadView.setTitle(mActivity.getModule().name); - holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item)); - - if (item.changelog != null && !item.changelog.isEmpty()) { - holder.txtChangesTitle.setVisibility(View.VISIBLE); - holder.txtChanges.setVisibility(View.VISIBLE); - - if (item.changelogIsHtml) { - holder.txtChanges.setText(RepoParser.parseSimpleHtml(item.changelog)); - holder.txtChanges.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - holder.txtChanges.setText(item.changelog); - holder.txtChanges.setMovementMethod(null); - } - - } else { - holder.txtChangesTitle.setVisibility(View.GONE); - holder.txtChanges.setVisibility(View.GONE); - } - - return view; - } - } - - private static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback { - private final ModuleVersion moduleVersion; - - public DownloadModuleCallback(ModuleVersion moduleVersion) { - this.moduleVersion = moduleVersion; - } - - @Override - public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { - File localFile = new File(info.localFilename); - if (!localFile.isFile()) - return; - - if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) { - try { - String actualMd5Sum = HashUtil.md5(localFile); - if (!moduleVersion.md5sum.equals(actualMd5Sum)) { - Toast.makeText(context, context.getString(R.string.download_md5sum_incorrect, - actualMd5Sum, moduleVersion.md5sum), Toast.LENGTH_LONG).show(); - DownloadsUtil.removeById(context, info.id); - return; - } - } catch (Exception e) { - Toast.makeText(context, context.getString(R.string.download_could_not_read_file, - e.getMessage()), Toast.LENGTH_LONG).show(); - DownloadsUtil.removeById(context, info.id); - return; - } - } - - PackageManager pm = context.getPackageManager(); - PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0); - - if (packageInfo == null) { - Toast.makeText(context, R.string.download_no_valid_apk, Toast.LENGTH_LONG).show(); - DownloadsUtil.removeById(context, info.id); - return; - } - - if (!packageInfo.packageName.equals(moduleVersion.module.packageName)) { - Toast.makeText(context, - context.getString(R.string.download_incorrect_package_name, - packageInfo.packageName, moduleVersion.module.packageName), - Toast.LENGTH_LONG).show(); - DownloadsUtil.removeById(context, info.id); - return; - } - - Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); - installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - installIntent.setDataAndType(Uri.fromFile(localFile), DownloadsUtil.MIME_TYPE_APK); - //installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); - //installIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); - installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName); - context.startActivity(installIntent); - } - } -} + private static VersionsAdapter sAdapter; + private DownloadDetailsActivity mActivity; + private Module module; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + module = mActivity.getModule(); + if (module == null) + return; + + if (module.versions.isEmpty()) { + setEmptyText(getString(R.string.download_no_versions)); + setListShown(true); + } else { + RepoLoader repoLoader = RepoLoader.getInstance(); + if (!repoLoader.isVersionShown(module.versions.get(0))) { + TextView txtHeader = new TextView(getActivity()); + txtHeader.setText(R.string.download_test_version_not_shown); + txtHeader.setTextColor(getResources().getColor(R.color.warning)); + txtHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mActivity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS); + } + }); + getListView().addHeaderView(txtHeader); + } + + sAdapter = new VersionsAdapter(mActivity, mActivity.getInstalledModule()); + for (ModuleVersion version : module.versions) { + if (repoLoader.isVersionShown(version)) + sAdapter.add(version); + } + setListAdapter(sAdapter); + } + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); + int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); + getListView().setDivider(null); + getListView().setDividerHeight(sixDp); + getListView().setPadding(eightDp, eightDp, eightDp, eightDp); + getListView().setClipToPadding(false); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + DownloadView.mClickedButton.performClick(); + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + static class ViewHolder { + TextView txtStatus; + TextView txtVersion; + TextView txtRelType; + TextView txtUploadDate; + DownloadView downloadView; + TextView txtChangesTitle; + TextView txtChanges; + } + + public static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback { + private final ModuleVersion moduleVersion; + + public DownloadModuleCallback(ModuleVersion moduleVersion) { + this.moduleVersion = moduleVersion; + } + + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + File localFile = new File(info.localFilename); + + if (!localFile.isFile()) + return; + + if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) { + try { + String actualMd5Sum = HashUtil.md5(localFile); + if (!moduleVersion.md5sum.equals(actualMd5Sum)) { + Toast.makeText(context, context.getString(R.string.download_md5sum_incorrect, actualMd5Sum, moduleVersion.md5sum), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } catch (Exception e) { + Toast.makeText(context, context.getString(R.string.download_could_not_read_file, e.getMessage()), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } + + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0); + + if (packageInfo == null) { + Toast.makeText(context, R.string.download_no_valid_apk, Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + if (!packageInfo.packageName.equals(moduleVersion.module.packageName)) { + Toast.makeText(context, context.getString(R.string.download_incorrect_package_name, packageInfo.packageName, moduleVersion.module.packageName), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + new InstallApkUtil(context, info).execute(); + } + } + + private class VersionsAdapter extends ArrayAdapter { + private final DateFormat mDateFormatter = DateFormat + .getDateInstance(DateFormat.SHORT); + private final int mColorRelTypeStable; + private final int mColorRelTypeOthers; + private final int mColorInstalled; + private final int mColorUpdateAvailable; + private final String mTextInstalled; + private final String mTextUpdateAvailable; + private final int mInstalledVersionCode; + + public VersionsAdapter(Context context, InstalledModule installed) { + super(context, R.layout.list_item_version); + mColorRelTypeStable = ThemeUtil.getThemeColor(context, android.R.attr.textColorTertiary); + mColorRelTypeOthers = getResources().getColor(R.color.warning); + mColorInstalled = ThemeUtil.getThemeColor(context, R.attr.download_status_installed); + mColorUpdateAvailable = getResources().getColor(R.color.download_status_update_available); + mTextInstalled = getString(R.string.download_section_installed) + ":"; + mTextUpdateAvailable = getString(R.string.download_section_update_available) + ":"; + mInstalledVersionCode = (installed != null) ? installed.versionCode : -1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.list_item_version, null, true); + ViewHolder viewHolder = new ViewHolder(); + viewHolder.txtStatus = view.findViewById(R.id.txtStatus); + viewHolder.txtVersion = view.findViewById(R.id.txtVersion); + viewHolder.txtRelType = view.findViewById(R.id.txtRelType); + viewHolder.txtUploadDate = view.findViewById(R.id.txtUploadDate); + viewHolder.downloadView = view.findViewById(R.id.downloadView); + viewHolder.txtChangesTitle = view.findViewById(R.id.txtChangesTitle); + viewHolder.txtChanges = view.findViewById(R.id.txtChanges); + viewHolder.downloadView.fragment = DownloadDetailsVersionsFragment.this; + view.setTag(viewHolder); + } + + ViewHolder holder = (ViewHolder) view.getTag(); + ModuleVersion item = getItem(position); + + holder.txtVersion.setText(item.name); + holder.txtRelType.setText(item.relType.getTitleId()); + holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE + ? mColorRelTypeStable : mColorRelTypeOthers); + + if (item.uploaded > 0) { + holder.txtUploadDate.setText( + mDateFormatter.format(new Date(item.uploaded))); + holder.txtUploadDate.setVisibility(View.VISIBLE); + } else { + holder.txtUploadDate.setVisibility(View.GONE); + } + + if (item.code <= 0 || mInstalledVersionCode <= 0 + || item.code < mInstalledVersionCode) { + holder.txtStatus.setVisibility(View.GONE); + } else if (item.code == mInstalledVersionCode) { + holder.txtStatus.setText(mTextInstalled); + holder.txtStatus.setTextColor(mColorInstalled); + holder.txtStatus.setVisibility(View.VISIBLE); + } else { // item.code > mInstalledVersionCode + holder.txtStatus.setText(mTextUpdateAvailable); + holder.txtStatus.setTextColor(mColorUpdateAvailable); + holder.txtStatus.setVisibility(View.VISIBLE); + } + + holder.downloadView.setUrl(item.downloadLink); + holder.downloadView.setTitle(mActivity.getModule().name); + holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item)); + + if (item.changelog != null && !item.changelog.isEmpty()) { + holder.txtChangesTitle.setVisibility(View.VISIBLE); + holder.txtChanges.setVisibility(View.VISIBLE); + + if (item.changelogIsHtml) { + holder.txtChanges.setText(RepoParser.parseSimpleHtml(getActivity(), item.changelog, holder.txtChanges)); + holder.txtChanges.setTransformationMethod(new LinkTransformationMethod(getActivity())); + holder.txtChanges.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + holder.txtChanges.setText(item.changelog); + holder.txtChanges.setMovementMethod(null); + } + + } else { + holder.txtChangesTitle.setVisibility(View.GONE); + holder.txtChanges.setVisibility(View.GONE); + } + + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java index e9da1d7a1..04aaa4501 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/DownloadFragment.java @@ -1,20 +1,21 @@ package de.robv.android.xposed.installer; -import java.text.DateFormat; -import java.util.Date; - -import android.app.AlertDialog; -import android.support.v4.app.Fragment; +import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.SearchView; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -23,306 +24,412 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CursorAdapter; import android.widget.FilterQueryProvider; import android.widget.TextView; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.text.DateFormat; +import java.util.Date; import de.robv.android.xposed.installer.repo.RepoDb; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumnsIndexes; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; -import de.robv.android.xposed.installer.util.NavUtil; import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.RepoLoader.RepoListener; import de.robv.android.xposed.installer.util.ThemeUtil; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; -public class DownloadFragment extends Fragment implements RepoListener, ModuleListener { - private SharedPreferences mPref; - private DownloadsAdapter mAdapter; - private String mFilterText; - private RepoLoader mRepoLoader; - private ModuleUtil mModuleUtil; - private int mSortingOrder; - private SearchView mSearchView; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mPref = XposedApp.getPreferences(); - mRepoLoader = RepoLoader.getInstance(); - mModuleUtil = ModuleUtil.getInstance(); - mAdapter = new DownloadsAdapter(getActivity()); - mAdapter.setFilterQueryProvider(new FilterQueryProvider() { - @Override - public Cursor runQuery(CharSequence constraint) { - // TODO Instead of this workaround, show a "downloads disabled" message - if (XposedApp.getInstance().areDownloadsEnabled()) - return RepoDb.queryModuleOverview(mSortingOrder, constraint); - else - return null; - } - }); - mSortingOrder = mPref.getInt("download_sorting_order", RepoDb.SORT_STATUS); - setHasOptionsMenu(true); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.tab_downloader, container, false); - StickyListHeadersListView lv = (StickyListHeadersListView) v.findViewById(R.id.listModules); - - mRepoLoader.addListener(this, true); - mModuleUtil.addListener(this); - lv.setAdapter(mAdapter); - - lv.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Cursor cursor = (Cursor) mAdapter.getItem(position); - String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME); - - Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); - detailsIntent.setData(Uri.fromParts("package", packageName, null)); - detailsIntent.putExtra(NavUtil.FINISH_ON_UP_NAVIGATION, true); - startActivity(detailsIntent); - NavUtil.setTransitionSlideEnter(getActivity()); - } - }); - lv.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - // Expand the search view when the SEARCH key is triggered - if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP - && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { - if (mSearchView != null) - mSearchView.setIconified(false); - return true; - } - return false; - } - }); - - setHasOptionsMenu(true); - - return v; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mRepoLoader.removeListener(this); - mModuleUtil.removeListener(this); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_download, menu); - - // Setup search button - final MenuItem searchItem = menu.findItem(R.id.menu_search); - mSearchView = (SearchView) searchItem.getActionView(); - mSearchView.setIconifiedByDefault(true); - mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - setFilter(query); - mSearchView.clearFocus(); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - setFilter(newText); - return true; - } - }); - MenuItemCompat.setOnActionExpandListener(searchItem, - new MenuItemCompat.OnActionExpandListener() { - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - setFilter(null); - return true; // Return true to collapse action view - } - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - return true; // Return true to expand action view - } - }); - } - - private void setFilter(String filterText) { - mFilterText = filterText; - reloadItems(); - } - - private void reloadItems() { - mAdapter.getFilter().filter(mFilterText); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_refresh: - mRepoLoader.triggerReload(true); - return true; - case R.id.menu_sort: - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.download_sorting_title); - builder.setSingleChoiceItems(R.array.download_sort_order, mSortingOrder, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mSortingOrder = which; - mPref.edit().putInt("download_sorting_order", mSortingOrder).commit(); - reloadItems(); - dialog.dismiss(); - } - }); - builder.show(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onRepoReloaded(final RepoLoader loader) { - reloadItems(); - } - - @Override - public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { - reloadItems(); - } - - @Override - public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { - reloadItems(); - } - - - private class DownloadsAdapter extends CursorAdapter implements StickyListHeadersAdapter { - private final Context mContext; - private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); - private final LayoutInflater mInflater; - private String[] sectionHeadersStatus; - private String[] sectionHeadersDate; - - public DownloadsAdapter(Context context) { - super(context, null, 0); - mContext = context; - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - Resources res = context.getResources(); - sectionHeadersStatus = new String[] { - res.getString(R.string.download_section_framework), - res.getString(R.string.download_section_update_available), - res.getString(R.string.download_section_installed), - res.getString(R.string.download_section_not_installed), - }; - sectionHeadersDate = new String[] { - res.getString(R.string.download_section_24h), - res.getString(R.string.download_section_7d), - res.getString(R.string.download_section_30d), - res.getString(R.string.download_section_older) - }; - } - - @Override - public View getHeaderView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = mInflater.inflate(R.layout.list_sticky_header_download, parent, false); - } - - long section = getHeaderId(position); - - TextView tv = (TextView) convertView.findViewById(android.R.id.title); - tv.setText(mSortingOrder == RepoDb.SORT_STATUS ? sectionHeadersStatus[(int)section] : sectionHeadersDate[(int) section]); - return convertView; - } - - @Override - public long getHeaderId(int position) { - Cursor cursor = (Cursor) getItem(position); - long created = cursor.getLong(OverviewColumnsIndexes.CREATED); - long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); - boolean isFramework = cursor.getInt(OverviewColumnsIndexes.IS_FRAMEWORK) > 0; - boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; - boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; - - if (mSortingOrder != RepoDb.SORT_STATUS) { - long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created; - long age = System.currentTimeMillis() - timestamp; - final long mSecsPerDay = 24 * 60 * 60 * 1000L; - if (age < mSecsPerDay) - return 0; - if (age < 7 * mSecsPerDay) - return 1; - if (age < 30 * mSecsPerDay) - return 2; - return 3; - } else { - if (isFramework) - return 0; - - if (hasUpdate) - return 1; - else if (isInstalled) - return 2; - else - return 3; - } - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.list_item_download, parent, false); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - String title = cursor.getString(OverviewColumnsIndexes.TITLE); - String summary = cursor.getString(OverviewColumnsIndexes.SUMMARY); - String installedVersion = cursor.getString(OverviewColumnsIndexes.INSTALLED_VERSION); - String latestVersion = cursor.getString(OverviewColumnsIndexes.LATEST_VERSION); - long created = cursor.getLong(OverviewColumnsIndexes.CREATED); - long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); - boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; - boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; - - TextView txtTitle = (TextView) view.findViewById(android.R.id.text1); - txtTitle.setText(title); - - TextView txtSummary = (TextView) view.findViewById(android.R.id.text2); - txtSummary.setText(summary); - - TextView txtStatus = (TextView) view.findViewById(R.id.downloadStatus); - if (hasUpdate) { - txtStatus.setText(mContext.getString(R.string.download_status_update_available, installedVersion, latestVersion)); - txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available)); - txtStatus.setVisibility(View.VISIBLE); - } else if (isInstalled) { - txtStatus.setText(mContext.getString(R.string.download_status_installed, installedVersion)); - txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed)); - txtStatus.setVisibility(View.VISIBLE); - } else { - txtStatus.setVisibility(View.GONE); - } - - String creationDate = mDateFormatter.format(new Date(created)); - String updateDate = mDateFormatter.format(new Date(updated)); - ((TextView) view.findViewById(R.id.timestamps)).setText( - getString(R.string.download_timestamps, creationDate, updateDate)); - } - } -} +import static android.content.Context.MODE_PRIVATE; + +public class DownloadFragment extends Fragment implements RepoListener, ModuleListener, SharedPreferences.OnSharedPreferenceChangeListener { + public static Activity sActivity; + private SharedPreferences mPref; + private DownloadsAdapter mAdapter; + private String mFilterText; + private RepoLoader mRepoLoader; + private ModuleUtil mModuleUtil; + private int mSortingOrder; + private SearchView mSearchView; + private StickyListHeadersListView mListView; + private SharedPreferences mIgnoredUpdatesPref; + private boolean changed = false; + private View backgroundList; + private BroadcastReceiver connectionListener = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + + if (backgroundList != null && mRepoLoader != null) { + if (networkInfo == null) { + ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.no_connection_available); + backgroundList.findViewById(R.id.progress).setVisibility(View.GONE); + } else { + ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.update_download_list); + backgroundList.findViewById(R.id.progress).setVisibility(View.VISIBLE); + } + + mRepoLoader.triggerReload(true); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mPref = XposedApp.getPreferences(); + mRepoLoader = RepoLoader.getInstance(); + mModuleUtil = ModuleUtil.getInstance(); + mAdapter = new DownloadsAdapter(getActivity()); + mAdapter.setFilterQueryProvider(new FilterQueryProvider() { + @Override + public Cursor runQuery(CharSequence constraint) { + return RepoDb.queryModuleOverview(mSortingOrder, constraint); + } + }); + mSortingOrder = mPref.getInt("download_sorting_order", + RepoDb.SORT_STATUS); + + mIgnoredUpdatesPref = getContext() + .getSharedPreferences("update_ignored", MODE_PRIVATE); + setHasOptionsMenu(true); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mAdapter != null && mListView != null) { + mListView.setAdapter(mAdapter); + } + + sActivity = getActivity(); + } + + @Override + public void onResume() { + super.onResume(); + + mIgnoredUpdatesPref.registerOnSharedPreferenceChangeListener(this); + if (changed) { + reloadItems(); + changed = !changed; + } + + getActivity().registerReceiver(connectionListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + @Override + public void onPause() { + super.onPause(); + + getActivity().unregisterReceiver(connectionListener); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mIgnoredUpdatesPref.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_downloader, container, false); + backgroundList = v.findViewById(R.id.background_list); + + mListView = v.findViewById(R.id.listModules); + if (Build.VERSION.SDK_INT >= 26) { + mListView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); + } + final SwipeRefreshLayout refreshLayout = v.findViewById(R.id.swiperefreshlayout); + refreshLayout.setColorSchemeColors(XposedApp.getColor(getContext())); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + mRepoLoader.setSwipeRefreshLayout(refreshLayout); + mRepoLoader.triggerReload(true); + } + }); + mRepoLoader.addListener(this, true); + mModuleUtil.addListener(this); + mListView.setAdapter(mAdapter); + mListView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mListView.getChildAt(0) != null) { + refreshLayout.setEnabled(mListView.getFirstVisiblePosition() == 0 && mListView.getChildAt(0).getTop() == 0); + } + } + }); + + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME); + + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", packageName, null)); + startActivity(detailsIntent); + } + }); + mListView.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // Expand the search view when the SEARCH key is triggered + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { + if (mSearchView != null) + mSearchView.setIconified(false); + return true; + } + return false; + } + }); + + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mRepoLoader.removeListener(this); + mModuleUtil.removeListener(this); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_download, menu); + + // Setup search button + final MenuItem searchItem = menu.findItem(R.id.menu_search); + mSearchView = (SearchView) searchItem.getActionView(); + mSearchView.setIconifiedByDefault(true); + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + setFilter(query); + mSearchView.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + setFilter(newText); + return true; + } + }); + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + setFilter(null); + return true; // Return true to collapse action view + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; // Return true to expand action view + } + }); + } + + private void setFilter(String filterText) { + mFilterText = filterText; + reloadItems(); + backgroundList.setVisibility(View.GONE); + } + + private void reloadItems() { + mAdapter.getFilter().filter(mFilterText); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_sort: + new MaterialDialog.Builder(getActivity()) + .title(R.string.download_sorting_title) + .items(R.array.download_sort_order) + .itemsCallbackSingleChoice(mSortingOrder, + new MaterialDialog.ListCallbackSingleChoice() { + @Override + public boolean onSelection(MaterialDialog materialDialog, View view, int i, CharSequence charSequence) { + mSortingOrder = i; + mPref.edit().putInt("download_sorting_order", mSortingOrder).apply(); + reloadItems(); + materialDialog.dismiss(); + return true; + } + }) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onRepoReloaded(final RepoLoader loader) { + reloadItems(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + reloadItems(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reloadItems(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + changed = true; + } + + private class DownloadsAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private final Context mContext; + private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); + private final LayoutInflater mInflater; + private final SharedPreferences mPrefs; + private String[] sectionHeadersStatus; + private String[] sectionHeadersDate; + + public DownloadsAdapter(Context context) { + super(context, null, 0); + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPrefs = context.getSharedPreferences("update_ignored", MODE_PRIVATE); + + Resources res = context.getResources(); + sectionHeadersStatus = new String[]{ + res.getString(R.string.download_section_framework), + res.getString(R.string.download_section_update_available), + res.getString(R.string.download_section_installed), + res.getString(R.string.download_section_not_installed),}; + sectionHeadersDate = new String[]{ + res.getString(R.string.download_section_24h), + res.getString(R.string.download_section_7d), + res.getString(R.string.download_section_30d), + res.getString(R.string.download_section_older)}; + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_sticky_header_download, parent, false); + } + + long section = getHeaderId(position); + + TextView tv = convertView.findViewById(android.R.id.title); + tv.setText(mSortingOrder == RepoDb.SORT_STATUS + ? sectionHeadersStatus[(int) section] + : sectionHeadersDate[(int) section]); + return convertView; + } + + @Override + public long getHeaderId(int position) { + Cursor cursor = (Cursor) getItem(position); + long created = cursor.getLong(OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); + boolean isFramework = cursor.getInt(OverviewColumnsIndexes.IS_FRAMEWORK) > 0; + boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean updateIgnored = mPrefs.getBoolean(cursor.getString(OverviewColumnsIndexes.PKGNAME), false); + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; + + if (hasUpdate && updateIgnored && updateIgnorePreference) { + hasUpdate = false; + } + + if (mSortingOrder != RepoDb.SORT_STATUS) { + long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created; + long age = System.currentTimeMillis() - timestamp; + final long mSecsPerDay = 24 * 60 * 60 * 1000L; + if (age < mSecsPerDay) + return 0; + if (age < 7 * mSecsPerDay) + return 1; + if (age < 30 * mSecsPerDay) + return 2; + return 3; + } else { + if (isFramework) + return 0; + + if (hasUpdate) + return 1; + else if (isInstalled) + return 2; + else + return 3; + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.list_item_download, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + String title = cursor.getString(OverviewColumnsIndexes.TITLE); + String summary = cursor.getString(OverviewColumnsIndexes.SUMMARY); + String installedVersion = cursor.getString(OverviewColumnsIndexes.INSTALLED_VERSION); + String latestVersion = cursor.getString(OverviewColumnsIndexes.LATEST_VERSION); + long created = cursor.getLong(OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); + boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean updateIgnored = mPrefs.getBoolean(cursor.getString(OverviewColumnsIndexes.PKGNAME), false); + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; + + if (hasUpdate && updateIgnored && updateIgnorePreference) { + hasUpdate = false; + } + + TextView txtTitle = view.findViewById(android.R.id.text1); + txtTitle.setText(title); + + TextView txtSummary = view.findViewById(android.R.id.text2); + txtSummary.setText(summary); + + TextView txtStatus = view.findViewById(R.id.downloadStatus); + if (hasUpdate) { + txtStatus.setText(mContext.getString( + R.string.download_status_update_available, + installedVersion, latestVersion)); + txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available)); + txtStatus.setVisibility(View.VISIBLE); + } else if (isInstalled) { + txtStatus.setText(mContext.getString( + R.string.download_status_installed, installedVersion)); + txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed)); + txtStatus.setVisibility(View.VISIBLE); + } else { + txtStatus.setVisibility(View.GONE); + } + + String creationDate = mDateFormatter.format(new Date(created)); + String updateDate = mDateFormatter.format(new Date(updated)); + ((TextView) view.findViewById(R.id.timestamps)).setText(getString(R.string.download_timestamps, creationDate, updateDate)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/DownloadReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/DownloadReceiver.java deleted file mode 100644 index 12e51e2d6..000000000 --- a/app/src/main/java/de/robv/android/xposed/installer/DownloadReceiver.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.robv.android.xposed.installer; - -import android.app.DownloadManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import de.robv.android.xposed.installer.util.DownloadsUtil; - -public class DownloadReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - String action = intent.getAction(); - if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { - long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId); - } - } -} diff --git a/app/src/main/java/de/robv/android/xposed/installer/InstallerFragment.java b/app/src/main/java/de/robv/android/xposed/installer/InstallerFragment.java deleted file mode 100644 index 54847fd85..000000000 --- a/app/src/main/java/de/robv/android/xposed/installer/InstallerFragment.java +++ /dev/null @@ -1,832 +0,0 @@ -package de.robv.android.xposed.installer; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -import android.app.Activity; -import android.app.AlertDialog; -import android.support.v4.app.Fragment; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.os.Looper; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.TextView; -import de.robv.android.xposed.installer.util.AssetUtil; -import de.robv.android.xposed.installer.util.NavUtil; -import de.robv.android.xposed.installer.util.NotificationUtil; -import de.robv.android.xposed.installer.util.RootUtil; -import de.robv.android.xposed.installer.util.ThemeUtil; - -public class InstallerFragment extends Fragment { - private String APP_PROCESS_NAME = null; - private final String BINARIES_FOLDER = AssetUtil.getBinariesFolder(); - private static final String JAR_PATH = "/system/framework/XposedBridge.jar"; - private static final String JAR_PATH_NEWVERSION = JAR_PATH + ".newversion"; - private final LinkedList mCompatibilityErrors = new LinkedList(); - private RootUtil mRootUtil = new RootUtil(); - private boolean mHadSegmentationFault = false; - - private static final String PREF_LAST_SEEN_BINARY = "last_seen_binary"; - - private ProgressDialog dlgProgress; - private TextView txtInstallError, txtKnownIssue; - private Button btnInstallMode, btnInstall, btnUninstall, btnSoftReboot, btnReboot; - - private static final int INSTALL_MODE_NORMAL = 0; - private static final int INSTALL_MODE_RECOVERY_AUTO = 1; - private static final int INSTALL_MODE_RECOVERY_MANUAL = 2; - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Activity activity = getActivity(); - - dlgProgress = new ProgressDialog(activity); - dlgProgress.setIndeterminate(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.tab_installer, container, false); - - btnInstallMode = (Button) v.findViewById(R.id.framework_install_mode); - txtInstallError = (TextView) v.findViewById(R.id.framework_install_errors); - txtKnownIssue = (TextView) v.findViewById(R.id.framework_known_issue); - - btnInstall = (Button) v.findViewById(R.id.btnInstall); - btnUninstall = (Button) v.findViewById(R.id.btnUninstall); - btnSoftReboot = (Button) v.findViewById(R.id.btnSoftReboot); - btnReboot = (Button) v.findViewById(R.id.btnReboot); - - btnInstallMode.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(getActivity(), XposedBaseActivity.class); - startActivity(intent); - } - }); - - // FIXME - /* - boolean isCompatible = false; - if (BINARIES_FOLDER == null) { - // incompatible processor architecture - } else if (Build.VERSION.SDK_INT == 15) { - APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk15"; - isCompatible = checkCompatibility(); - - } else if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT <= 19) { - APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk16"; - isCompatible = checkCompatibility(); - - } else if (Build.VERSION.SDK_INT > 19) { - APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk16"; - isCompatible = checkCompatibility(); - if (isCompatible) { - txtInstallError.setText(String.format(getString(R.string.not_tested_but_compatible), Build.VERSION.SDK_INT)); - txtInstallError.setVisibility(View.VISIBLE); - } - } - */ - - // FIXME - /* - if (isCompatible) { - btnInstall.setOnClickListener(new AsyncClickListener(btnInstall.getText()) { - @Override - public void onAsyncClick(View v) { - final boolean success = install(); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - refreshVersions(); - if (success) - ModuleUtil.getInstance().updateModulesList(false); - - // Start tracking the last seen version, irrespective of the installation method and the outcome. - // 0 or a stale version might be registered, if a recovery installation was requested - // It will get up to date when the last seen version is updated on a later panel startup - XposedApp.getPreferences().edit().putInt(PREF_LAST_SEEN_BINARY, appProcessInstalledVersion).commit(); - // Dismiss any warning already being displayed - getView().findViewById(R.id.install_reverted_warning).setVisibility(View.GONE); - } - }); - } - }); - } else { - String errorText = String.format(getString(R.string.phone_not_compatible), Build.VERSION.SDK_INT, Build.CPU_ABI); - if (!mCompatibilityErrors.isEmpty()) - errorText += "\n\n" + TextUtils.join("\n", mCompatibilityErrors); - txtInstallError.setText(errorText); - txtInstallError.setVisibility(View.VISIBLE); - btnInstall.setEnabled(false); - } - - btnUninstall.setOnClickListener(new AsyncClickListener(btnUninstall.getText()) { - @Override - public void onAsyncClick(View v) { - uninstall(); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - refreshVersions(); - - // Update tracking of the last seen version - if (appProcessInstalledVersion == 0) { - // Uninstall completed, check if an Xposed binary doesn't reappear - XposedApp.getPreferences().edit().putInt(PREF_LAST_SEEN_BINARY, -1).commit(); - } else { - // Xposed binary still in place. - // Stop tracking last seen version, as uninstall might complete later or not - XposedApp.getPreferences().edit().remove(PREF_LAST_SEEN_BINARY).commit(); - } - // Dismiss any warning already being displayed - getView().findViewById(R.id.install_reverted_warning).setVisibility(View.GONE); - } - }); - } - }); - */ - - String installedXposedVersion = XposedApp.getXposedProp().get("version"); - if (installedXposedVersion == null) { - txtInstallError.setText(R.string.installation_lollipop); - txtInstallError.setTextColor(getResources().getColor(R.color.warning)); - } else { - int installedXposedVersionInt = extractIntPart(installedXposedVersion); - if (installedXposedVersionInt == XposedApp.getActiveXposedVersion()) { - txtInstallError.setText(getString(R.string.installed_lollipop, installedXposedVersion)); - txtInstallError.setTextColor(getResources().getColor(R.color.darker_green)); - } else { - txtInstallError.setText(getString(R.string.installed_lollipop_inactive, installedXposedVersion)); - txtInstallError.setTextColor(getResources().getColor(R.color.warning)); - } - } - txtInstallError.setVisibility(View.VISIBLE); - - btnInstall.setEnabled(false); - btnInstallMode.setEnabled(false); - btnUninstall.setEnabled(false); - - btnReboot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - areYouSure(R.string.reboot, new AsyncDialogClickListener(btnReboot.getText()) { - @Override - public void onAsyncClick(DialogInterface dialog, int which) { - reboot(null); - } - }); - } - }); - - btnSoftReboot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - areYouSure(R.string.soft_reboot, new AsyncDialogClickListener(btnSoftReboot.getText()) { - @Override - public void onAsyncClick(DialogInterface dialog, int which) { - softReboot(); - } - }); - } - }); - - if (!XposedApp.getPreferences().getBoolean("hide_install_warning", false)) { - final View dontShowAgainView = inflater.inflate(R.layout.dialog_install_warning, null); - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.install_warning_title) - .setView(dontShowAgainView) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - CheckBox checkBox = (CheckBox) dontShowAgainView.findViewById(android.R.id.checkbox); - if (checkBox.isChecked()) - XposedApp.getPreferences().edit().putBoolean("hide_install_warning", true).commit(); - } - }) - .setCancelable(false) - .show(); - } - - /* Detection of reverts to /system/bin/app_process. - * LastSeenBinary can be: - * missing - do nothing - * -1 - Uninstall was performed, check if an Xposed binary didn't reappear - * >= 0 - Make sure a downgrade or non-xposed binary doesn't occur - * Also auto-update the value to the latest version found - */ - /* - int lastSeenBinary = XposedApp.getPreferences().getInt(PREF_LAST_SEEN_BINARY, Integer.MIN_VALUE); - if (lastSeenBinary != Integer.MIN_VALUE) { - final View vInstallRevertedWarning = v.findViewById(R.id.install_reverted_warning); - final TextView txtInstallRevertedWarning = (TextView) v.findViewById(R.id.install_reverted_warning_text); - vInstallRevertedWarning.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Stop tracking and dismiss the info panel - XposedApp.getPreferences().edit().remove(PREF_LAST_SEEN_BINARY).commit(); - vInstallRevertedWarning.setVisibility(View.GONE); - } - }); - - if (lastSeenBinary < 0 && appProcessInstalledVersion > 0) { - // Uninstall was previously completed but an Xposed binary has reappeared - txtInstallRevertedWarning.setText(getString(R.string.uninstall_reverted, - versionToText(appProcessInstalledVersion))); - vInstallRevertedWarning.setVisibility(View.VISIBLE); - } else if (appProcessInstalledVersion < lastSeenBinary) { - // Previously installed binary was either restored to stock or downgraded, probably - // following a reboot on a locked system - txtInstallRevertedWarning.setText(getString(R.string.install_reverted, - versionToText(lastSeenBinary), versionToText(appProcessInstalledVersion))); - vInstallRevertedWarning.setVisibility(View.VISIBLE); - } else if (appProcessInstalledVersion > lastSeenBinary) { - // Current binary is newer, register it and keep monitoring for future downgrades - XposedApp.getPreferences().edit().putInt(PREF_LAST_SEEN_BINARY, appProcessInstalledVersion).commit(); - } else { - // All is ok - } - } - */ - - return v; - } - - private static int extractIntPart(String str) { - int result = 0, length = str.length(); - for (int offset = 0; offset < length; offset++) { - char c = str.charAt(offset); - if ('0' <= c && c <= '9') - result = result * 10 + (c - '0'); - else - break; - } - return result; - } - - @Override - public void onResume() { - super.onResume(); - btnInstallMode.setText(getInstallModeText()); - NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULES_UPDATED); - mHadSegmentationFault = false; - refreshKnownIssue(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - mRootUtil.dispose(); - } - - private abstract class AsyncClickListener implements View.OnClickListener { - private final CharSequence mProgressDlgText; - - public AsyncClickListener(CharSequence progressDlgText) { - mProgressDlgText = progressDlgText; - } - - @Override - public final void onClick(final View v) { - if (mProgressDlgText != null) { - dlgProgress.setMessage(mProgressDlgText); - dlgProgress.show(); - } - new Thread() { - public void run() { - onAsyncClick(v); - dlgProgress.dismiss(); - } - }.start(); - } - - protected abstract void onAsyncClick(View v); - } - - private abstract class AsyncDialogClickListener implements DialogInterface.OnClickListener { - private final CharSequence mProgressDlgText; - - public AsyncDialogClickListener(CharSequence progressDlgText) { - mProgressDlgText = progressDlgText; - } - - @Override - public void onClick(final DialogInterface dialog, final int which) { - if (mProgressDlgText != null) { - dlgProgress.setMessage(mProgressDlgText); - dlgProgress.show(); - } - new Thread() { - public void run() { - onAsyncClick(dialog, which); - dlgProgress.dismiss(); - } - }.start(); - } - - protected abstract void onAsyncClick(DialogInterface dialog, int which); - } - - private String versionToText(int version) { - return (version == 0) ? getString(R.string.none) : Integer.toString(version); - } - - private void refreshKnownIssue() { - String issueName = null; - String issueLink = null; - - if (new File("/system/framework/core.jar.jex").exists()) { - issueName = "Aliyun OS"; - issueLink = "http://forum.xda-developers.com/showpost.php?p=52289793&postcount=5"; - - } else if (new File("/data/miui/DexspyInstaller.jar").exists() || checkClassExists("miui.dexspy.DexspyInstaller")) { - issueName = "MIUI/Dexspy"; - issueLink = "http://forum.xda-developers.com/showpost.php?p=52291098&postcount=6"; - - } else if (mHadSegmentationFault) { - issueName = "Segmentation fault"; - issueLink = "http://forum.xda-developers.com/showpost.php?p=52292102&postcount=7"; - - } else if (checkClassExists("com.huawei.android.content.res.ResourcesEx") - || checkClassExists("android.content.res.NubiaResources")) { - issueName = "Resources subclass"; - issueLink = "http://forum.xda-developers.com/showpost.php?p=52801382&postcount=8"; - } - - if (issueName != null) { - final String issueLinkFinal = issueLink; - txtKnownIssue.setText(getString(R.string.install_known_issue, issueName)); - txtKnownIssue.setVisibility(View.VISIBLE); - txtKnownIssue.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - NavUtil.startURL(getActivity(), issueLinkFinal); - } - }); - if (btnInstall.isEnabled()) - btnInstall.setTextColor(getResources().getColor(R.color.warning)); - txtInstallError.setTextColor(ThemeUtil.getThemeColor(getActivity(), android.R.attr.textColorTertiary)); - } else { - txtKnownIssue.setVisibility(View.GONE); - // FIXME - //btnInstall.setTextColor(ThemeUtil.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); - //txtInstallError.setTextColor(getResources().getColor(R.color.warning)); - } - } - - private static boolean checkClassExists(String className) { - try { - Class.forName(className); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - private void showAlert(final String result) { - if (Looper.myLooper() != Looper.getMainLooper()) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - showAlert(result); - } - }); - return; - } - - AlertDialog dialog = new AlertDialog.Builder(getActivity()) - .setMessage(result) - .setPositiveButton(android.R.string.ok, null) - .create(); - dialog.show(); - TextView txtMessage = (TextView) dialog.findViewById(android.R.id.message); - txtMessage.setTextSize(14); - - mHadSegmentationFault = result.toLowerCase(Locale.US).contains("segmentation fault"); - refreshKnownIssue(); - } - - private void areYouSure(int messageTextId, DialogInterface.OnClickListener yesHandler) { - new AlertDialog.Builder(getActivity()) - .setTitle(messageTextId) - .setMessage(R.string.areyousure) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setPositiveButton(android.R.string.yes, yesHandler) - .setNegativeButton(android.R.string.no, null) - .create() - .show(); - } - - private void showConfirmDialog(final String message, final DialogInterface.OnClickListener yesHandler, - final DialogInterface.OnClickListener noHandler) { - if (Looper.myLooper() != Looper.getMainLooper()) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - showConfirmDialog(message, yesHandler, noHandler); - } - }); - return; - } - - AlertDialog dialog = new AlertDialog.Builder(getActivity()) - .setMessage(message) - .setPositiveButton(android.R.string.yes, yesHandler) - .setNegativeButton(android.R.string.no, noHandler) - .create(); - dialog.show(); - TextView txtMessage = (TextView) dialog.findViewById(android.R.id.message); - txtMessage.setTextSize(14); - - mHadSegmentationFault = message.toLowerCase(Locale.US).contains("segmentation fault"); - refreshKnownIssue(); - } - - private boolean checkCompatibility() { - mCompatibilityErrors.clear(); - return checkAppProcessCompatibility(); - } - - private boolean checkAppProcessCompatibility() { - try { - if (APP_PROCESS_NAME == null) - return false; - - File testFile = AssetUtil.writeAssetToCacheFile(APP_PROCESS_NAME, "app_process", 00700); - if (testFile == null) { - mCompatibilityErrors.add("could not write app_process to cache"); - return false; - } - - Process p = Runtime.getRuntime().exec(new String[] { testFile.getAbsolutePath(), "--xposedversion" }); - - BufferedReader stdout = new BufferedReader(new InputStreamReader(p.getInputStream())); - String result = stdout.readLine(); - stdout.close(); - - BufferedReader stderr = new BufferedReader(new InputStreamReader(p.getErrorStream())); - String errorLine; - while ((errorLine = stderr.readLine()) != null) { - mCompatibilityErrors.add(errorLine); - } - stderr.close(); - - p.destroy(); - - testFile.delete(); - return result != null && result.startsWith("Xposed version: "); - } catch (IOException e) { - mCompatibilityErrors.add(e.getMessage()); - return false; - } - } - - private boolean startShell() { - if (mRootUtil.startShell()) - return true; - - showAlert(getString(R.string.root_failed)); - return false; - } - - private int getInstallMode() { - int mode = XposedApp.getPreferences().getInt("install_mode", INSTALL_MODE_NORMAL); - if (mode < INSTALL_MODE_NORMAL || mode > INSTALL_MODE_RECOVERY_MANUAL) - mode = INSTALL_MODE_NORMAL; - return mode; - } - - private String getInstallModeText() { - final int installMode = getInstallMode(); - switch (installMode) { - case INSTALL_MODE_NORMAL: - return getString(R.string.install_mode_normal); - case INSTALL_MODE_RECOVERY_AUTO: - return getString(R.string.install_mode_recovery_auto); - case INSTALL_MODE_RECOVERY_MANUAL: - return getString(R.string.install_mode_recovery_manual); - } - throw new IllegalStateException("unknown install mode " + installMode); - } - - private boolean install() { - final int installMode = getInstallMode(); - - if (!startShell()) - return false; - - List messages = new LinkedList(); - boolean showAlert = true; - try { - messages.add(getString(R.string.sdcard_location, XposedApp.getInstance().getExternalFilesDir(null))); - messages.add(""); - - messages.add(getString(R.string.file_copying, "Xposed-Disabler-Recovery.zip")); - if (AssetUtil.writeAssetToSdcardFile("Xposed-Disabler-Recovery.zip", 00644) == null) { - messages.add(""); - messages.add(getString(R.string.file_extract_failed, "Xposed-Disabler-Recovery.zip")); - return false; - } - - File appProcessFile = AssetUtil.writeAssetToFile(APP_PROCESS_NAME, new File(XposedApp.BASE_DIR + "bin/app_process"), 00700); - if (appProcessFile == null) { - showAlert(getString(R.string.file_extract_failed, "app_process")); - return false; - } - - if (installMode == INSTALL_MODE_NORMAL) { - // Normal installation - messages.add(getString(R.string.file_mounting_writable, "/system")); - if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) { - messages.add(getString(R.string.file_mount_writable_failed, "/system")); - messages.add(getString(R.string.file_trying_to_continue)); - } - - if (new File("/system/bin/app_process.orig").exists()) { - messages.add(getString(R.string.file_backup_already_exists, "/system/bin/app_process.orig")); - } else { - if (mRootUtil.executeWithBusybox("cp -a /system/bin/app_process /system/bin/app_process.orig", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_backup_failed, "/system/bin/app_process")); - return false; - } else { - messages.add(getString(R.string.file_backup_successful, "/system/bin/app_process.orig")); - } - - mRootUtil.executeWithBusybox("sync", messages); - } - - messages.add(getString(R.string.file_copying, "app_process")); - if (mRootUtil.executeWithBusybox("cp -a " + appProcessFile.getAbsolutePath() + " /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_copy_failed, "app_process", "/system/bin")); - return false; - } - if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process")); - return false; - } - if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process")); - return false; - } - - } else if (installMode == INSTALL_MODE_RECOVERY_AUTO) { - if (!prepareAutoFlash(messages, "Xposed-Installer-Recovery.zip")) - return false; - - } else if (installMode == INSTALL_MODE_RECOVERY_MANUAL) { - if (!prepareManualFlash(messages, "Xposed-Installer-Recovery.zip")) - return false; - } - - File blocker = new File(XposedApp.BASE_DIR + "conf/disabled"); - if (blocker.exists()) { - messages.add(getString(R.string.file_removing, blocker.getAbsolutePath())); - if (mRootUtil.executeWithBusybox("rm " + blocker.getAbsolutePath(), messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_remove_failed, blocker.getAbsolutePath())); - return false; - } - } - - messages.add(getString(R.string.file_copying, "XposedBridge.jar")); - File jarFile = AssetUtil.writeAssetToFile("XposedBridge.jar", new File(JAR_PATH_NEWVERSION), 00644); - if (jarFile == null) { - messages.add(""); - messages.add(getString(R.string.file_extract_failed, "XposedBridge.jar")); - return false; - } - - mRootUtil.executeWithBusybox("sync", messages); - - showAlert = false; - messages.add(""); - if (installMode == INSTALL_MODE_NORMAL) - offerReboot(messages); - else - offerRebootToRecovery(messages, "Xposed-Installer-Recovery.zip", installMode); - - return true; - - } finally { - AssetUtil.removeBusybox(); - - if (showAlert) - showAlert(TextUtils.join("\n", messages).trim()); - } - } - - private boolean uninstall() { - final int installMode = getInstallMode(); - - new File(JAR_PATH_NEWVERSION).delete(); - new File(JAR_PATH).delete(); - new File(XposedApp.BASE_DIR + "bin/app_process").delete(); - - if (!startShell()) - return false; - - List messages = new LinkedList(); - boolean showAlert = true; - try { - messages.add(getString(R.string.sdcard_location, XposedApp.getInstance().getExternalFilesDir(null))); - messages.add(""); - - if (installMode == INSTALL_MODE_NORMAL) { - messages.add(getString(R.string.file_mounting_writable, "/system")); - if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) { - messages.add(getString(R.string.file_mount_writable_failed, "/system")); - messages.add(getString(R.string.file_trying_to_continue)); - } - - messages.add(getString(R.string.file_backup_restoring, "/system/bin/app_process.orig")); - if (!new File("/system/bin/app_process.orig").exists()) { - messages.add(""); - messages.add(getString(R.string.file_backup_not_found, "/system/bin/app_process.orig")); - return false; - } - - if (mRootUtil.executeWithBusybox("mv /system/bin/app_process.orig /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_move_failed, "/system/bin/app_process.orig", "/system/bin/app_process")); - return false; - } - if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process")); - return false; - } - if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process")); - return false; - } - // Might help on some SELinux-enforced ROMs, shouldn't hurt on others - mRootUtil.execute("/system/bin/restorecon /system/bin/app_process", null); - - } else if (installMode == INSTALL_MODE_RECOVERY_AUTO) { - if (!prepareAutoFlash(messages, "Xposed-Disabler-Recovery.zip")) - return false; - - } else if (installMode == INSTALL_MODE_RECOVERY_MANUAL) { - if (!prepareManualFlash(messages, "Xposed-Disabler-Recovery.zip")) - return false; - } - - showAlert = false; - messages.add(""); - if (installMode == INSTALL_MODE_NORMAL) - offerReboot(messages); - else - offerRebootToRecovery(messages, "Xposed-Disabler-Recovery.zip", installMode); - - return true; - - } finally { - AssetUtil.removeBusybox(); - - if (showAlert) - showAlert(TextUtils.join("\n", messages).trim()); - } - } - - private boolean prepareAutoFlash(List messages, String file) { - if (mRootUtil.execute("ls /cache/recovery", null) != 0) { - messages.add(getString(R.string.file_creating_directory, "/cache/recovery")); - if (mRootUtil.executeWithBusybox("mkdir /cache/recovery", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_create_directory_failed, "/cache/recovery")); - return false; - } - } - - messages.add(getString(R.string.file_copying, file)); - File tempFile = AssetUtil.writeAssetToCacheFile(file, 00644); - if (tempFile == null) { - messages.add(""); - messages.add(getString(R.string.file_extract_failed, file)); - return false; - } - - if (mRootUtil.executeWithBusybox("cp -a " + tempFile.getAbsolutePath() + " /cache/recovery/" + file, messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_copy_failed, file, "/cache")); - tempFile.delete(); - return false; - } - - tempFile.delete(); - - messages.add(getString(R.string.file_writing_recovery_command)); - if (mRootUtil.execute("echo \"--update_package=/cache/recovery/" + file + "\n--show_text\" > /cache/recovery/command", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.file_writing_recovery_command_failed)); - return false; - } - - return true; - } - - private boolean prepareManualFlash(List messages, String file) { - messages.add(getString(R.string.file_copying, file)); - if (AssetUtil.writeAssetToSdcardFile(file, 00644) == null) { - messages.add(""); - messages.add(getString(R.string.file_extract_failed, file)); - return false; - } - - return true; - } - - private void offerReboot(List messages) { - messages.add(getString(R.string.file_done)); - messages.add(""); - messages.add(getString(R.string.reboot_confirmation)); - showConfirmDialog(TextUtils.join("\n", messages).trim(), - new AsyncDialogClickListener(getString(R.string.reboot)) { - @Override - protected void onAsyncClick(DialogInterface dialog, int which) { - reboot(null); - } - }, null); - } - - private void offerRebootToRecovery(List messages, final String file, final int installMode) { - if (installMode == INSTALL_MODE_RECOVERY_AUTO) - messages.add(getString(R.string.auto_flash_note, file)); - else - messages.add(getString(R.string.manual_flash_note, file)); - - messages.add(""); - messages.add(getString(R.string.reboot_recovery_confirmation)); - showConfirmDialog(TextUtils.join("\n", messages).trim(), - new AsyncDialogClickListener(getString(R.string.reboot)) { - @Override - protected void onAsyncClick(DialogInterface dialog, int which) { - reboot("recovery"); - } - }, - new AsyncDialogClickListener(null) { - @Override - protected void onAsyncClick(DialogInterface dialog, int which) { - if (installMode == INSTALL_MODE_RECOVERY_AUTO) { - // clean up to avoid unwanted flashing - mRootUtil.executeWithBusybox("rm /cache/recovery/command", null); - mRootUtil.executeWithBusybox("rm /cache/recovery/" + file, null); - AssetUtil.removeBusybox(); - } - } - }); - } - - private void softReboot() { - if (!startShell()) - return; - - List messages = new LinkedList(); - if (mRootUtil.execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", messages) != 0) { - messages.add(""); - messages.add(getString(R.string.reboot_failed)); - showAlert(TextUtils.join("\n", messages).trim()); - } - } - - private void reboot(String mode) { - if (!startShell()) - return; - - List messages = new LinkedList(); - - String command = "reboot"; - if (mode != null) { - command += " " + mode; - if (mode.equals("recovery")) - // create a flag used by some kernels to boot into recovery - mRootUtil.executeWithBusybox("touch /cache/recovery/boot", messages); - } - - if (mRootUtil.executeWithBusybox(command, messages) != 0) { - messages.add(""); - messages.add(getString(R.string.reboot_failed)); - showAlert(TextUtils.join("\n", messages).trim()); - } - AssetUtil.removeBusybox(); - } -} diff --git a/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java b/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java index c4f6ec4c8..21a214d39 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/LogsFragment.java @@ -1,204 +1,351 @@ package de.robv.android.xposed.installer; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.Calendar; - +import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; +import android.support.v4.content.FileProvider; 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.CheckBox; import android.widget.HorizontalScrollView; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Calendar; + +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; +import static de.robv.android.xposed.installer.XposedApp.createFolder; + public class LogsFragment extends Fragment { - private File mFileErrorLog = new File(XposedApp.BASE_DIR + "log/error.log"); - private File mFileErrorLogOld = new File(XposedApp.BASE_DIR + "log/error.log.old"); - private static final int MAX_LOG_SIZE = 2*1024*1024; // 2 MB - private TextView mTxtLog; - private ScrollView mSVLog; - private HorizontalScrollView mHSVLog; - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.tab_logs, container, false); - mTxtLog = (TextView) v.findViewById(R.id.txtLog); - mSVLog = (ScrollView) v.findViewById(R.id.svLog); - mHSVLog = (HorizontalScrollView) v.findViewById(R.id.hsvLog); - reloadErrorLog(); - return v; - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_logs, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_refresh: - reloadErrorLog(); - return true; - case R.id.menu_send: - send(); - return true; - case R.id.menu_save: - save(); - return true; - case R.id.menu_clear: - clear(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void reloadErrorLog() { - StringBuilder logContent = new StringBuilder(15 * 1024); - try { - FileInputStream fis = new FileInputStream(mFileErrorLog); - long skipped = skipLargeFile(fis, mFileErrorLog.length()); - if (skipped > 0) { - logContent.append("-----------------\n"); - logContent.append(getResources().getString(R.string.log_too_large, MAX_LOG_SIZE / 1024, skipped / 1024)); - logContent.append("\n-----------------\n\n"); - } - Reader reader = new InputStreamReader(fis); - char[] temp = new char[1024]; - int read; - while ((read = reader.read(temp)) > 0) { - logContent.append(temp, 0, read); - } - reader.close(); - } catch (IOException e) { - logContent.append(getResources().getString(R.string.logs_load_failed)); - logContent.append('\n'); - logContent.append(e.getMessage()); - } - - if (logContent.length() > 0) - mTxtLog.setText(logContent.toString()); - else - mTxtLog.setText(R.string.log_is_empty); - - mSVLog.post(new Runnable() { - @Override - public void run() { - mSVLog.scrollTo(0, mTxtLog.getHeight()); - } - }); - mHSVLog.post(new Runnable() { - @Override - public void run() { - mHSVLog.scrollTo(0, 0); - } - }); - } - - private void clear() { - try { - new FileOutputStream(mFileErrorLog).close();; - mFileErrorLogOld.delete(); - Toast.makeText(getActivity(), R.string.logs_cleared, Toast.LENGTH_SHORT).show(); - reloadErrorLog(); - } catch (IOException e) { - Toast.makeText(getActivity(), - getResources().getString(R.string.logs_clear_failed) + "\n" + e.getMessage(), - Toast.LENGTH_LONG).show(); - return; - } - } - - private void send() { - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(mFileErrorLog)); - sendIntent.setType("application/text"); // text/plain is handled wrongly by too many apps - startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend))); - } - - @SuppressLint("DefaultLocale") - private void save() { - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); - return; - } - - Calendar now = Calendar.getInstance(); - String filename = String.format("xposed_%s_%04d%02d%02d_%02d%02d%02d.log", "error", - 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(getActivity().getExternalFilesDir(null), filename); - - try { - FileInputStream in = new FileInputStream(mFileErrorLog); - FileOutputStream out = new FileOutputStream(targetFile); - - long skipped = skipLargeFile(in, mFileErrorLog.length()); - if (skipped > 0) { - StringBuilder logContent = new StringBuilder(512); - logContent.append("-----------------\n"); - logContent.append(getResources().getString(R.string.log_too_large, MAX_LOG_SIZE / 1024, skipped / 1024)); - logContent.append("\n-----------------\n\n"); - out.write(logContent.toString().getBytes()); - } - - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) > 0){ - out.write(buffer, 0, len); - } - in.close(); - out.close(); - } catch (IOException e) { - Toast.makeText(getActivity(), - getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), - Toast.LENGTH_LONG).show(); - return; - } - - Toast.makeText(getActivity(), targetFile.toString(), Toast.LENGTH_LONG).show(); - } - - private long skipLargeFile(InputStream is, long length) throws IOException { - if (length < MAX_LOG_SIZE) - return 0; - - long skipped = length - MAX_LOG_SIZE; - long yetToSkip = skipped; - do { - yetToSkip -= is.skip(yetToSkip); - } while (yetToSkip > 0); - - int c; - do { - c = is.read(); - if (c == -1) - break; - skipped++; - } while (c != '\n'); - - return skipped; - } + + private File mFileErrorLog = new File(XposedApp.BASE_DIR + "log/error.log"); + private File mFileErrorLogOld = new File( + XposedApp.BASE_DIR + "log/error.log.old"); + private TextView mTxtLog; + private ScrollView mSVLog; + private HorizontalScrollView mHSVLog; + private MenuItem mClickedMenuItem = null; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_logs, container, false); + mTxtLog = v.findViewById(R.id.txtLog); + mTxtLog.setTextIsSelectable(true); + mSVLog = v.findViewById(R.id.svLog); + mHSVLog = v.findViewById(R.id.hsvLog); +/* + View scrollTop = v.findViewById(R.id.scroll_top); + View scrollDown = v.findViewById(R.id.scroll_down); + + scrollTop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + scrollTop(); + } + }); + scrollDown.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + scrollDown(); + } + }); +*/ + + if (!XposedApp.getPreferences().getBoolean("hide_logcat_warning", false)) { + final View dontShowAgainView = inflater.inflate(R.layout.dialog_install_warning, null); + + TextView message = dontShowAgainView.findViewById(android.R.id.message); + message.setText(R.string.not_logcat); + + new MaterialDialog.Builder(getActivity()) + .title(R.string.install_warning_title) + .customView(dontShowAgainView, false) + .positiveText(android.R.string.ok) + .callback(new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox); + if (checkBox.isChecked()) + XposedApp.getPreferences().edit().putBoolean("hide_logcat_warning", true).apply(); + } + }).cancelable(false).show(); + } + return v; + } + + @Override + public void onResume() { + super.onResume(); + reloadErrorLog(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_logs, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + mClickedMenuItem = item; + switch (item.getItemId()) { + case R.id.menu_scroll_top: + scrollTop(); + break; + case R.id.menu_scroll_down: + scrollDown(); + break; + case R.id.menu_refresh: + reloadErrorLog(); + return true; + case R.id.menu_send: + try { + send(); + } catch (NullPointerException ignored) { + } + return true; + case R.id.menu_save: + save(); + return true; + case R.id.menu_clear: + clear(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void scrollTop() { + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, 0); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void scrollDown() { + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, mTxtLog.getHeight()); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void reloadErrorLog() { + new LogsReader().execute(mFileErrorLog); + mSVLog.post(new Runnable() { + @Override + public void run() { + mSVLog.scrollTo(0, mTxtLog.getHeight()); + } + }); + mHSVLog.post(new Runnable() { + @Override + public void run() { + mHSVLog.scrollTo(0, 0); + } + }); + } + + private void clear() { + try { + new FileOutputStream(mFileErrorLog).close(); + mFileErrorLogOld.delete(); + mTxtLog.setText(R.string.log_is_empty); + Toast.makeText(getActivity(), R.string.logs_cleared, + Toast.LENGTH_SHORT).show(); + reloadErrorLog(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_clear_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + + private void send() { + Uri uri = FileProvider.getUriForFile(getActivity(), "de.robv.android.xposed.installer.fileprovider", mFileErrorLog); + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, uri); + sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(save())); + sendIntent.setType("application/html"); + startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend))); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOptionsItemSelected(mClickedMenuItem); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @SuppressLint("DefaultLocale") + private File save() { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return null; + } + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return null; + } + + Calendar now = Calendar.getInstance(); + String filename = String.format( + "xposed_%s_%04d%02d%02d_%02d%02d%02d.log", "error", + 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(createFolder(), filename); + + try { + FileInputStream in = new FileInputStream(mFileErrorLog); + FileOutputStream out = new FileOutputStream(targetFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + + Toast.makeText(getActivity(), targetFile.toString(), + Toast.LENGTH_LONG).show(); + return targetFile; + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return null; + } + } + + private class LogsReader extends AsyncTask { + + private static final int MAX_LOG_SIZE = 1000 * 1024; // 1000 KB + private MaterialDialog mProgressDialog; + + private long skipLargeFile(BufferedReader is, long length) throws IOException { + if (length < MAX_LOG_SIZE) + return 0; + + long skipped = length - MAX_LOG_SIZE; + long yetToSkip = skipped; + do { + yetToSkip -= is.skip(yetToSkip); + } while (yetToSkip > 0); + + int c; + do { + c = is.read(); + if (c == -1) + break; + skipped++; + } while (c != '\n'); + + return skipped; + + } + + @Override + protected void onPreExecute() { + mTxtLog.setText(""); + mProgressDialog = new MaterialDialog.Builder(getContext()).content(R.string.loading).progress(true, 0).show(); + } + + @Override + protected String doInBackground(File... log) { + Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2); + + StringBuilder llog = new StringBuilder(15 * 10 * 1024); + try { + File logfile = log[0]; + BufferedReader br; + br = new BufferedReader(new FileReader(logfile)); + long skipped = skipLargeFile(br, logfile.length()); + if (skipped > 0) { + llog.append("-----------------\n"); + llog.append("Log too long"); + llog.append("\n-----------------\n\n"); + } + + char[] temp = new char[1024]; + int read; + while ((read = br.read(temp)) > 0) { + llog.append(temp, 0, read); + } + br.close(); + } catch (IOException e) { + llog.append("Cannot read log"); + llog.append(e.getMessage()); + } + + return llog.toString(); + } + + @Override + protected void onPostExecute(String llog) { + mProgressDialog.dismiss(); + mTxtLog.setText(llog); + + if (llog.length() == 0) + mTxtLog.setText(R.string.log_is_empty); + } + + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java b/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java new file mode 100644 index 000000000..28df02cac --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/ModulesBookmark.java @@ -0,0 +1,342 @@ +package de.robv.android.xposed.installer; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.InstallApkUtil; +import de.robv.android.xposed.installer.util.RepoLoader; +import de.robv.android.xposed.installer.util.ThemeUtil; + +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; +import static de.robv.android.xposed.installer.XposedApp.darkenColor; + +public class ModulesBookmark extends XposedBaseActivity { + + private static RepoLoader mRepoLoader; + private static View container; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + mRepoLoader = RepoLoader.getInstance(); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.bookmarks); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + container = findViewById(R.id.container); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction().add(R.id.container, new ModulesBookmarkFragment()).commit(); + } + } + + public static class ModulesBookmarkFragment extends Fragment implements AdapterView.OnItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + + private List mBookmarkedModules = new ArrayList<>(); + private BookmarkModuleAdapter mAdapter; + private SharedPreferences mBookmarksPref; + private boolean changed; + private MenuItem mClickedMenuItem = null; + private ListView mListView; + private View mBackgroundList; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mBookmarksPref = getActivity().getSharedPreferences("bookmarks", MODE_PRIVATE); + mBookmarksPref.registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onResume() { + super.onResume(); + + if (changed) + getModules(); + + if (Build.VERSION.SDK_INT >= 21) { + getActivity().getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(getActivity()), 0.85f)); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mBookmarksPref.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getListView().setDivider(null); + getListView().setDividerHeight(getDp(6)); + getListView().setPadding(getDp(8), getDp(8), getDp(8), getDp(8)); + getListView().setOnItemClickListener(this); + getListView().setClipToPadding(false); + getListView().setEmptyView(mBackgroundList); + registerForContextMenu(getListView()); + + mAdapter = new BookmarkModuleAdapter(getActivity()); + getModules(); + getListView().setAdapter(mAdapter); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.list_fragment, container, false); + + mListView = view.findViewById(android.R.id.list); + + mBackgroundList = view.findViewById(R.id.background_list); + ((ImageView) view.findViewById(R.id.background_list_iv)).setImageResource(R.drawable.ic_bookmark); + ((TextView) view.findViewById(R.id.list_status)).setText(R.string.no_bookmark_added); + + return view; + } + + private void getModules() { + mAdapter.clear(); + mBookmarkedModules.clear(); + for (String s : mBookmarksPref.getAll().keySet()) { + boolean isBookmarked = mBookmarksPref.getBoolean(s, false); + + if (isBookmarked) { + Module m = mRepoLoader.getModule(s); + if (m != null) mBookmarkedModules.add(m); + } + } + Collections.sort(mBookmarkedModules, new Comparator() { + @Override + public int compare(Module mod1, Module mod2) { + return mod1.name.compareTo(mod2.name); + } + }); + mAdapter.addAll(mBookmarkedModules); + mAdapter.notifyDataSetChanged(); + } + + private int getDp(float value) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, metrics); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", mBookmarkedModules.get(position).packageName, null)); + startActivity(detailsIntent); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + changed = true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + Module module = getItemFromContextMenuInfo(menuInfo); + if (module == null) + return; + + menu.setHeaderTitle(module.name); + getActivity().getMenuInflater().inflate(R.menu.context_menu_modules_bookmark, menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + final Module module = getItemFromContextMenuInfo( + item.getMenuInfo()); + if (module == null) + return false; + + final String pkg = module.packageName; + ModuleVersion mv = DownloadsUtil.getStableVersion(module); + + if (mv == null) + return false; + + mClickedMenuItem = item; + + switch (item.getItemId()) { + case R.id.install_bookmark: + DownloadsUtil.addModule(getContext(), module.name, mv.downloadLink, false, new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + new InstallApkUtil(getContext(), info).execute(); + } + }); + break; + case R.id.install_remove_bookmark: + DownloadsUtil.addModule(getContext(), module.name, mv.downloadLink, false, new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + new InstallApkUtil(getContext(), info).execute(); + remove(pkg); + } + }); + break; + case R.id.download_bookmark: + if (checkPermissions()) + return false; + + DownloadsUtil.addModule(getContext(), module.name, mv.downloadLink, true, new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + Toast.makeText(context, getString(R.string.module_saved, info.localFilename), Toast.LENGTH_SHORT).show(); + } + }); + break; + case R.id.download_remove_bookmark: + if (checkPermissions()) + return false; + + DownloadsUtil.addModule(getContext(), module.name, mv.downloadLink, true, new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + remove(pkg); + Toast.makeText(context, getString(R.string.module_saved, info.localFilename), Toast.LENGTH_SHORT).show(); + } + }); + break; + case R.id.remove: + remove(pkg); + break; + } + + return false; + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onContextItemSelected(mClickedMenuItem); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + private void remove(final String pkg) { + mBookmarksPref.edit().putBoolean(pkg, false).apply(); + + Snackbar.make(container, R.string.bookmark_removed, Snackbar.LENGTH_SHORT).setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + mBookmarksPref.edit().putBoolean(pkg, true).apply(); + + getModules(); + } + }).show(); + + getModules(); + } + + private Module getItemFromContextMenuInfo(ContextMenu.ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + int position = info.position - getListView().getHeaderViewsCount(); + return (position >= 0) ? (Module) getListView().getAdapter().getItem(position) : null; + } + + public ListView getListView() { + return mListView; + } + } + + private static class BookmarkModuleAdapter extends ArrayAdapter { + public BookmarkModuleAdapter(Context context) { + super(context, R.layout.list_item_module, R.id.title); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + + view.findViewById(R.id.checkbox).setVisibility(View.GONE); + view.findViewById(R.id.version_name).setVisibility(View.GONE); + view.findViewById(R.id.icon).setVisibility(View.GONE); + + Module item = getItem(position); + + ((TextView) view.findViewById(R.id.title)).setText(item.name); + ((TextView) view.findViewById(R.id.description)) + .setText(item.summary); + + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java b/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java index eac1fa3f1..761446d04 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java +++ b/app/src/main/java/de/robv/android/xposed/installer/ModulesFragment.java @@ -1,10 +1,6 @@ package de.robv.android.xposed.installer; -import java.text.Collator; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; - +import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -12,17 +8,28 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; -import android.support.v4.app.ListFragment; +import android.os.Environment; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.util.DisplayMetrics; +import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +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.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ArrayAdapter; import android.widget.CheckBox; @@ -32,296 +39,576 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; + +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import de.robv.android.xposed.installer.installation.StatusInstallerFragment; +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; import de.robv.android.xposed.installer.repo.RepoDb; import de.robv.android.xposed.installer.repo.RepoDb.RowNotFoundException; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.InstallApkUtil; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; import de.robv.android.xposed.installer.util.NavUtil; -import de.robv.android.xposed.installer.util.NotificationUtil; +import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.ThemeUtil; -public class ModulesFragment extends ListFragment implements ModuleListener { - public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; - private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE"; - private static final String PLAY_STORE_PACKAGE = "com.android.vending"; - private static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s"; - private static String PLAY_STORE_LABEL = null; - private int installedXposedVersion; - private ModuleUtil mModuleUtil; - private ModuleAdapter mAdapter = null; - private PackageManager mPm = null; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mModuleUtil = ModuleUtil.getInstance(); - mPm = getActivity().getPackageManager(); - if (PLAY_STORE_LABEL == null) { - try { - ApplicationInfo ai = mPm.getApplicationInfo(PLAY_STORE_PACKAGE, 0); - PLAY_STORE_LABEL = mPm.getApplicationLabel(ai).toString(); - } catch (NameNotFoundException ignored) {} - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - installedXposedVersion = XposedApp.getActiveXposedVersion(); - if (installedXposedVersion <= 0) { - View notActiveNote = getActivity().getLayoutInflater().inflate( - R.layout.xposed_not_active_note, getListView(), false); - notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG); - getListView().addHeaderView(notActiveNote); - } - - mAdapter = new ModuleAdapter(getActivity()); - reloadModules.run(); - setListAdapter(mAdapter); - setEmptyText(getActivity().getString(R.string.no_xposed_modules_found)); - registerForContextMenu(getListView()); - mModuleUtil.addListener(this); - - ActionBar actionBar = ((WelcomeActivity) getActivity()).getSupportActionBar(); - - DisplayMetrics metrics = getResources().getDisplayMetrics(); - int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); - int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); - int toolBarDp = actionBar.getHeight() == 0 ? 196 : actionBar.getHeight(); - - getListView().setDivider(null); - getListView().setDividerHeight(sixDp); - getListView().setPadding(eightDp, toolBarDp + eightDp, eightDp, eightDp); - getListView().setClipToPadding(false); - } - - @Override - public void onResume() { - super.onResume(); - NotificationUtil.cancelAll(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mModuleUtil.removeListener(this); - setListAdapter(null); - mAdapter = null; - } - - private Runnable reloadModules = new Runnable() { - public void run() { - mAdapter.setNotifyOnChange(false); - mAdapter.clear(); - mAdapter.addAll(mModuleUtil.getModules().values()); - final Collator col = Collator.getInstance(Locale.getDefault()); - mAdapter.sort(new Comparator() { - @Override - public int compare(InstalledModule lhs, InstalledModule rhs) { - return col.compare(lhs.getAppName(), rhs.getAppName()); - } - }); - mAdapter.notifyDataSetChanged(); - } - }; - - @Override - public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { - getActivity().runOnUiThread(reloadModules); - } - - @Override - public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { - getActivity().runOnUiThread(reloadModules); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - String packageName = (String) v.getTag(); - if (packageName == null) - return; - - if (packageName.equals(NOT_ACTIVE_NOTE_TAG)) { - ((WelcomeActivity) getActivity()).switchFragment(0); - return; - } - - Intent launchIntent = getSettingsIntent(packageName); - if (launchIntent != null) - startActivity(launchIntent); - else - Toast.makeText(getActivity(), getActivity().getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - InstalledModule installedModule = getItemFromContextMenuInfo(menuInfo); - if (installedModule == null) - return; - - menu.setHeaderTitle(installedModule.getAppName()); - getActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu); - - if (getSettingsIntent(installedModule.packageName) == null) - menu.removeItem(R.id.menu_launch); - - try { - String support = RepoDb.getModuleSupport(installedModule.packageName); - if (NavUtil.parseURL(support) == null) - menu.removeItem(R.id.menu_support); - } catch (RowNotFoundException e) { - menu.removeItem(R.id.menu_download_updates); - menu.removeItem(R.id.menu_support); - } - - String installer = mPm.getInstallerPackageName(installedModule.packageName); - if (PLAY_STORE_LABEL != null && PLAY_STORE_PACKAGE.equals(installer)) - menu.findItem(R.id.menu_play_store).setTitle(PLAY_STORE_LABEL); - else - menu.removeItem(R.id.menu_play_store); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - InstalledModule module = getItemFromContextMenuInfo(item.getMenuInfo()); - if (module == null) - return false; - - switch (item.getItemId()) { - case R.id.menu_launch: - startActivity(getSettingsIntent(module.packageName)); - return true; - - case R.id.menu_download_updates: - Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); - detailsIntent.setData(Uri.fromParts("package", module.packageName, null)); - startActivity(detailsIntent); - return true; - - case R.id.menu_support: - NavUtil.startURL(getActivity(), RepoDb.getModuleSupport(module.packageName)); - return true; - - case R.id.menu_play_store: - Intent i = new Intent(android.content.Intent.ACTION_VIEW); - i.setData(Uri.parse(String.format(PLAY_STORE_LINK, module.packageName))); - i.setPackage(PLAY_STORE_PACKAGE); - try { - startActivity(i); - } catch (ActivityNotFoundException e) { - i.setPackage(null); - startActivity(i); - } - return true; - - case R.id.menu_app_info: - startActivity(new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", module.packageName, null))); - return true; - - case R.id.menu_uninstall: - startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, - Uri.fromParts("package", module.packageName, null))); - return true; - } - - return false; - } - - private InstalledModule getItemFromContextMenuInfo(ContextMenuInfo menuInfo) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - int position = info.position - getListView().getHeaderViewsCount(); - return (position >= 0) ? (InstalledModule) getListAdapter().getItem(position) : null; - } - - private Intent getSettingsIntent(String packageName) { - // taken from ApplicationPackageManager.getLaunchIntentForPackage(String) - // first looks for an Xposed-specific category, falls back to getLaunchIntentForPackage - PackageManager pm = getActivity().getPackageManager(); - - Intent intentToResolve = new Intent(Intent.ACTION_MAIN); - intentToResolve.addCategory(SETTINGS_CATEGORY); - intentToResolve.setPackage(packageName); - List ris = pm.queryIntentActivities(intentToResolve, 0); - - if (ris == null || ris.size() <= 0) { - return pm.getLaunchIntentForPackage(packageName); - } - - Intent intent = new Intent(intentToResolve); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); - return intent; - } - - private class ModuleAdapter extends ArrayAdapter { - public ModuleAdapter(Context context) { - super(context, R.layout.list_item_module, R.id.title); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - - if (convertView == null) { - // The reusable view was created for the first time, set up the listener on the checkbox - ((CheckBox) view.findViewById(R.id.checkbox)).setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - String packageName = (String) buttonView.getTag(); - boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked; - if (changed) { - mModuleUtil.setModuleEnabled(packageName, isChecked); - mModuleUtil.updateModulesList(true); - } - } - }); - } - - InstalledModule item = getItem(position); - - TextView version = (TextView) view.findViewById(R.id.version_name); - version.setText(item.versionName); - - // Store the package name in some views' tag for later access - ((CheckBox) view.findViewById(R.id.checkbox)).setTag(item.packageName); - view.setTag(item.packageName); - - ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); - - TextView descriptionText = (TextView) view.findViewById(R.id.description); - if (!item.getDescription().isEmpty()) { - descriptionText.setText(item.getDescription()); - descriptionText.setTextColor(ThemeUtil.getThemeColor(getContext(), android.R.attr.textColorSecondary)); - } else { - descriptionText.setText(getString(R.string.module_empty_description)); - descriptionText.setTextColor(getResources().getColor(R.color.warning)); - } - - CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox); - checkbox.setChecked(mModuleUtil.isModuleEnabled(item.packageName)); - TextView warningText = (TextView) view.findViewById(R.id.warning); - - if (item.minVersion == 0) { - checkbox.setEnabled(false); - warningText.setText(getString(R.string.no_min_version_specified)); - warningText.setVisibility(View.VISIBLE); - } else if (installedXposedVersion != 0 && item.minVersion > installedXposedVersion) { - checkbox.setEnabled(false); - warningText.setText(String.format(getString(R.string.warning_xposed_min_version), - item.minVersion)); - warningText.setVisibility(View.VISIBLE); - } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { - checkbox.setEnabled(false); - warningText.setText(String.format(getString(R.string.warning_min_version_too_low), - item.minVersion, ModuleUtil.MIN_MODULE_VERSION)); - warningText.setVisibility(View.VISIBLE); - } else { - checkbox.setEnabled(true); - warningText.setVisibility(View.GONE); - } - return view; - } - } +import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; +import static de.robv.android.xposed.installer.XposedApp.createFolder; + +public class ModulesFragment extends Fragment implements ModuleListener, AdapterView.OnItemClickListener { + public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; + public static final String PLAY_STORE_PACKAGE = "com.android.vending"; + public static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s"; + public static final String XPOSED_REPO_LINK = "http://repo.xposed.info/module/%s"; + private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE"; + private static String PLAY_STORE_LABEL = null; + private int installedXposedVersion; + private ModuleUtil mModuleUtil; + private ModuleAdapter mAdapter = null; + private PackageManager mPm = null; + private Runnable reloadModules = new Runnable() { + public void run() { + mAdapter.setNotifyOnChange(false); + mAdapter.clear(); + mAdapter.addAll(mModuleUtil.getModules().values()); + final Collator col = Collator.getInstance(Locale.getDefault()); + mAdapter.sort(new Comparator() { + @Override + public int compare(InstalledModule lhs, InstalledModule rhs) { + return col.compare(lhs.getAppName(), rhs.getAppName()); + } + }); + mAdapter.notifyDataSetChanged(); + } + }; + private MenuItem mClickedMenuItem = null; + private ListView mListView; + private View mBackgroundList; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mModuleUtil = ModuleUtil.getInstance(); + mPm = getActivity().getPackageManager(); + if (PLAY_STORE_LABEL == null) { + try { + ApplicationInfo ai = mPm.getApplicationInfo(PLAY_STORE_PACKAGE, + 0); + PLAY_STORE_LABEL = mPm.getApplicationLabel(ai).toString(); + } catch (NameNotFoundException ignored) { + } + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + installedXposedVersion = XposedApp.getXposedVersion(); + if (Build.VERSION.SDK_INT >= 21) { + if (installedXposedVersion <= 0) { + addHeader(); + } + } else { + if (StatusInstallerFragment.DISABLE_FILE.exists()) installedXposedVersion = -1; + if (installedXposedVersion <= 0) { + addHeader(); + } + } + mAdapter = new ModuleAdapter(getActivity()); + reloadModules.run(); + getListView().setAdapter(mAdapter); + registerForContextMenu(getListView()); + mModuleUtil.addListener(this); + + ActionBar actionBar = ((WelcomeActivity) getActivity()).getSupportActionBar(); + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); + int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); + assert actionBar != null; + int toolBarDp = actionBar.getHeight() == 0 ? 196 : actionBar.getHeight(); + + getListView().setDivider(null); + getListView().setDividerHeight(sixDp); + getListView().setPadding(eightDp, toolBarDp + eightDp, eightDp, eightDp); + getListView().setClipToPadding(false); + getListView().setOnItemClickListener(this); + getListView().setEmptyView(mBackgroundList); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.list_fragment, container, false); + + mListView = view.findViewById(android.R.id.list); + + mBackgroundList = view.findViewById(R.id.background_list); + ((ImageView) view.findViewById(R.id.background_list_iv)).setImageResource(R.drawable.ic_nav_modules); + ((TextView) view.findViewById(R.id.list_status)).setText(R.string.no_xposed_modules_found); + + return view; + } + + private void addHeader() { + View notActiveNote = getActivity().getLayoutInflater().inflate(R.layout.xposed_not_active_note, getListView(), false); + notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG); + getListView().addHeaderView(notActiveNote); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_modules, menu); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOptionsItemSelected(mClickedMenuItem); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.bookmarks) { + startActivity(new Intent(getActivity(), ModulesBookmark.class)); + return true; + } + + File enabledModulesPath = new File(createFolder(), "enabled_modules.list"); + File installedModulesPath = new File(createFolder(), "installed_modules.list"); + File listModules = new File(XposedApp.ENABLED_MODULES_LIST_FILE); + + mClickedMenuItem = item; + + if (checkPermissions()) + return false; + + switch (item.getItemId()) { + case R.id.export_enabled_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return false; + } + + if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.no_enabled_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + createFolder(); + + FileInputStream in = new FileInputStream(listModules); + FileOutputStream out = new FileOutputStream(enabledModulesPath); + + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(getActivity(), enabledModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.export_installed_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + Map installedModules = ModuleUtil.getInstance().getModules(); + + if (installedModules.isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.no_installed_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + createFolder(); + + FileWriter fw = new FileWriter(installedModulesPath); + BufferedWriter bw = new BufferedWriter(fw); + PrintWriter fileOut = new PrintWriter(bw); + + Set keys = installedModules.keySet(); + for (Object key1 : keys) { + String packageName = (String) key1; + fileOut.println(packageName); + } + + fileOut.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(getActivity(), installedModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.import_installed_modules: + return importModules(installedModulesPath); + case R.id.import_enabled_modules: + return importModules(enabledModulesPath); + } + return super.onOptionsItemSelected(item); + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + private boolean importModules(File path) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + InputStream ips = null; + RepoLoader repoLoader = RepoLoader.getInstance(); + List list = new ArrayList<>(); + if (!path.exists()) { + Toast.makeText(getActivity(), getString(R.string.no_backup_found), + Toast.LENGTH_LONG).show(); + return false; + } + try { + ips = new FileInputStream(path); + } catch (FileNotFoundException e) { + Log.e(XposedApp.TAG, "ModulesFragment -> " + e.getMessage()); + } + + if (path.length() == 0) { + Toast.makeText(getActivity(), R.string.file_is_empty, + Toast.LENGTH_LONG).show(); + return false; + } + + try { + assert ips != null; + InputStreamReader ipsr = new InputStreamReader(ips); + BufferedReader br = new BufferedReader(ipsr); + String line; + while ((line = br.readLine()) != null) { + Module m = repoLoader.getModule(line); + + if (m == null) { + Toast.makeText(getActivity(), getString(R.string.download_details_not_found, + line), Toast.LENGTH_SHORT).show(); + } else { + list.add(m); + } + } + br.close(); + } catch (ActivityNotFoundException | IOException e) { + Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); + } + + for (final Module m : list) { + ModuleVersion mv = null; + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + mv = mvTemp; + break; + } + } + + if (mv != null) { + DownloadsUtil.addModule(getContext(), m.name, mv.downloadLink, false, new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + new InstallApkUtil(getContext(), info).execute(); + } + }); + } + } + + ModuleUtil.getInstance().reloadInstalledModules(); + + return true; + } + + private void showAlert(final String result) { + MaterialDialog dialog = new MaterialDialog.Builder(getActivity()).content(result).positiveText(android.R.string.ok).build(); + dialog.show(); + + TextView txtMessage = (TextView) dialog + .findViewById(android.R.id.message); + try { + txtMessage.setTextSize(14); + } catch (NullPointerException ignored) { + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mModuleUtil.removeListener(this); + getListView().setAdapter(null); + mAdapter = null; + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + getActivity().runOnUiThread(reloadModules); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + getActivity().runOnUiThread(reloadModules); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + InstalledModule installedModule = getItemFromContextMenuInfo(menuInfo); + if (installedModule == null) + return; + + menu.setHeaderTitle(installedModule.getAppName()); + getActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu); + + if (getSettingsIntent(installedModule.packageName) == null) + menu.removeItem(R.id.menu_launch); + + try { + String support = RepoDb + .getModuleSupport(installedModule.packageName); + if (NavUtil.parseURL(support) == null) + menu.removeItem(R.id.menu_support); + } catch (RowNotFoundException e) { + menu.removeItem(R.id.menu_download_updates); + menu.removeItem(R.id.menu_support); + } + + try { + String installer = mPm.getInstallerPackageName(installedModule.packageName); + if (PLAY_STORE_LABEL != null && PLAY_STORE_PACKAGE.equals(installer)) + menu.findItem(R.id.menu_play_store).setTitle(PLAY_STORE_LABEL); + else + menu.removeItem(R.id.menu_play_store); + } catch (Exception e) { + menu.removeItem(R.id.menu_play_store); + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + InstalledModule module = getItemFromContextMenuInfo(item.getMenuInfo()); + if (module == null) + return false; + + switch (item.getItemId()) { + case R.id.menu_launch: + startActivity(getSettingsIntent(module.packageName)); + return true; + + case R.id.menu_download_updates: + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", module.packageName, null)); + startActivity(detailsIntent); + return true; + + case R.id.menu_support: + NavUtil.startURL(getActivity(), Uri.parse(RepoDb.getModuleSupport(module.packageName))); + return true; + + case R.id.menu_play_store: + Intent i = new Intent(android.content.Intent.ACTION_VIEW); + i.setData(Uri.parse(String.format(PLAY_STORE_LINK, module.packageName))); + i.setPackage(PLAY_STORE_PACKAGE); + try { + startActivity(i); + } catch (ActivityNotFoundException e) { + i.setPackage(null); + startActivity(i); + } + return true; + + case R.id.menu_app_info: + startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null))); + return true; + + case R.id.menu_uninstall: + startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null))); + return true; + } + + return false; + } + + private InstalledModule getItemFromContextMenuInfo(ContextMenuInfo menuInfo) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + return (InstalledModule) getListView().getAdapter().getItem(info.position); + } + + private Intent getSettingsIntent(String packageName) { + // taken from + // ApplicationPackageManager.getLaunchIntentForPackage(String) + // first looks for an Xposed-specific category, falls back to + // getLaunchIntentForPackage + PackageManager pm = getActivity().getPackageManager(); + + Intent intentToResolve = new Intent(Intent.ACTION_MAIN); + intentToResolve.addCategory(SETTINGS_CATEGORY); + intentToResolve.setPackage(packageName); + List ris = pm.queryIntentActivities(intentToResolve, 0); + + if (ris == null || ris.size() <= 0) { + return pm.getLaunchIntentForPackage(packageName); + } + + Intent intent = new Intent(intentToResolve); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); + return intent; + } + + public ListView getListView() { + return mListView; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + String packageName = (String) view.getTag(); + if (packageName == null) + return; + + if (packageName.equals(NOT_ACTIVE_NOTE_TAG)) { + ((WelcomeActivity) getActivity()).switchFragment(0); + return; + } + + Intent launchIntent = getSettingsIntent(packageName); + if (launchIntent != null) + startActivity(launchIntent); + else + Toast.makeText(getActivity(), getActivity().getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); + } + + private class ModuleAdapter extends ArrayAdapter { + public ModuleAdapter(Context context) { + super(context, R.layout.list_item_module, R.id.title); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + + if (convertView == null) { + // The reusable view was created for the first time, set up the + // listener on the checkbox + ((CheckBox) view.findViewById(R.id.checkbox)).setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + String packageName = (String) buttonView.getTag(); + boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked; + if (changed) { + mModuleUtil.setModuleEnabled(packageName, isChecked); + mModuleUtil.updateModulesList(true); + } + } + }); + } + + InstalledModule item = getItem(position); + + TextView version = view.findViewById(R.id.version_name); + version.setText(item.versionName); + version.setSelected(true); + version.setTextColor(Color.parseColor("#808080")); + + // Store the package name in some views' tag for later access + view.findViewById(R.id.checkbox).setTag(item.packageName); + view.setTag(item.packageName); + + ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); + + TextView descriptionText = view.findViewById(R.id.description); + if (!item.getDescription().isEmpty()) { + descriptionText.setText(item.getDescription()); + descriptionText.setTextColor(ThemeUtil.getThemeColor(getContext(), android.R.attr.textColorSecondary)); + } else { + descriptionText.setText(getString(R.string.module_empty_description)); + descriptionText.setTextColor(getResources().getColor(R.color.warning)); + } + + CheckBox checkbox = view.findViewById(R.id.checkbox); + checkbox.setChecked(mModuleUtil.isModuleEnabled(item.packageName)); + TextView warningText = view.findViewById(R.id.warning); + + if (item.minVersion == 0) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.no_min_version_specified)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion > 0 && item.minVersion > installedXposedVersion) { + checkbox.setEnabled(false); + warningText.setText(String.format(getString(R.string.warning_xposed_min_version), item.minVersion)); + warningText.setVisibility(View.VISIBLE); + } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { + checkbox.setEnabled(false); + warningText.setText(String.format(getString(R.string.warning_min_version_too_low), item.minVersion, ModuleUtil.MIN_MODULE_VERSION)); + warningText.setVisibility(View.VISIBLE); + } else if (item.isInstalledOnExternalStorage()) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.warning_installed_on_external_storage)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion == 0 || (installedXposedVersion == -1 && !StatusInstallerFragment.DISABLE_FILE.exists())) { + checkbox.setEnabled(false); + warningText.setText(getString(R.string.not_installed_no_lollipop)); + warningText.setVisibility(View.VISIBLE); + } else { + checkbox.setEnabled(true); + warningText.setVisibility(View.GONE); + } + return view; + } + } + } diff --git a/app/src/main/java/de/robv/android/xposed/installer/PackageChangeReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/PackageChangeReceiver.java deleted file mode 100644 index 2e4d2e02f..000000000 --- a/app/src/main/java/de/robv/android/xposed/installer/PackageChangeReceiver.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.robv.android.xposed.installer; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import de.robv.android.xposed.installer.util.AssetUtil; -import de.robv.android.xposed.installer.util.ModuleUtil; -import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; -import de.robv.android.xposed.installer.util.NotificationUtil; - -public class PackageChangeReceiver extends BroadcastReceiver { - private final static ModuleUtil mModuleUtil = ModuleUtil.getInstance(); - - @Override - public void onReceive(final Context context, final Intent intent) { - if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED) - && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) - // Ignore existing packages being removed in order to be updated - return; - - String packageName = getPackageName(intent); - if (packageName == null) - return; - - if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) { - // make sure that the change is for the complete package, not only a component - String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); - if (components != null) { - boolean isForPackage = false; - for (String component : components) { - if (packageName.equals(component)) { - isForPackage = true; - break; - } - } - if (!isForPackage) - return; - } - } - - if (packageName.equals(AssetUtil.STATIC_BUSYBOX_PACKAGE)) { - AssetUtil.checkStaticBusyboxAvailability(); - AssetUtil.removeBusybox(); - return; - } - - InstalledModule module = ModuleUtil.getInstance().reloadSingleModule(packageName); - if (module == null || intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { - // Package being removed, disable it if it was a previously active Xposed mod - if (mModuleUtil.isModuleEnabled(packageName)) { - mModuleUtil.setModuleEnabled(packageName, false); - mModuleUtil.updateModulesList(false); - } - return; - } - - if (mModuleUtil.isModuleEnabled(packageName)) { - mModuleUtil.updateModulesList(false); - NotificationUtil.showModulesUpdatedNotification(); - } else { - NotificationUtil.showNotActivatedNotification(packageName, module.getAppName()); - } - } - - private static String getPackageName(Intent intent) { - Uri uri = intent.getData(); - return (uri != null) ? uri.getSchemeSpecificPart() : null; - } -} diff --git a/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java b/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java index 26661cd9e..baf33bace 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/SettingsActivity.java @@ -1,37 +1,58 @@ package de.robv.android.xposed.installer; +import android.Manifest; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.SwitchPreference; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.Toast; +import com.afollestad.materialdialogs.color.ColorChooserDialog; +import com.afollestad.materialdialogs.folderselector.FolderChooserDialog; + import java.io.File; import java.io.IOException; +import java.util.Locale; import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.ThemeUtil; -import de.robv.android.xposed.installer.util.UIUtil; -public class SettingsActivity extends XposedBaseActivity { +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; +import static de.robv.android.xposed.installer.XposedApp.darkenColor; + +public class SettingsActivity extends XposedBaseActivity implements ColorChooserDialog.ColorCallback, FolderChooserDialog.FolderCallback { + + private static SwitchPreference navBar; + private Toolbar toolbar; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setTheme(this); setContentView(R.layout.activity_container); - if (UIUtil.isLollipop()) { - this.getWindow().setStatusBarColor(this.getResources().getColor(R.color.colorPrimaryDark)); - } - - Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); - mToolbar.setNavigationOnClickListener(new View.OnClickListener() { + toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { finish(); @@ -44,16 +65,116 @@ public void onClick(View view) { ab.setDisplayHomeAsUpEnabled(true); } + setFloating(toolbar, 0); + if (savedInstanceState == null) { getFragmentManager().beginTransaction() - .add(R.id.container, new SettingsFragment()) - .commit(); + .add(R.id.container, new SettingsFragment()).commit(); + } + + } + + @Override + public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { + if (folder.canWrite()) { + XposedApp.getPreferences().edit().putString("download_location", folder.getPath()).apply(); + } else { + Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_SHORT).show(); } } - public static class SettingsFragment extends PreferenceFragment { + @Override + public void onColorSelection(ColorChooserDialog dialog, @ColorInt int color) { + int colorFrom = XposedApp.getColor(this); + + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, color); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + int color = (int) animator.getAnimatedValue(); + + toolbar.setBackgroundColor(color); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + int darkenColor = XposedApp.darkenColor(color, 0.85f); + + getWindow().setStatusBarColor(darkenColor); + + if (navBar != null && navBar.isChecked()) { + getWindow().setNavigationBarColor(darkenColor); + } + } + } + }); + colorAnimation.setDuration(750); + colorAnimation.start(); + + if (!dialog.isAccentMode()) { + XposedApp.getPreferences().edit().putInt("colors", color).apply(); + } + } + + public static class SettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + + public final static int[] PRIMARY_COLORS = new int[]{ + Color.parseColor("#F44336"), + Color.parseColor("#E91E63"), + Color.parseColor("#9C27B0"), + Color.parseColor("#673AB7"), + Color.parseColor("#3F51B5"), + Color.parseColor("#2196F3"), + Color.parseColor("#03A9F4"), + Color.parseColor("#00BCD4"), + Color.parseColor("#009688"), + Color.parseColor("#4CAF50"), + Color.parseColor("#8BC34A"), + Color.parseColor("#CDDC39"), + Color.parseColor("#FFEB3B"), + Color.parseColor("#FFC107"), + Color.parseColor("#FF9800"), + Color.parseColor("#FF5722"), + Color.parseColor("#795548"), + Color.parseColor("#9E9E9E"), + Color.parseColor("#607D8B") + }; + private static final File mDisableResourcesFlag = new File(XposedApp.BASE_DIR + "conf/disable_resources"); + private Preference mClickedPreference; + + private Preference.OnPreferenceChangeListener iconChange = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String act = ".WelcomeActivity"; + String[] iconsValues = new String[]{"dvdandroid", "hjmodi", "rovo", "rovoold", "staol"}; + + Context context = getActivity(); + PackageManager pm = getActivity().getPackageManager(); + String packName = getActivity().getPackageName(); + + for (String s : iconsValues) { + pm.setComponentEnabledSetting(new ComponentName(packName, packName + act + s), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); + } + + act += iconsValues[Integer.parseInt((String) newValue)]; + + int drawable = XposedApp.iconsValues[Integer.parseInt((String) newValue)]; + + if (Build.VERSION.SDK_INT >= 21) { + + ActivityManager.TaskDescription tDesc = new ActivityManager.TaskDescription(getString(R.string.app_name), + XposedApp.drawableToBitmap(context.getDrawable(drawable)), + XposedApp.getColor(context)); + getActivity().setTaskDescription(tDesc); + } + + pm.setComponentEnabledSetting(new ComponentName(context, packName + act), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); + return true; + } + }; + + private Preference downloadLocation; + public SettingsFragment() { } @@ -62,20 +183,21 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.prefs); - findPreference("enable_downloads").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (Boolean) newValue; - if (enabled) { - preference.getEditor().putBoolean("enable_downloads", enabled).apply(); - RepoLoader.getInstance().refreshRepositories(); - RepoLoader.getInstance().triggerReload(true); - } else { - RepoLoader.getInstance().clear(true); - } - return true; - } - }); + PreferenceGroup groupApp = (PreferenceGroup) findPreference("group_app"); + PreferenceGroup lookFeel = (PreferenceGroup) findPreference("look_and_feel"); + + Preference headsUp = findPreference("heads_up"); + Preference colors = findPreference("colors"); + Preference forceEnglish = findPreference("force_english"); + downloadLocation = findPreference("download_location"); + + ListPreference customIcon = (ListPreference) findPreference("custom_icon"); + navBar = (SwitchPreference) findPreference("nav_bar"); + + if (Build.VERSION.SDK_INT < 21) { + groupApp.removePreference(headsUp); + lookFeel.removePreference(navBar); + } findPreference("release_type_global").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override @@ -104,15 +226,97 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { } }); - Preference prefTheme = findPreference("theme"); - prefTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - getActivity().recreate(); - getActivity().finish(); // prevents 2 instances of settings from opening - return true; + colors.setOnPreferenceClickListener(this); + customIcon.setOnPreferenceChangeListener(iconChange); + downloadLocation.setOnPreferenceClickListener(this); + + if (Locale.getDefault().getLanguage().contains("en") + && !XposedApp.getPreferences().getBoolean("force_english", false)) { + groupApp.removePreference(forceEnglish); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + + if (Build.VERSION.SDK_INT >= 21) + getActivity().getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(getActivity()), 0.85f)); + } + + @Override + public void onPause() { + super.onPause(); + + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals("theme") || key.equals("nav_bar") || key.equals("ignore_chinese")) + getActivity().recreate(); + + if (key.equals("force_english")) + Toast.makeText(getActivity(), getString(R.string.warning_language), Toast.LENGTH_SHORT).show(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + SettingsActivity act = (SettingsActivity) getActivity(); + if (act == null) + return false; + + if (preference.getKey().equals("colors")) { + new ColorChooserDialog.Builder(act, preference.getTitleRes()) + .backButton(R.string.back) + .allowUserColorInput(false) + .customColors(PRIMARY_COLORS, null) + .doneButton(android.R.string.ok) + .preselect(XposedApp.getColor(act)).show(); + } else if (preference.getKey().equals(downloadLocation.getKey())) { + if (checkPermissions()) { + mClickedPreference = downloadLocation; + return false; } - }); + + new FolderChooserDialog.Builder(act) + .cancelButton(android.R.string.cancel) + .initialPath(XposedApp.getDownloadPath()) + .show(); + } + + return true; + } + + private boolean checkPermissions() { + if (Build.VERSION.SDK_INT < 23) return false; + + if (ActivityCompat.checkSelfPermission(getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedPreference != null) { + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + onPreferenceClick(mClickedPreference); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java b/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java index f8eaba887..d190d241b 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/SupportActivity.java @@ -1,8 +1,8 @@ package de.robv.android.xposed.installer; -import android.support.v4.app.Fragment; -import android.content.Intent; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; @@ -10,64 +10,81 @@ import android.view.ViewGroup; import android.widget.TextView; +import de.robv.android.xposed.installer.util.NavUtil; import de.robv.android.xposed.installer.util.ThemeUtil; -import de.robv.android.xposed.installer.util.UIUtil; + +import static de.robv.android.xposed.installer.XposedApp.darkenColor; public class SupportActivity extends XposedBaseActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ThemeUtil.setTheme(this); - setContentView(R.layout.activity_container); - - if (UIUtil.isLollipop()) { - this.getWindow().setStatusBarColor(this.getResources().getColor(R.color.colorPrimaryDark)); - } - - Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - - mToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setTitle(R.string.nav_item_support); - ab.setDisplayHomeAsUpEnabled(true); - } - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .add(R.id.container, new SupportFragment()) - .commit(); - } - } - - public static class SupportFragment extends Fragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - ViewGroup vg = (ViewGroup) inflater.inflate(R.layout.tab_support, container, false); - - TextView txtModuleSupport = ((TextView) vg.findViewById(R.id.tab_support_module_description)); - txtModuleSupport.setText(getString(R.string.support_modules_description, getString(R.string.module_support))); - txtModuleSupport.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(getActivity(), XposedBaseActivity.class); - startActivity(intent); - } - }); - - return vg; - } - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_container); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.nav_item_support); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, 0); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction().add(R.id.container, new SupportFragment()).commit(); + } + } + + public static class SupportFragment extends Fragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + if (Build.VERSION.SDK_INT >= 21) + getActivity().getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(getActivity()), 0.85f)); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_support, container, false); + + View installerSupportView = v.findViewById(R.id.installerSupportView); + View faqView = v.findViewById(R.id.faqView); + View donateView = v.findViewById(R.id.donateView); + TextView txtModuleSupport = v.findViewById(R.id.tab_support_module_description); + + txtModuleSupport.setText(getString(R.string.support_modules_description, + getString(R.string.module_support))); + + setupView(installerSupportView, R.string.support_material_xda); + setupView(faqView, R.string.support_faq_url); + setupView(donateView, R.string.support_donate_url); + + return v; + } + + public void setupView(View v, final int url) { + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), getString(url)); + } + }); + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java b/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java index ffb5ffa05..23de86d65 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/WelcomeActivity.java @@ -3,6 +3,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; @@ -14,7 +15,12 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.LinearLayout; +import de.robv.android.xposed.installer.installation.AdvancedInstallerFragment; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; @@ -22,168 +28,257 @@ import de.robv.android.xposed.installer.util.RepoLoader.RepoListener; import de.robv.android.xposed.installer.util.ThemeUtil; -public class WelcomeActivity extends XposedBaseActivity implements - NavigationView.OnNavigationItemSelectedListener, ModuleListener, RepoListener { - - private RepoLoader mRepoLoader; - - private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID"; - - private Toolbar mToolbar; - private DrawerLayout mDrawerLayout; - private NavigationView mNavigationView; - private ActionBarDrawerToggle mDrawerToggle; - private int mSelectedId; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ThemeUtil.setTheme(this); - setContentView(R.layout.activity_welcome); - - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - mToolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - - mNavigationView = (NavigationView) findViewById(R.id.navigation_view); - mNavigationView.setNavigationItemSelectedListener(this); - - mDrawerToggle = new ActionBarDrawerToggle(this, - mDrawerLayout, - mToolbar, - R.string.navigation_drawer_open, - R.string.navigation_drawer_close); - mDrawerLayout.setDrawerListener(mDrawerToggle); - mDrawerLayout.setStatusBarBackgroundColor(getResources().getColor(R.color.colorPrimaryDark)); - mDrawerToggle.syncState(); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - mSelectedId = mNavigationView.getMenu().getItem(prefs.getInt("default_view", 0)).getItemId(); - mSelectedId = savedInstanceState == null ? mSelectedId : savedInstanceState.getInt(SELECTED_ITEM_ID); - mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); - - if (savedInstanceState == null) { - navigate(mSelectedId); - } - - mRepoLoader = RepoLoader.getInstance(); - ModuleUtil.getInstance().addListener(this); - mRepoLoader.addListener(this, false); - - notifyDataSetChanged(); - } - - public void switchFragment(int itemId) { - mSelectedId = mNavigationView.getMenu().getItem(itemId).getItemId(); - mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); - navigate(mSelectedId); - } - - private void navigate(final int itemId) { - Fragment navFragment = null; - switch (itemId) { - case R.id.drawer_item_1: - setTitle(R.string.app_name); - navFragment = new InstallerFragment(); - break; - case R.id.drawer_item_2: - setTitle(R.string.nav_item_modules); - navFragment = new ModulesFragment(); - break; - case R.id.drawer_item_3: - setTitle(R.string.nav_item_download); - navFragment = new DownloadFragment(); - break; - case R.id.drawer_item_4: - setTitle(R.string.nav_item_logs); - navFragment = new LogsFragment(); - break; - case R.id.drawer_item_5: - startActivity(new Intent(this, SettingsActivity.class)); - return; - case R.id.drawer_item_6: - startActivity(new Intent(this, SupportActivity.class)); - return; - case R.id.drawer_item_7: - startActivity(new Intent(this, AboutActivity.class)); - return; - } - - if (navFragment != null) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.content_frame, navFragment).commit(); - } - } - - @Override - public boolean onNavigationItemSelected(MenuItem menuItem) { - menuItem.setChecked(true); - mSelectedId = menuItem.getItemId(); - mDrawerLayout.closeDrawer(GravityCompat.START); - navigate(mSelectedId); - return true; - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(SELECTED_ITEM_ID, mSelectedId); - } - - @Override - public void onBackPressed() { - if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { - mDrawerLayout.closeDrawer(GravityCompat.START); - } else { - super.onBackPressed(); - } - } - - private void notifyDataSetChanged() { - View parentLayout = findViewById(R.id.content_frame); - String frameworkUpdateVersion = mRepoLoader.getFrameworkUpdateVersion(); - boolean moduleUpdateAvailable = mRepoLoader.hasModuleUpdates(); - - Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.content_frame); - if (currentFragment instanceof DownloadDetailsFragment) { - if (frameworkUpdateVersion != null) { - Snackbar.make(parentLayout, - R.string.welcome_framework_update_available + " " + - String.valueOf(frameworkUpdateVersion), - Snackbar.LENGTH_LONG).show(); - } - } - - if (moduleUpdateAvailable) { - Snackbar.make(parentLayout, R.string.modules_updates_available, Snackbar.LENGTH_LONG) - .setAction("VIEW", new View.OnClickListener() { - @Override - public void onClick(View view) { - switchFragment(2); - } - }).show(); - } - } - - @Override - public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { - notifyDataSetChanged(); - } - - @Override - public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { - notifyDataSetChanged(); - } - - @Override - public void onRepoReloaded(RepoLoader loader) { - notifyDataSetChanged(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - ModuleUtil.getInstance().removeListener(this); - mRepoLoader.removeListener(this); - } -} +import static de.robv.android.xposed.installer.XposedApp.darkenColor; + +public class WelcomeActivity extends XposedBaseActivity + implements NavigationView.OnNavigationItemSelectedListener, + ModuleListener, RepoListener { + + private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID"; + private final Handler mDrawerHandler = new Handler(); + private RepoLoader mRepoLoader; + private DrawerLayout mDrawerLayout; + private int mPrevSelectedId; + private NavigationView mNavigationView; + private int mSelectedId; + private Toolbar mToolbar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeUtil.setTheme(this); + setContentView(R.layout.activity_welcome); + + mDrawerLayout = findViewById(R.id.drawer_layout); + mToolbar = findViewById(R.id.toolbar); + setSupportActionBar(mToolbar); + + mNavigationView = findViewById(R.id.navigation_view); + assert mNavigationView != null; + mNavigationView.setNavigationItemSelectedListener(this); + + ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(this, + mDrawerLayout, mToolbar, R.string.navigation_drawer_open, + R.string.navigation_drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed state + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, 0); // this disables the animation + } + }; + mDrawerLayout.setDrawerListener(mDrawerToggle); + mDrawerToggle.syncState(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mSelectedId = mNavigationView.getMenu().getItem(prefs.getInt("default_view", 0)).getItemId(); + mSelectedId = savedInstanceState == null ? mSelectedId : savedInstanceState.getInt(SELECTED_ITEM_ID); + mPrevSelectedId = mSelectedId; + mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); + + if (savedInstanceState == null) { + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + + boolean openDrawer = prefs.getBoolean("open_drawer", false); + + if (openDrawer) + mDrawerLayout.openDrawer(GravityCompat.START); + else + mDrawerLayout.closeDrawers(); + } + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + int value = extras.getInt("fragment", prefs.getInt("default_view", 0)); + switchFragment(value); + } + + mRepoLoader = RepoLoader.getInstance(); + ModuleUtil.getInstance().addListener(this); + mRepoLoader.addListener(this, false); + + notifyDataSetChanged(); + + } + + @Override + protected void onResume() { + super.onResume(); + + mDrawerLayout.setStatusBarBackgroundColor(darkenColor(XposedApp.getColor(this), 0.85f)); + + } + + public void switchFragment(int itemId) { + mSelectedId = mNavigationView.getMenu().getItem(itemId).getItemId(); + mNavigationView.getMenu().findItem(mSelectedId).setChecked(true); + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + mDrawerLayout.closeDrawers(); + } + + private void navigate(final int itemId) { + final View elevation = findViewById(R.id.elevation); + Fragment navFragment = null; + switch (itemId) { + case R.id.drawer_item_1: + mPrevSelectedId = itemId; + setTitle(R.string.app_name); + navFragment = new AdvancedInstallerFragment(); + break; + case R.id.drawer_item_2: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_modules); + navFragment = new ModulesFragment(); + break; + case R.id.drawer_item_3: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_download); + navFragment = new DownloadFragment(); + break; + case R.id.drawer_item_4: + mPrevSelectedId = itemId; + setTitle(R.string.nav_item_logs); + navFragment = new LogsFragment(); + break; + case R.id.drawer_item_5: + startActivity(new Intent(this, SettingsActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + case R.id.drawer_item_6: + startActivity(new Intent(this, SupportActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + case R.id.drawer_item_7: + startActivity(new Intent(this, AboutActivity.class)); + mNavigationView.getMenu().findItem(mPrevSelectedId).setChecked(true); + return; + } + + final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(4)); + + if (navFragment != null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out); + try { + transaction.replace(R.id.content_frame, navFragment).commit(); + + if (elevation != null) { + params.topMargin = navFragment instanceof AdvancedInstallerFragment ? dp(48) : 0; + + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + elevation.setLayoutParams(params); + } + }; + a.setDuration(150); + elevation.startAnimation(a); + } + } catch (IllegalStateException ignored) { + } + } + } + + public int dp(float value) { + float density = getApplicationContext().getResources().getDisplayMetrics().density; + + if (value == 0) { + return 0; + } + return (int) Math.ceil(density * value); + } + + @Override + public boolean onNavigationItemSelected(MenuItem menuItem) { + menuItem.setChecked(true); + mSelectedId = menuItem.getItemId(); + mDrawerHandler.removeCallbacksAndMessages(null); + mDrawerHandler.postDelayed(new Runnable() { + @Override + public void run() { + navigate(mSelectedId); + } + }, 250); + mDrawerLayout.closeDrawers(); + return true; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(SELECTED_ITEM_ID, mSelectedId); + } + + @Override + public void onBackPressed() { + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { + mDrawerLayout.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + private void notifyDataSetChanged() { + View parentLayout = findViewById(R.id.content_frame); + String frameworkUpdateVersion = mRepoLoader.getFrameworkUpdateVersion(); + boolean moduleUpdateAvailable = mRepoLoader.hasModuleUpdates(); + + Fragment currentFragment = getSupportFragmentManager() + .findFragmentById(R.id.content_frame); + if (currentFragment instanceof DownloadDetailsFragment) { + if (frameworkUpdateVersion != null) { + Snackbar.make(parentLayout, R.string.welcome_framework_update_available + " " + String.valueOf(frameworkUpdateVersion), Snackbar.LENGTH_LONG).show(); + } + } + + boolean snackBar = getSharedPreferences( + getPackageName() + "_preferences", MODE_PRIVATE).getBoolean("snack_bar", true); + + if (moduleUpdateAvailable && snackBar) { + Snackbar.make(parentLayout, R.string.modules_updates_available, Snackbar.LENGTH_LONG).setAction(getString(R.string.view), new View.OnClickListener() { + @Override + public void onClick(View view) { + switchFragment(2); + } + }).show(); + } + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + notifyDataSetChanged(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { + notifyDataSetChanged(); + } + + @Override + public void onRepoReloaded(RepoLoader loader) { + notifyDataSetChanged(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + ModuleUtil.getInstance().removeListener(this); + mRepoLoader.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java index a9eb445ae..56a42d822 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java +++ b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java @@ -1,210 +1,402 @@ package de.robv.android.xposed.installer; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.ActivityManager; import android.app.Application; import android.app.Application.ActivityLifecycleCallbacks; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; import android.preference.PreferenceManager; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.ActionBar; +import android.util.DisplayMetrics; import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.robv.android.xposed.installer.receivers.PackageChangeReceiver; import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.NotificationUtil; import de.robv.android.xposed.installer.util.RepoLoader; public class XposedApp extends Application implements ActivityLifecycleCallbacks { - public static final String TAG = "XposedInstaller"; - - @SuppressLint("SdCardPath") - public static final String BASE_DIR = "/data/data/de.robv.android.xposed.installer/"; - private static final File XPOSED_PROP_FILE = new File("/system/xposed.prop"); - - private static XposedApp mInstance = null; - private static Thread mUiThread; - private static Handler mMainHandler; - - private boolean mIsUiLoaded = false; - private Activity mCurrentActivity = null; - private SharedPreferences mPref; - private Map mXposedProp; - - public void onCreate() { - super.onCreate(); - mInstance = this; - mUiThread = Thread.currentThread(); - mMainHandler = new Handler(); - - mPref = PreferenceManager.getDefaultSharedPreferences(this); - reloadXposedProp(); - createDirectories(); - cleanup(); - NotificationUtil.init(); - AssetUtil.checkStaticBusyboxAvailability(); - AssetUtil.removeBusybox(); - - registerActivityLifecycleCallbacks(this); - } - - private void createDirectories() { - mkdirAndChmod("bin", 00771); - mkdirAndChmod("conf", 00771); - mkdirAndChmod("log", 00777); - } - - private void cleanup() { - if (!mPref.getBoolean("cleaned_up_sdcard", false)) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - File sdcard = Environment.getExternalStorageDirectory(); - new File(sdcard, "Xposed-Disabler-CWM.zip").delete(); - new File(sdcard, "Xposed-Disabler-Recovery.zip").delete(); - new File(sdcard, "Xposed-Installer-Recovery.zip").delete(); - mPref.edit().putBoolean("cleaned_up_sdcard", true).apply(); - } - } - - if (!mPref.getBoolean("cleaned_up_debug_log", false)) { - new File(XposedApp.BASE_DIR + "log/debug.log").delete(); - new File(XposedApp.BASE_DIR + "log/debug.log.old").delete(); - mPref.edit().putBoolean("cleaned_up_debug_log", true).apply(); - } - } - - private void mkdirAndChmod(String dir, int permissions) { - dir = BASE_DIR + dir; - new File(dir).mkdir(); - FileUtils.setPermissions(dir, permissions, -1, -1); - } - - public static XposedApp getInstance() { - return mInstance; - } - - public static void runOnUiThread(Runnable action) { - if (Thread.currentThread() != mUiThread) { - mMainHandler.post(action); - } else { - action.run(); - } - } - - // This method is hooked by XposedBridge to return the current version - public static int getActiveXposedVersion() { - return -1; - } - - private void reloadXposedProp() { - Map map = Collections.emptyMap(); - if (XPOSED_PROP_FILE.canRead()) { - FileInputStream is = null; - try { - is = new FileInputStream(XPOSED_PROP_FILE); - map = parseXposedProp(is); - } catch (IOException e) { - Log.e(XposedApp.TAG, "Could not read " + XPOSED_PROP_FILE.getPath(), e); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ignored) {} - } - } - } - - synchronized (this) { - mXposedProp = map; - } - } - - private Map parseXposedProp(InputStream stream) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); - Map map = new LinkedHashMap(); - String line; - while ((line = reader.readLine()) != null) { - String[] parts = line.split("=", 2); - if (parts.length != 2) - continue; - - String key = parts[0].trim(); - if (key.charAt(0) == '#') - continue; - - map.put(key, parts[1].trim()); - } - return Collections.unmodifiableMap(map); - } - - public static Map getXposedProp() { - synchronized (mInstance) { - return mInstance.mXposedProp; - } - } - - public boolean areDownloadsEnabled() { - if (!mPref.getBoolean("enable_downloads", true)) - return false; - - if (checkCallingOrSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) - return false; - - return true; - } - - public static SharedPreferences getPreferences() { - return mInstance.mPref; - } - - public void updateProgressIndicator() { - final boolean isLoading = RepoLoader.getInstance().isLoading() || ModuleUtil.getInstance().isLoading(); - runOnUiThread(new Runnable() { - @Override - public void run() { - synchronized (XposedApp.this) { - if (mCurrentActivity != null) - mCurrentActivity.setProgressBarIndeterminateVisibility(isLoading); - } - } - }); - } - - @Override - public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { - if (mIsUiLoaded) - return; - - RepoLoader.getInstance().triggerFirstLoadIfNecessary(); - mIsUiLoaded = true; - } - - @Override - public synchronized void onActivityResumed(Activity activity) { - mCurrentActivity = activity; - updateProgressIndicator(); - } - - @Override - public synchronized void onActivityPaused(Activity activity) { - activity.setProgressBarIndeterminateVisibility(false); - mCurrentActivity = null; - } - - @Override public void onActivityStarted(Activity activity) {} - @Override public void onActivityStopped(Activity activity) {} - @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} - @Override public void onActivityDestroyed(Activity activity) {} + public static final String TAG = "XposedInstaller"; + + @SuppressLint("SdCardPath") + private static final String BASE_DIR_LEGACY = "/data/data/de.robv.android.xposed.installer/"; + public static final String BASE_DIR = Build.VERSION.SDK_INT >= 24 + ? "/data/user_de/0/de.robv.android.xposed.installer/" : BASE_DIR_LEGACY; + public static final String ENABLED_MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/enabled_modules.list"; + private static final File XPOSED_PROP_FILE_SYSTEMLESS_1 = new File("/magisk/xposed/system/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_2 = new File("/su/xposed/system/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_3 = new File("/vendor/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_4 = new File("/xposed/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_5 = new File("/magisk/PurifyXposed/system/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_6 = new File("/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_7 = new File("/sbin/xposed.prop"); + private static final File XPOSED_PROP_FILE_SYSTEMLESS_OFFICIAL = new File("/su/xposed/xposed.prop"); + private static final File XPOSED_PROP_FILE = new File("/system/xposed.prop"); + public static int WRITE_EXTERNAL_PERMISSION = 69; + public static int[] iconsValues = new int[]{R.mipmap.ic_launcher, R.mipmap.ic_launcher_hjmodi, R.mipmap.ic_launcher_rovo, R.mipmap.ic_launcher_rovo_old, R.mipmap.ic_launcher_staol}; + private static Pattern PATTERN_APP_PROCESS_VERSION = Pattern.compile(".*with Xposed support \\(version (.+)\\).*"); + private static XposedApp mInstance = null; + private static Thread mUiThread; + private static Handler mMainHandler; + private boolean mIsUiLoaded = false; + private SharedPreferences mPref; + private InstallZipUtil.XposedProp mXposedProp; + private Activity mCurrentActivity = null; + + public static XposedApp getInstance() { + return mInstance; + } + + public static void runOnUiThread(Runnable action) { + if (Thread.currentThread() != mUiThread) { + mMainHandler.post(action); + } else { + action.run(); + } + } + + public static File createFolder() { + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/XposedInstaller/"); + + if (!dir.exists()) dir.mkdir(); + + return dir; + } + + public static void postOnUiThread(Runnable action) { + mMainHandler.post(action); + } + + public static Integer getXposedVersion() { + if (Build.VERSION.SDK_INT >= 21) { + return getActiveXposedVersion(); + } else { + return getInstalledAppProcessVersion(); + } + } + + private static int getInstalledAppProcessVersion() { + try { + return getAppProcessVersion(new FileInputStream("/system/bin/app_process")); + } catch (IOException e) { + return 0; + } + } + + private static int getAppProcessVersion(InputStream is) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = br.readLine()) != null) { + if (!line.contains("Xposed")) + continue; + + Matcher m = PATTERN_APP_PROCESS_VERSION.matcher(line); + if (m.find()) { + is.close(); + return ModuleUtil.extractIntPart(m.group(1)); + } + } + is.close(); + return 0; + } + + // This method is hooked by XposedBridge to return the current version + public static Integer getActiveXposedVersion() { + return -1; + } + + public static InstallZipUtil.XposedProp getXposedProp() { + synchronized (mInstance) { + return mInstance.mXposedProp; + } + } + + public static SharedPreferences getPreferences() { + return mInstance.mPref; + } + + public static int getColor(Context context) { + SharedPreferences prefs = context.getSharedPreferences(context.getPackageName() + "_preferences", MODE_PRIVATE); + int defaultColor = context.getResources().getColor(R.color.colorPrimary); + + return prefs.getInt("colors", defaultColor); + } + + public static void setColors(ActionBar actionBar, Object value, Activity activity) { + int color = (int) value; + SharedPreferences prefs = activity.getSharedPreferences(activity.getPackageName() + "_preferences", MODE_PRIVATE); + + int drawable = iconsValues[Integer.parseInt(prefs.getString("custom_icon", "0"))]; + + if (actionBar != null) + actionBar.setBackgroundDrawable(new ColorDrawable(color)); + + if (Build.VERSION.SDK_INT >= 21) { + + ActivityManager.TaskDescription tDesc = new ActivityManager.TaskDescription(activity.getString(R.string.app_name), + drawableToBitmap(activity.getDrawable(drawable)), color); + activity.setTaskDescription(tDesc); + + if (getPreferences().getBoolean("nav_bar", false)) { + activity.getWindow().setNavigationBarColor(darkenColor(color, 0.85f)); + } else { + int black = activity.getResources().getColor(android.R.color.black); + activity.getWindow().setNavigationBarColor(black); + } + } + } + + public static Bitmap drawableToBitmap(Drawable drawable) { + Bitmap bitmap; + + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** + * @author PeterCxy https://github.com/PeterCxy/Lolistat/blob/aide/app/src/ + * main/java/info/papdt/lolistat/support/Utility.java + */ + public static int darkenColor(int color, float factor) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= factor; + return Color.HSVToColor(hsv); + } + + public static String getDownloadPath() { + return getPreferences().getString("download_location", Environment.getExternalStorageDirectory() + "/XposedInstaller"); + } + + public void onCreate() { + super.onCreate(); + mInstance = this; + mUiThread = Thread.currentThread(); + mMainHandler = new Handler(); + + mPref = PreferenceManager.getDefaultSharedPreferences(this); + reloadXposedProp(); + createDirectories(); + delete(new File(Environment.getExternalStorageDirectory() + "/XposedInstaller/.temp")); + NotificationUtil.init(); + AssetUtil.removeBusybox(); + registerReceivers(); + + registerActivityLifecycleCallbacks(this); + + @SuppressLint("SimpleDateFormat") DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy"); + Date date = new Date(); + + if (!mPref.getString("date", "").equals(dateFormat.format(date))) { + mPref.edit().putString("date", dateFormat.format(date)).apply(); + + try { + Log.i(TAG, String.format("XposedInstaller - %s - %s", BuildConfig.APP_VERSION, getPackageManager().getPackageInfo(getPackageName(), 0).versionName)); + } catch (PackageManager.NameNotFoundException ignored) { + } + } + + if (mPref.getBoolean("force_english", false)) { + Resources res = getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + android.content.res.Configuration conf = res.getConfiguration(); + conf.locale = Locale.ENGLISH; + res.updateConfiguration(conf, dm); + } + } + + private void registerReceivers() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + registerReceiver(new PackageChangeReceiver(), filter); + + PendingIntent.getBroadcast(this, 0, + new Intent(this, PackageChangeReceiver.class), 0); + } + + private void delete(File file) { + if (file != null) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) for (File f : file.listFiles()) delete(f); + } + file.delete(); + } + } + + private void createDirectories() { + FileUtils.setPermissions(BASE_DIR, 00711, -1, -1); + mkdirAndChmod("conf", 00771); + mkdirAndChmod("log", 00777); + + if (Build.VERSION.SDK_INT >= 24) { + try { + Method deleteDir = FileUtils.class.getDeclaredMethod("deleteContentsAndDir", File.class); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "bin")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "conf")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "log")); + } catch (ReflectiveOperationException e) { + Log.w(XposedApp.TAG, "Failed to delete obsolete directories", e); + } + } + } + + private void mkdirAndChmod(String dir, int permissions) { + dir = BASE_DIR + dir; + new File(dir).mkdir(); + FileUtils.setPermissions(getFilesDir().getParent(), 00751, -1, -1); + FileUtils.setPermissions(dir, permissions, -1, -1); + } + + private void reloadXposedProp() { + InstallZipUtil.XposedProp prop = null; + File file = null; + + if (XPOSED_PROP_FILE.canRead()) { + file = XPOSED_PROP_FILE; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_OFFICIAL.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_OFFICIAL; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_1.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_1; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_2.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_2; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_3.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_3; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_4.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_4; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_5.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_5; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_6.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_6; + } else if (XPOSED_PROP_FILE_SYSTEMLESS_7.canRead()) { + file = XPOSED_PROP_FILE_SYSTEMLESS_7; + } + + if (file != null) { + FileInputStream is = null; + try { + is = new FileInputStream(file); + prop = InstallZipUtil.parseXposedProp(is); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not read " + file.getPath(), e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignored) { + } + } + } + } + + synchronized (this) { + mXposedProp = prop; + } + } + + public void updateProgressIndicator(final SwipeRefreshLayout refreshLayout) { + final boolean isLoading = RepoLoader.getInstance().isLoading() || ModuleUtil.getInstance().isLoading(); + runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (XposedApp.this) { + if (mCurrentActivity != null) { + mCurrentActivity.setProgressBarIndeterminateVisibility(isLoading); + if (refreshLayout != null) + refreshLayout.setRefreshing(isLoading); + } + } + } + }); + } + + @Override + public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (mIsUiLoaded) + return; + + RepoLoader.getInstance().triggerFirstLoadIfNecessary(); + mIsUiLoaded = true; + } + + @Override + public synchronized void onActivityResumed(Activity activity) { + mCurrentActivity = activity; + updateProgressIndicator(null); + } + + @Override + public synchronized void onActivityPaused(Activity activity) { + activity.setProgressBarIndeterminateVisibility(false); + mCurrentActivity = null; + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java b/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java index 01385f74f..4bb18dbc6 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java +++ b/app/src/main/java/de/robv/android/xposed/installer/XposedBaseActivity.java @@ -1,32 +1,44 @@ package de.robv.android.xposed.installer; import android.os.Bundle; +import android.support.annotation.StringRes; import android.support.v7.app.AppCompatActivity; +import android.view.WindowManager; -import de.robv.android.xposed.installer.util.NavUtil; import de.robv.android.xposed.installer.util.ThemeUtil; public abstract class XposedBaseActivity extends AppCompatActivity { - public boolean leftActivityWithSlideAnim = false; - public int mTheme = -1; + public int mTheme = -1; - @Override - protected void onCreate(Bundle savedInstanceBundle) { - super.onCreate(savedInstanceBundle); - ThemeUtil.setTheme(this); - } + @Override + protected void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + ThemeUtil.setTheme(this); + } - @Override - protected void onResume() { - super.onResume(); - ThemeUtil.reloadTheme(this); + @Override + protected void onResume() { + super.onResume(); + XposedApp.setColors(getSupportActionBar(), XposedApp.getColor(this), this); + ThemeUtil.reloadTheme(this); + } - if (leftActivityWithSlideAnim) - NavUtil.setTransitionSlideLeave(this); - leftActivityWithSlideAnim = false; - } + public void setFloating(android.support.v7.widget.Toolbar toolbar, @StringRes int details) { + 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); - public void setLeftWithSlideAnim(boolean newValue) { - this.leftActivityWithSlideAnim = newValue; - } -} + if (details != 0) { + toolbar.setTitle(details); + } + toolbar.setNavigationIcon(R.drawable.ic_close); + setFinishOnTouchOutside(true); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/AdvancedInstallerFragment.java b/app/src/main/java/de/robv/android/xposed/installer/installation/AdvancedInstallerFragment.java new file mode 100644 index 000000000..e08da01f5 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/AdvancedInstallerFragment.java @@ -0,0 +1,330 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.TextUtils; +import android.util.Log; +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.CheckBox; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.annimon.stream.Stream; +import com.annimon.stream.function.Predicate; +import com.google.gson.Gson; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.installer.BuildConfig; +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.RootUtil; +import de.robv.android.xposed.installer.util.json.JSONUtils; +import de.robv.android.xposed.installer.util.json.XposedTab; + +import static android.content.Context.MODE_PRIVATE; + +public class AdvancedInstallerFragment extends Fragment { + + private static ViewPager mPager; + private TabLayout mTabLayout; + private RootUtil mRootUtil = new RootUtil(); + private TabsAdapter tabsAdapter; + + public static void gotoPage(int page) {mPager.setCurrentItem(page);} + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.tab_advanced_installer, container, false); + mPager = view.findViewById(R.id.pager); + mTabLayout = view.findViewById(R.id.tab_layout); + + tabsAdapter = new TabsAdapter(getChildFragmentManager()); + tabsAdapter.notifyDataSetChanged(); + mPager.setAdapter(tabsAdapter); + mTabLayout.setupWithViewPager(mPager); + + setHasOptionsMenu(true); + new JSONParser().execute(); + + if (!XposedApp.getPreferences().getBoolean("hide_install_warning", false)) { + final View dontShowAgainView = inflater.inflate(R.layout.dialog_install_warning, null); + + new MaterialDialog.Builder(getActivity()) + .title(R.string.install_warning_title) + .customView(dontShowAgainView, false) + .positiveText(android.R.string.ok) + .callback(new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox); + if (checkBox.isChecked()) + XposedApp.getPreferences().edit().putBoolean("hide_install_warning", true).apply(); + } + }).cancelable(false).show(); + } + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + mTabLayout.setBackgroundColor(XposedApp.getColor(getContext())); + + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_installer, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.reboot: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + reboot(null); + } + }); + } else { + reboot(null); + } + break; + case R.id.soft_reboot: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.soft_reboot, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + softReboot(); + } + }); + } else { + softReboot(); + } + break; + case R.id.reboot_recovery: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot_recovery, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + reboot("recovery"); + } + }); + } else { + reboot("recovery"); + } + break; + } + + return super.onOptionsItemSelected(item); + } + + private boolean startShell() { + if (mRootUtil.startShell()) + return true; + + showAlert(getString(R.string.root_failed)); + return false; + } + + private void areYouSure(int contentTextId, MaterialDialog.ButtonCallback yesHandler) { + new MaterialDialog.Builder(getActivity()).title(R.string.areyousure) + .content(contentTextId) + .iconAttr(android.R.attr.alertDialogIcon) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no).callback(yesHandler).show(); + } + + private void showAlert(final String result) { + if (Looper.myLooper() != Looper.getMainLooper()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + showAlert(result); + } + }); + return; + } + + MaterialDialog dialog = new MaterialDialog.Builder(getActivity()).content(result).positiveText(android.R.string.ok).build(); + dialog.show(); + + TextView txtMessage = (TextView) dialog + .findViewById(android.R.id.message); + try { + txtMessage.setTextSize(14); + } catch (NullPointerException ignored) { + } + } + + private void softReboot() { + if (!startShell()) + return; + + List messages = new LinkedList<>(); + if (mRootUtil.execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.reboot_failed)); + showAlert(TextUtils.join("\n", messages).trim()); + } + } + + private void reboot(String mode) { + if (!startShell()) + return; + + List messages = new LinkedList<>(); + + String command = "reboot"; + if (mode != null) { + command += " " + mode; + if (mode.equals("recovery")) + // create a flag used by some kernels to boot into recovery + mRootUtil.executeWithBusybox("touch /cache/recovery/boot", messages); + } + + if (mRootUtil.executeWithBusybox(command, messages) != 0) { + messages.add(""); + messages.add(getString(R.string.reboot_failed)); + showAlert(TextUtils.join("\n", messages).trim()); + } + AssetUtil.removeBusybox(); + } + + private class JSONParser extends AsyncTask { + + private String newApkVersion = null; + private String newApkLink = null; + private String newApkChangelog = null; + private boolean noZips = false; + + @Override + protected Boolean doInBackground(Void... params) { + try { + String originalJson = JSONUtils.getFileContent(JSONUtils.JSON_LINK); + String newJson = JSONUtils.listZip(); + + String jsonString = originalJson.replace("%XPOSED_ZIP%", newJson); + + final JSONUtils.XposedJson xposedJson = new Gson().fromJson(jsonString, JSONUtils.XposedJson.class); + + List tabs = Stream.of(xposedJson.tabs) + .filter(new Predicate() { + @Override + public boolean test(XposedTab value) { + return value.sdks.contains(Build.VERSION.SDK_INT); + } + }).toList(); + + noZips = tabs.isEmpty(); + + for (XposedTab tab : tabs) { + tabsAdapter.addFragment(tab.name, BaseAdvancedInstaller.newInstance(tab)); + } + + newApkVersion = xposedJson.apk.version; + newApkLink = xposedJson.apk.link; + newApkChangelog = xposedJson.apk.changelog; + + return true; + } catch (Exception e) { + e.printStackTrace(); + Log.e(XposedApp.TAG, "AdvancedInstallerFragment -> " + e.getMessage()); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + + try { + tabsAdapter.notifyDataSetChanged(); + + if (!result) { + StatusInstallerFragment.setError(true/* connection failed */, true /* so no sdks available*/); + } else { + StatusInstallerFragment.setError(false /*connection ok*/, noZips /*if counter is 0 there aren't sdks*/); + } + + if (newApkVersion == null) return; + + SharedPreferences prefs; + try { + prefs = getContext().getSharedPreferences(getContext().getPackageName() + "_preferences", MODE_PRIVATE); + + prefs.edit().putString("changelog_" + newApkVersion, newApkChangelog).apply(); + } catch (NullPointerException ignored) { + } + + BigInteger a = new BigInteger(BuildConfig.APP_VERSION); + BigInteger b = new BigInteger(newApkVersion); + + if (a.compareTo(b) == -1) { + StatusInstallerFragment.setUpdate(newApkLink, newApkChangelog); + } + + } catch (Exception ignored) { + } + + } + } + + private class TabsAdapter extends FragmentPagerAdapter { + + private final ArrayList titles = new ArrayList<>(); + private final ArrayList listFragment = new ArrayList<>(); + + TabsAdapter(FragmentManager mgr) { + super(mgr); + addFragment(getString(R.string.status), new StatusInstallerFragment()); + } + + void addFragment(String title, Fragment fragment) { + titles.add(title); + listFragment.add(fragment); + } + + @Override + public int getCount() { return listFragment.size(); } + + @Override + public Fragment getItem(int position) { + return listFragment.get(position); + } + + @Override + public String getPageTitle(int position) { + return titles.get(position); + } + } + +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/BaseAdvancedInstaller.java b/app/src/main/java/de/robv/android/xposed/installer/installation/BaseAdvancedInstaller.java new file mode 100644 index 000000000..d41e37f43 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/BaseAdvancedInstaller.java @@ -0,0 +1,639 @@ +package de.robv.android.xposed.installer.installation; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.RootUtil; +import de.robv.android.xposed.installer.util.json.XposedTab; +import de.robv.android.xposed.installer.util.json.XposedZip; + +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; +import static de.robv.android.xposed.installer.XposedApp.runOnUiThread; + +public class BaseAdvancedInstaller extends Fragment implements DownloadsUtil.DownloadFinishedCallback { + + public static final String JAR_PATH = XposedApp.BASE_DIR + "bin/XposedBridge.jar"; + private static final int INSTALL_MODE_NORMAL = 0; + private static final int INSTALL_MODE_RECOVERY_AUTO = 1; + private static final int INSTALL_MODE_RECOVERY_MANUAL = 2; + private static final String BINARIES_FOLDER = AssetUtil.getBinariesFolder(); + public static String APP_PROCESS_NAME = null; + private static RootUtil mRootUtil = new RootUtil(); + private List messages = new ArrayList<>(); + private View mClickedButton; + + public static BaseAdvancedInstaller newInstance(XposedTab tab) { + BaseAdvancedInstaller myFragment = new BaseAdvancedInstaller(); + + Bundle args = new Bundle(); + args.putParcelable("tab", tab); + myFragment.setArguments(args); + + return myFragment; + } + + protected List installers() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.getInstallers(); + } + + protected List uninstallers() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.uninstallers; + } + + protected String compatibility() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.getCompatibility(); + } + + protected String incompatibility() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.getIncompatibility(); + } + + protected String author() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.author; + } + + protected String xdaUrl() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.getSupport(); + } + + protected boolean isStable() { + XposedTab tab = getArguments().getParcelable("tab"); + + assert tab != null; + return tab.stable; + } + + private boolean checkPermissions() { + if (Build.VERSION.SDK_INT < 23) return false; + + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mRootUtil.dispose(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.single_installer_view, container, false); + + final Spinner chooserInstallers = view.findViewById(R.id.chooserInstallers); + final Spinner chooserUninstallers = view.findViewById(R.id.chooserUninstallers); + final Button btnInstall = view.findViewById(R.id.btnInstall); + final Button btnUninstall = view.findViewById(R.id.btnUninstall); + ImageView infoInstaller = view.findViewById(R.id.infoInstaller); + ImageView infoUninstaller = view.findViewById(R.id.infoUninstaller); + TextView compatibleTv = view.findViewById(R.id.compatibilityTv); + TextView incompatibleTv = view.findViewById(R.id.incompatibilityTv); + TextView author = view.findViewById(R.id.author); + View showOnXda = view.findViewById(R.id.show_on_xda); + + try { + chooserInstallers.setAdapter(new XposedZip.MyAdapter(getContext(), installers())); + chooserUninstallers.setAdapter(new XposedZip.MyAdapter(getContext(), uninstallers())); + } catch (Exception ignored) {} + + if (Build.VERSION.SDK_INT >= 21 && installers().size() >= 3 && uninstallers().size() >= 4) { + if (StatusInstallerFragment.ARCH.contains("86")) { + chooserInstallers.setSelection(2); + chooserUninstallers.setSelection(3); + } else if (StatusInstallerFragment.ARCH.contains("64")) { + chooserInstallers.setSelection(1); + chooserUninstallers.setSelection(1); + } + } + + infoInstaller.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem(); + String s = getString(R.string.infoInstaller, + selectedInstaller.name, Build.VERSION.SDK_INT, + selectedInstaller.architecture, + selectedInstaller.version); + + new MaterialDialog.Builder(getContext()).title(R.string.info) + .content(s).positiveText(android.R.string.ok).show(); + } + }); + infoUninstaller.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem(); + String s = getString(R.string.infoUninstaller, + selectedUninstaller.name, + selectedUninstaller.architecture, + selectedUninstaller.version); + + new MaterialDialog.Builder(getContext()).title(R.string.info) + .content(s).positiveText(android.R.string.ok).show(); + } + }); + + btnInstall.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mClickedButton = v; + if (checkPermissions()) return; + + areYouSure(R.string.warningArchitecture, + new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + + XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem(); + + checkAndDelete(selectedInstaller.name); + + new DownloadsUtil.Builder(getContext()) + .setTitle(selectedInstaller.name) + .setUrl(selectedInstaller.link) + .setSave(true) + .setCallback(BaseAdvancedInstaller.this) + .setMimeType(DownloadsUtil.MIME_TYPES.ZIP) + .setDialog(true) + .download(); + } + }); + } + }); + + btnUninstall.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mClickedButton = v; + if (checkPermissions()) return; + + areYouSure(R.string.warningArchitecture, + new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + + XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem(); + + checkAndDelete(selectedUninstaller.name); + + new DownloadsUtil.Builder(getContext()) + .setTitle(selectedUninstaller.name) + .setUrl(selectedUninstaller.link) + .setSave(true) + .setCallback(BaseAdvancedInstaller.this) + .setMimeType(DownloadsUtil.MIME_TYPES.ZIP) + .setDialog(true) + .download(); + } + }); + } + }); + + compatibleTv.setText(compatibility()); + incompatibleTv.setText(incompatibility()); + author.setText(getString(R.string.download_author, author())); + + try { + if (uninstallers().size() == 0) { + infoUninstaller.setVisibility(View.GONE); + chooserUninstallers.setVisibility(View.GONE); + btnUninstall.setVisibility(View.GONE); + } + } catch (Exception ignored) {} + + if (!isStable()) { + view.findViewById(R.id.warning_unstable).setVisibility(View.VISIBLE); + } + + showOnXda.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), xdaUrl()); + } + }); + + if (Build.VERSION.SDK_INT == 15) { + APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk15"; + } else if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT <= 18) { + APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk16"; + } else if (Build.VERSION.SDK_INT == 19) { + APP_PROCESS_NAME = BINARIES_FOLDER + "app_process_xposed_sdk19"; + } + + return view; + } + + private void checkAndDelete(String name) { + new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/XposedInstaller/" + name + ".zip").delete(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedButton != null) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + mClickedButton.performClick(); + } + }, 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onDownloadFinished(final Context context, final DownloadsUtil.DownloadInfo info) { + messages.clear(); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, getString(R.string.downloadZipOk, info.localFilename), Toast.LENGTH_LONG).show(); + } + }); + + if (getInstallMode() == INSTALL_MODE_RECOVERY_MANUAL) + return; + + if (getInstallMode() == INSTALL_MODE_NORMAL) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + runOnUiThread(new Runnable() { + @Override + public void run() { + areYouSure(R.string.install_warning, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + + if (!startShell()) return; + + if (info.localFilename.contains("Disabler")) { + prepareUninstall(messages); + } else { + prepareInstall(messages); + } + offerReboot(messages); + } + }); + } + }); + return; + } else if (InstallZipUtil.checkZip(InstallZipUtil.getZip(info.localFilename)).isFlashableInApp()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + areYouSure(R.string.install_warning, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + + if (!startShell()) return; + + Intent install = new Intent(getContext(), InstallationActivity.class); + install.putExtra(Flashable.KEY, new FlashDirectly(info.localFilename, false)); + startActivity(install); + } + }); + } + }); + return; + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.not_flashable_inapp, Toast.LENGTH_LONG).show(); + } + }); + } + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + areYouSure(R.string.install_warning, new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + + if (!startShell()) return; + + prepareAutoFlash(messages, new File(info.localFilename)); + offerRebootToRecovery(messages, info.title, INSTALL_MODE_RECOVERY_AUTO); + } + }); + } + }); + } + + private void areYouSure(int contentTextId, MaterialDialog.ButtonCallback yesHandler) { + new MaterialDialog.Builder(getActivity()).title(R.string.areyousure) + .content(contentTextId) + .iconAttr(android.R.attr.alertDialogIcon) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no).callback(yesHandler).show(); + } + + private boolean startShell() { + if (mRootUtil.startShell()) + return true; + + showAlert(getString(R.string.root_failed)); + return false; + } + + private void showAlert(final String result) { + if (Looper.myLooper() != Looper.getMainLooper()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + showAlert(result); + } + }); + return; + } + + MaterialDialog dialog = new MaterialDialog.Builder(getActivity()).content(result).positiveText(android.R.string.ok).build(); + dialog.show(); + + TextView txtMessage = (TextView) dialog + .findViewById(android.R.id.message); + try { + txtMessage.setTextSize(14); + } catch (NullPointerException ignored) { + } + } + + private int getInstallMode() { + return XposedApp.getPreferences().getInt("install_mode", INSTALL_MODE_NORMAL); + } + + private void showConfirmDialog(final String message, final MaterialDialog.ButtonCallback callback) { + if (Looper.myLooper() != Looper.getMainLooper()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + showConfirmDialog(message, callback); + } + }); + return; + } + + new MaterialDialog.Builder(getActivity()) + .content(message).positiveText(android.R.string.yes) + .negativeText(android.R.string.no).callback(callback).show(); + } + + private boolean prepareInstall(List messages) { + File appProcessFile = AssetUtil.writeAssetToFile(APP_PROCESS_NAME, new File(XposedApp.BASE_DIR + "bin/app_process"), 00700); + if (appProcessFile == null) { + showAlert(getString(R.string.file_extract_failed, "app_process")); + return false; + } + + messages.add(getString(R.string.file_copying, "XposedBridge.jar")); + File jarFile = AssetUtil.writeAssetToFile("XposedBridge.jar", new File(JAR_PATH), 00644); + if (jarFile == null) { + messages.add(""); + messages.add(getString(R.string.file_extract_failed, "XposedBridge.jar")); + return false; + } + + mRootUtil.executeWithBusybox("sync", messages); + + messages.add(getString(R.string.file_mounting_writable, "/system")); + if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) { + messages.add(getString(R.string.file_mount_writable_failed, "/system")); + messages.add(getString(R.string.file_trying_to_continue)); + } + + if (new File("/system/bin/app_process.orig").exists()) { + messages.add(getString(R.string.file_backup_already_exists, "/system/bin/app_process.orig")); + } else { + if (mRootUtil.executeWithBusybox("cp -a /system/bin/app_process /system/bin/app_process.orig", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_backup_failed, "/system/bin/app_process")); + return false; + } else { + messages.add(getString(R.string.file_backup_successful, "/system/bin/app_process.orig")); + } + + mRootUtil.executeWithBusybox("sync", messages); + } + + messages.add(getString(R.string.file_copying, "app_process")); + if (mRootUtil.executeWithBusybox("cp -a " + appProcessFile.getAbsolutePath() + " /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_copy_failed, "app_process", "/system/bin")); + return false; + } + if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process")); + return false; + } + if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process")); + return false; + } + + return true; + } + + private boolean prepareUninstall(List messages) { + new File(JAR_PATH).delete(); + new File(XposedApp.BASE_DIR + "bin/app_process").delete(); + + if (!startShell()) + return false; + + + messages.add(getString(R.string.file_mounting_writable, "/system")); + if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) { + messages.add(getString(R.string.file_mount_writable_failed, "/system")); + messages.add(getString(R.string.file_trying_to_continue)); + } + + messages.add(getString(R.string.file_backup_restoring, "/system/bin/app_process.orig")); + if (!new File("/system/bin/app_process.orig").exists()) { + messages.add(""); + messages.add(getString(R.string.file_backup_not_found, "/system/bin/app_process.orig")); + return false; + } + + if (mRootUtil.executeWithBusybox("mv /system/bin/app_process.orig /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_move_failed, "/system/bin/app_process.orig", "/system/bin/app_process")); + return false; + } + if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process")); + return false; + } + if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) { + messages.add(""); + messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process")); + return false; + } + // Might help on some SELinux-enforced ROMs, shouldn't hurt on others + mRootUtil.execute("/system/bin/restorecon /system/bin/app_process", (RootUtil.LineCallback) null); + + return true; + } + + private boolean prepareAutoFlash(List messages, File file) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + File appProcessFile = AssetUtil.writeAssetToFile(APP_PROCESS_NAME, new File(XposedApp.BASE_DIR + "bin/app_process"), 00700); + if (appProcessFile == null) { + showAlert(getString(R.string.file_extract_failed, "app_process")); + return false; + } + + messages.add(getString(R.string.file_copying, "XposedBridge.jar")); + File jarFile = AssetUtil.writeAssetToFile("XposedBridge.jar", new File(JAR_PATH), 00644); + if (jarFile == null) { + messages.add(""); + messages.add(getString(R.string.file_extract_failed, "XposedBridge.jar")); + return false; + } + + mRootUtil.executeWithBusybox("sync", messages); + } + + Intent install = new Intent(getContext(), InstallationActivity.class); + install.putExtra(Flashable.KEY, new FlashRecoveryAuto(file.getAbsoluteFile())); + startActivity(install); + + return true; + } + + private void offerReboot(List messages) { + messages.add(getString(R.string.file_done)); + messages.add(""); + messages.add(getString(R.string.reboot_confirmation)); + showConfirmDialog(TextUtils.join("\n", messages).trim(), + new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + reboot(null); + } + }); + } + + private void offerRebootToRecovery(List messages, final String file, final int installMode) { + if (installMode == INSTALL_MODE_RECOVERY_AUTO) + messages.add(getString(R.string.auto_flash_note, file)); + else + messages.add(getString(R.string.manual_flash_note, file)); + + messages.add(""); + messages.add(getString(R.string.reboot_recovery_confirmation)); + showConfirmDialog(TextUtils.join("\n", messages).trim(), + new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + reboot("recovery"); + } + + @Override + public void onNegative(MaterialDialog dialog) { + super.onNegative(dialog); + if (installMode == INSTALL_MODE_RECOVERY_AUTO) { + // clean up to avoid unwanted flashing + mRootUtil.executeWithBusybox("rm /cache/recovery/command"); + mRootUtil.executeWithBusybox("rm /cache/recovery/" + file); + AssetUtil.removeBusybox(); + } + } + } + + ); + } + + private void reboot(String mode) { + if (!startShell()) + return; + + List messages = new LinkedList<>(); + + String command = "reboot"; + if (mode != null) { + command += " " + mode; + if (mode.equals("recovery")) + // create a flag used by some kernels to boot into recovery + mRootUtil.executeWithBusybox("touch /cache/recovery/boot", messages); + } + + if (mRootUtil.executeWithBusybox(command, messages) != 0) { + messages.add(""); + messages.add(getString(R.string.reboot_failed)); + showAlert(TextUtils.join("\n", messages).trim()); + } + AssetUtil.removeBusybox(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java new file mode 100644 index 000000000..d565766c7 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashCallback.java @@ -0,0 +1,23 @@ +package de.robv.android.xposed.installer.installation; + +import de.robv.android.xposed.installer.util.RootUtil; +import eu.chainfire.libsuperuser.Shell; + +public interface FlashCallback extends RootUtil.LineCallback { + void onStarted(); + void onDone(); + void onError(int exitCode, String error); + + int OK = 0; + int ERROR_GENERIC = 1; + + // SU errors + int ERROR_TIMEOUT = Shell.OnCommandResultListener.WATCHDOG_EXIT; + int ERROR_SHELL_DIED = Shell.OnCommandResultListener.SHELL_DIED; + int ERROR_NO_ROOT_ACCESS = Shell.OnCommandResultListener.SHELL_EXEC_FAILED; + + // ZIP errors + int ERROR_INVALID_ZIP = -100; + int ERROR_NOT_FLASHABLE_IN_APP = -101; + int ERROR_INSTALLER_NEEDS_UPDATE = -102; +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java new file mode 100644 index 000000000..79e3ba9de --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashDirectly.java @@ -0,0 +1,102 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.AssetUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; +import static de.robv.android.xposed.installer.util.InstallZipUtil.reportMissingFeatures; +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; +import static de.robv.android.xposed.installer.util.RootUtil.getShellPath; + +public class FlashDirectly extends Flashable { + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FlashDirectly createFromParcel(Parcel in) { + return new FlashDirectly(in); + } + + @Override + public FlashDirectly[] newArray(int size) { + return new FlashDirectly[size]; + } + }; + private final boolean mSystemless; + + public FlashDirectly(String zipPath, boolean systemless) { + super(new File(zipPath)); + mSystemless = systemless; + } + + protected FlashDirectly(Parcel in) { + super(in); + mSystemless = in.readInt() == 1; + } + + public void flash(Context context, FlashCallback callback) { + InstallZipUtil.ZipCheckResult zipCheck = openAndCheckZip(callback); + if (zipCheck == null) { + return; + } + + ZipFile zip = zipCheck.getZip(); + if (!zipCheck.isFlashableInApp()) { + triggerError(callback, FlashCallback.ERROR_NOT_FLASHABLE_IN_APP); + closeSilently(zip); + return; + } + + // Extract update-binary. + ZipEntry entry = zip.getEntry("META-INF/com/google/android/update-binary"); + File updateBinaryFile = new File(XposedApp.getInstance().getCacheDir(), "update-binary"); + try { + AssetUtil.writeStreamToFile(zip.getInputStream(entry), updateBinaryFile, 0700); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Could not extract update-binary", e); + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP); + return; + } finally { + closeSilently(zip); + } + + // Execute the flash commands. + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(callback)) { + return; + } + + callback.onStarted(); + + rootUtil.execute("export NO_UIPRINT=1", callback); + if (mSystemless) { + rootUtil.execute("export SYSTEMLESS=1", callback); + } + + int result = rootUtil.execute(getShellPath(updateBinaryFile) + " 2 1 " + getShellPath(mZipPath), callback); + if (result != 0) { + triggerError(callback, result); + return; + } + + callback.onDone(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mSystemless ? 1 : 0); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java new file mode 100644 index 000000000..d0e0ce1f2 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/FlashRecoveryAuto.java @@ -0,0 +1,95 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; +import java.util.ArrayList; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.util.InstallZipUtil.ZipCheckResult; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; + +public class FlashRecoveryAuto extends Flashable { + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FlashRecoveryAuto createFromParcel(Parcel in) { + return new FlashRecoveryAuto(in); + } + + @Override + public FlashRecoveryAuto[] newArray(int size) { + return new FlashRecoveryAuto[size]; + } + }; + + public FlashRecoveryAuto(File zipPath) { + super(zipPath); + } + + protected FlashRecoveryAuto(Parcel in) { + super(in); + } + + @Override + public void flash(Context context, FlashCallback callback) { + ZipCheckResult zipCheck = openAndCheckZip(callback); + if (zipCheck == null) { + return; + } else { + closeSilently(zipCheck.getZip()); + } + + final String zipName = mZipPath.getName(); + String cmd; + + // Execute the flash commands. + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(callback)) { + return; + } + + callback.onStarted(); + + // Make sure /cache/recovery/ exists. + if (rootUtil.execute("ls /cache/recovery", new ArrayList()) != 0) { + callback.onLine(context.getString(R.string.file_creating_directory, "/cache/recovery")); + if (rootUtil.executeWithBusybox("mkdir /cache/recovery", callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_create_directory_failed, "/cache/recovery")); + return; + } + } + + // Copy the ZIP to /cache/recovery/. + callback.onLine(context.getString(R.string.file_copying, zipName)); + cmd = "cp -a " + RootUtil.getShellPath(mZipPath) + " /cache/recovery/" + zipName; + if (rootUtil.executeWithBusybox(cmd, callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_copy_failed, zipName, "/cache/recovery")); + return; + } + + // Write the flashing command to /cache/recovery/command. + callback.onLine(context.getString(R.string.file_writing_recovery_command)); + cmd = "echo --update_package=/cache/recovery/" + zipName + " > /cache/recovery/command"; + if (rootUtil.execute(cmd, callback) != 0) { + callback.onError(FlashCallback.ERROR_GENERIC, + context.getString(R.string.file_writing_recovery_command_failed)); + return; + } + + callback.onLine(context.getString(R.string.auto_flash_note, zipName)); + callback.onDone(); + } + + @Override + public RootUtil.RebootMode getRebootMode() { + return RootUtil.RebootMode.RECOVERY; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java b/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java new file mode 100644 index 000000000..f47eb57f0 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/Flashable.java @@ -0,0 +1,78 @@ +package de.robv.android.xposed.installer.installation; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.closeSilently; +import static de.robv.android.xposed.installer.util.InstallZipUtil.reportMissingFeatures; +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; + +public abstract class Flashable implements Parcelable { + public static final String KEY = "flash"; + + protected final File mZipPath; + + public Flashable(File zipPath) { + mZipPath = zipPath; + } + + protected Flashable(Parcel in) { + mZipPath = (File) in.readSerializable(); + } + + protected InstallZipUtil.ZipCheckResult openAndCheckZip(FlashCallback callback) { + // Open the ZIP file. + ZipFile zip; + try { + zip = new ZipFile(mZipPath); + } catch (IOException e) { + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP, e.getLocalizedMessage()); + return null; + } + + // Do some checks. + InstallZipUtil.ZipCheckResult zipCheck = InstallZipUtil.checkZip(zip); + if (!zipCheck.isValidZip()) { + triggerError(callback, FlashCallback.ERROR_INVALID_ZIP); + closeSilently(zip); + return null; + } + + if (zipCheck.hasXposedProp()) { + Set missingFeatures = zipCheck.getXposedProp().getMissingInstallerFeatures(); + if (!missingFeatures.isEmpty()) { + reportMissingFeatures(missingFeatures); + triggerError(callback, FlashCallback.ERROR_INSTALLER_NEEDS_UPDATE); + closeSilently(zip); + return null; + } + } + + return zipCheck; + } + + public abstract void flash(Context context, FlashCallback callback); + + public RootUtil.RebootMode getRebootMode() { + return RootUtil.RebootMode.NORMAL; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeSerializable(mZipPath); + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java b/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java new file mode 100644 index 000000000..2e0c047f4 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/InstallationActivity.java @@ -0,0 +1,390 @@ +package de.robv.android.xposed.installer.installation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.LinearInterpolator; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.io.File; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.XposedBaseActivity; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.XposedApp.darkenColor; + +public class InstallationActivity extends XposedBaseActivity { + private static final int REBOOT_COUNTDOWN = 15000; + + private static final int MEDIUM_ANIM_TIME = XposedApp.getInstance().getResources() + .getInteger(android.R.integer.config_mediumAnimTime); + private static final int LONG_ANIM_TIME = XposedApp.getInstance().getResources() + .getInteger(android.R.integer.config_longAnimTime); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Flashable flashable = getIntent().getParcelableExtra(Flashable.KEY); + if (flashable == null) { + Log.e(XposedApp.TAG, InstallationActivity.class.getName() + ": Flashable is missing"); + finish(); + return; + } + + setContentView(R.layout.activity_container); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.install); + ab.setDisplayHomeAsUpEnabled(true); + } + + setFloating(toolbar, R.string.install); + + if (savedInstanceState == null) { + InstallationFragment logFragment = new InstallationFragment(); + getSupportFragmentManager().beginTransaction().replace(R.id.container, logFragment).commit(); + logFragment.startInstallation(this, flashable); + } + } + + @Override + public void onResume() { + super.onResume(); + + if (Build.VERSION.SDK_INT >= 21) + getWindow().setStatusBarColor(darkenColor(XposedApp.getColor(this), 0.85f)); + } + + public static class InstallationFragment extends Fragment implements FlashCallback { + private static final int TYPE_NONE = 0; + private static final int TYPE_ERROR = -1; + private static final int TYPE_OK = 1; + private Flashable mFlashable; + private TextView mLogText; + private ProgressBar mProgress; + private ImageView mConsoleResult; + private Button mBtnReboot; + private Button mBtnCancel; + + private static ValueAnimator createExpandCollapseAnimator(final View view, final boolean expand) { + ValueAnimator animator = new ValueAnimator() { + @Override + public void start() { + view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + int height = view.getMeasuredHeight(); + + int start = 0, end = 0; + if (expand) { + start = -height; + } else { + end = -height; + } + + setIntValues(start, end); + + super.start(); + } + }; + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + final ViewGroup.MarginLayoutParams layoutParams = ((ViewGroup.MarginLayoutParams) view.getLayoutParams()); + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + layoutParams.bottomMargin = (Integer) animation.getAnimatedValue(); + view.requestLayout(); + } + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!expand) { + view.setVisibility(View.GONE); + } + } + }); + + return animator; + } + + public void startInstallation(final Context context, final Flashable flashable) { + mFlashable = flashable; + new Thread("FlashZip") { + @Override + public void run() { + mFlashable.flash(context, InstallationFragment.this); + } + }.start(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.activity_installation, container, false); + + mLogText = view.findViewById(R.id.console); + mProgress = view.findViewById(R.id.progressBar); + mConsoleResult = view.findViewById(R.id.console_result); + mBtnReboot = view.findViewById(R.id.reboot); + mBtnCancel = view.findViewById(R.id.cancel); + + return view; + } + + @Override + public void onStarted() { + try { + Thread.sleep(LONG_ANIM_TIME * 3); + } catch (InterruptedException ignored) { + } + } + + @Override + public void onLine(final String line) { + try { + Thread.sleep(60); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(line, TYPE_NONE); + } + }); + } + + @Override + public void onErrorLine(final String line) { + try { + Thread.sleep(60); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(line, TYPE_ERROR); + } + }); + } + + @Override + public void onDone() { + try { + Thread.sleep(LONG_ANIM_TIME); + } catch (InterruptedException ignored) { + } + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText("\n" + getString(R.string.file_done), TYPE_OK); + + // Fade in the result image. + mConsoleResult.setImageResource(R.drawable.ic_check_circle); + mConsoleResult.setVisibility(View.VISIBLE); + ObjectAnimator fadeInResult = ObjectAnimator.ofFloat(mConsoleResult, "alpha", 0.0f, 0.03f); + fadeInResult.setDuration(MEDIUM_ANIM_TIME * 2); + + // Collapse the whole bottom bar. + View buttomBar = getView().findViewById(R.id.buttonPanel); + Animator collapseBottomBar = createExpandCollapseAnimator(buttomBar, false); + collapseBottomBar.setDuration(MEDIUM_ANIM_TIME); + collapseBottomBar.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgress.setIndeterminate(false); + mProgress.setRotation(180); + mProgress.setMax(REBOOT_COUNTDOWN); + mProgress.setProgress(REBOOT_COUNTDOWN); + + mBtnReboot.setVisibility(View.VISIBLE); + mBtnCancel.setVisibility(View.VISIBLE); + } + }); + + Animator expandBottomBar = createExpandCollapseAnimator(buttomBar, true); + expandBottomBar.setDuration(MEDIUM_ANIM_TIME * 2); + expandBottomBar.setStartDelay(LONG_ANIM_TIME * 4); + + final ObjectAnimator countdownProgress = ObjectAnimator.ofInt(mProgress, "progress", REBOOT_COUNTDOWN, 0); + countdownProgress.setDuration(REBOOT_COUNTDOWN); + countdownProgress.setInterpolator(new LinearInterpolator()); + + final ValueAnimator countdownButton = ValueAnimator.ofInt(REBOOT_COUNTDOWN / 1000, 0); + countdownButton.setDuration(REBOOT_COUNTDOWN); + countdownButton.setInterpolator(new LinearInterpolator()); + + final String format = getString(R.string.countdown); + final RootUtil.RebootMode rebootMode = mFlashable.getRebootMode(); + final String action = getString(rebootMode.titleRes); + mBtnReboot.setText(String.format(format, action, REBOOT_COUNTDOWN / 1000)); + + countdownButton.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + private int minWidth = 0; + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mBtnReboot.setText(String.format(format, action, animation.getAnimatedValue())); + + // Make sure that the button width doesn't shrink. + if (mBtnReboot.getWidth() > minWidth) { + minWidth = mBtnReboot.getWidth(); + mBtnReboot.setMinimumWidth(minWidth); + } + } + }); + + countdownButton.addListener(new AnimatorListenerAdapter() { + private boolean canceled = false; + + @Override + public void onAnimationCancel(Animator animation) { + canceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!canceled) { + mBtnReboot.callOnClick(); + } + } + }); + + mBtnReboot.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownProgress.cancel(); + countdownButton.cancel(); + + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell(InstallationFragment.this) + || !rootUtil.reboot(rebootMode, InstallationFragment.this)) { + onError(FlashCallback.ERROR_GENERIC, getString(R.string.reboot_failed)); + } + } + }); + + mBtnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownProgress.cancel(); + countdownButton.cancel(); + + getActivity().finish(); + } + }); + + AnimatorSet as = new AnimatorSet(); + as.play(fadeInResult); + as.play(collapseBottomBar).with(fadeInResult); + as.play(expandBottomBar).after(collapseBottomBar); + as.play(countdownProgress).after(expandBottomBar); + as.play(countdownButton).after(expandBottomBar); + as.start(); + } + }); + } + + @Override + public void onError(final int exitCode, final String error) { + XposedApp.postOnUiThread(new Runnable() { + @Override + public void run() { + appendText(error, TYPE_ERROR); + + mConsoleResult.setImageResource(R.drawable.ic_error); + mConsoleResult.setVisibility(View.VISIBLE); + ObjectAnimator fadeInResult = ObjectAnimator.ofFloat(mConsoleResult, "alpha", 0.0f, 0.03f); + fadeInResult.setDuration(MEDIUM_ANIM_TIME * 2); + + View buttomBar = getView().findViewById(R.id.buttonPanel); + Animator collapseBottomBar = createExpandCollapseAnimator(buttomBar, false); + collapseBottomBar.setDuration(MEDIUM_ANIM_TIME); + + AnimatorSet as = new AnimatorSet(); + as.play(fadeInResult); + as.play(collapseBottomBar).with(fadeInResult); + as.start(); + } + }); + } + + @SuppressLint("SetTextI18n") + private void appendText(String text, int type) { + int color; + switch (type) { + case TYPE_ERROR: + color = ContextCompat.getColor(getActivity(), R.color.red_500); + break; + case TYPE_OK: + color = ContextCompat.getColor(getActivity(), R.color.darker_green); + break; + default: + mLogText.append(text); + mLogText.append("\n"); + return; + } + + int start = mLogText.length(); + mLogText.append(text); + int end = mLogText.length(); + ((Spannable) mLogText.getText()).setSpan(new ForegroundColorSpan(color), start, end, 0); + mLogText.append("\n"); + } + + private boolean isOkSystemless() { + boolean suPartition = new File("/su").exists() && new File("/data/su.img").exists(); + boolean m = Build.VERSION.SDK_INT >= 23; + + /* + TODO: Add toggle for user to force system installation + */ + + return m && suPartition; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java b/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java new file mode 100644 index 000000000..4dfb2e093 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/installation/StatusInstallerFragment.java @@ -0,0 +1,521 @@ +package de.robv.android.xposed.installer.installation; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v7.widget.SwitchCompat; +import android.text.Html; +import android.util.Log; +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.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.DownloadsUtil; +import de.robv.android.xposed.installer.util.InstallApkUtil; +import de.robv.android.xposed.installer.util.InstallZipUtil; +import de.robv.android.xposed.installer.util.NavUtil; +import de.robv.android.xposed.installer.util.RootUtil; + +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class StatusInstallerFragment extends Fragment { + + public static final File DISABLE_FILE = new File(XposedApp.BASE_DIR + "conf/disabled"); + public static String ARCH = getArch(); + private static Activity sActivity; + private static Fragment sFragment; + private static String mUpdateLink; + private static ImageView mErrorIcon; + private static View mUpdateView; + private static View mUpdateButton; + private static TextView mErrorTv; + private static boolean isXposedInstalled = false; + private TextView txtKnownIssue; + + public static void setError(boolean connectionFailed, boolean noSdks) { + if (!connectionFailed && !noSdks) { + if (isXposedInstalled) return; + return; + } + + mErrorTv.setVisibility(View.VISIBLE); + mErrorIcon.setVisibility(View.VISIBLE); + if (noSdks) { + mErrorIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_warning_grey)); + mErrorTv.setText(String.format(sActivity.getString(R.string.phone_not_compatible), Build.VERSION.SDK_INT, Build.CPU_ABI)); + } + if (connectionFailed) { + mErrorIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_no_connection)); + mErrorTv.setText(sActivity.getString(R.string.loadingError)); + } + } + + public static void setUpdate(final String link, final String changelog) { + mUpdateLink = link; + + mUpdateView.setVisibility(View.VISIBLE); + mUpdateButton.setVisibility(View.VISIBLE); + mUpdateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + new MaterialDialog.Builder(sActivity) + .title(R.string.changes) + .content(Html.fromHtml(changelog)) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + update(); + } + }) + .positiveText(R.string.update) + .negativeText(R.string.later).show(); + } + }); + } + + private static void update() { + if (checkPermissions()) return; + + final String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/XposedInstaller/XposedInstaller_by_dvdandroid.apk"; + + new File(path).delete(); + + new DownloadsUtil.Builder(sActivity) + .setTitle("XposedInstaller_by_dvdandroid") + .setUrl(mUpdateLink) + .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_FRAMEWORK) + .setCallback(new DownloadsUtil.DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + new InstallApkUtil(context, info).execute(); + } + }) + .setMimeType(DownloadsUtil.MIME_TYPES.APK) + .setDialog(true) + .download(); + + } + + private static boolean checkPermissions() { + if (Build.VERSION.SDK_INT < 23) return false; + + if (ActivityCompat.checkSelfPermission(sActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + sFragment.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + private static boolean checkClassExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static String getCompleteArch() { + String info = ""; + + try { + FileReader fr = new FileReader("/proc/cpuinfo"); + BufferedReader br = new BufferedReader(fr); + String text; + while ((text = br.readLine()) != null) { + if (!text.startsWith("processor")) break; + } + br.close(); + String[] array = text != null ? text.split(":\\s+", 2) : new String[0]; + if (array.length >= 2) { + info += array[1] + " "; + } + } catch (IOException ignored) { + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + info += Build.SUPPORTED_ABIS[0]; + } else { + String arch = System.getenv("os.arch"); + if (arch != null) info += arch; + } + return info + " (" + getArch() + ")"; + } + + @SuppressWarnings("deprecation") + private static String getArch() { + if (Build.CPU_ABI.equals("arm64-v8a")) { + return "arm64"; + } else if (Build.CPU_ABI.equals("x86_64")) { + return "x86_64"; + } else if (Build.CPU_ABI.equals("mips64")) { + return "mips64"; + } else if (Build.CPU_ABI.startsWith("x86") || Build.CPU_ABI2.startsWith("x86")) { + return "x86"; + } else if (Build.CPU_ABI.startsWith("mips")) { + return "mips"; + } else if (Build.CPU_ABI.startsWith("armeabi-v5") || Build.CPU_ABI.startsWith("armeabi-v6")) { + return "armv5"; + } else { + return "arm"; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + sActivity = getActivity(); + sFragment = this; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + update(); + } + }, 500); + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.status_installer, container, false); + + mErrorIcon = v.findViewById(R.id.errorIcon); + mErrorTv = v.findViewById(R.id.errorTv); + mUpdateView = v.findViewById(R.id.updateView); + mUpdateButton = v.findViewById(R.id.click_to_update); + + txtKnownIssue = v.findViewById(R.id.framework_known_issue); + + TextView txtInstallError = v.findViewById(R.id.framework_install_errors); + View txtInstallContainer = v.findViewById(R.id.status_container); + ImageView txtInstallIcon = v.findViewById(R.id.status_icon); + + String installedXposedVersion; + try { + installedXposedVersion= XposedApp.getXposedProp().getVersion(); + } catch (NullPointerException e) { + installedXposedVersion = null; + } + + View disableView = v.findViewById(R.id.disableView); + final SwitchCompat xposedDisable = v.findViewById(R.id.disableSwitch); + + TextView androidSdk = v.findViewById(R.id.android_version); + TextView manufacturer = v.findViewById(R.id.ic_manufacturer); + TextView cpu = v.findViewById(R.id.cpu); + + if (Build.VERSION.SDK_INT >= 21) { + if (installedXposedVersion != null) { + int installedXposedVersionInt = extractIntPart(installedXposedVersion); + if (installedXposedVersionInt == XposedApp.getXposedVersion()) { + txtInstallError.setText(sActivity.getString(R.string.installed_lollipop, installedXposedVersion)); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.darker_green)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.darker_green)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_check_circle)); + isXposedInstalled = true; + } else { + txtInstallError.setText(sActivity.getString(R.string.installed_lollipop_inactive, installedXposedVersion)); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.amber_500)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.amber_500)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_warning)); + } + } else { + txtInstallError.setText(R.string.not_installed_no_lollipop); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.warning)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.warning)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_error)); + xposedDisable.setVisibility(View.GONE); + disableView.setVisibility(View.GONE); + } + } else { + int installedXposedVersionInt = XposedApp.getXposedVersion(); + if (installedXposedVersionInt != 0) { + txtInstallError.setText(sActivity.getString(R.string.installed_lollipop, "" + installedXposedVersionInt)); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.darker_green)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.darker_green)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_check_circle)); + isXposedInstalled = true; + if (DISABLE_FILE.exists()) { + txtInstallError.setText(sActivity.getString(R.string.installed_lollipop_inactive, "" + installedXposedVersionInt)); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.amber_500)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.amber_500)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_warning)); + } + } else { + txtInstallError.setText(sActivity.getString(R.string.not_installed_no_lollipop)); + txtInstallError.setTextColor(sActivity.getResources().getColor(R.color.warning)); + txtInstallContainer.setBackgroundColor(sActivity.getResources().getColor(R.color.warning)); + txtInstallIcon.setImageDrawable(sActivity.getResources().getDrawable(R.drawable.ic_error)); + xposedDisable.setVisibility(View.GONE); + disableView.setVisibility(View.GONE); + } + } + + xposedDisable.setChecked(!DISABLE_FILE.exists()); + + xposedDisable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (DISABLE_FILE.exists()) { + DISABLE_FILE.delete(); + Snackbar.make(xposedDisable, R.string.xposed_on_next_reboot, Snackbar.LENGTH_LONG).show(); + } else { + try { + DISABLE_FILE.createNewFile(); + Snackbar.make(xposedDisable, R.string.xposed_off_next_reboot, Snackbar.LENGTH_LONG).show(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "StatusInstallerFragment -> " + e.getMessage()); + } + } + } + }); + + androidSdk.setText(getString(R.string.android_sdk, getAndroidVersion(), Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); + manufacturer.setText(getUIFramework()); + cpu.setText(getCompleteArch()); + + determineVerifiedBootState(v); + + refreshKnownIssue(); + return v; + } + + private void determineVerifiedBootState(View v) { + try { + Class c = Class.forName("android.os.SystemProperties"); + Method m = c.getDeclaredMethod("get", String.class, String.class); + m.setAccessible(true); + + String propSystemVerified = (String) m.invoke(null, "partition.system.verified", "0"); + String propState = (String) m.invoke(null, "ro.boot.verifiedbootstate", ""); + File fileDmVerityModule = new File("/sys/module/dm_verity"); + + boolean verified = !propSystemVerified.equals("0"); + boolean detected = !propState.isEmpty() || fileDmVerityModule.exists(); + + TextView tv = v.findViewById(R.id.dmverity); + if (verified) { + tv.setText(R.string.verified_boot_active); + tv.setTextColor(getResources().getColor(R.color.warning)); + } else if (detected) { + tv.setText(R.string.verified_boot_deactivated); + v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE); + } else { + v.findViewById(R.id.dmverity_row).setVisibility(View.GONE); + } + } catch (Exception e) { + Log.e(XposedApp.TAG, "Could not detect Verified Boot state", e); + } + } + + @SuppressLint("StringFormatInvalid") + private void refreshKnownIssue() { + String issueName = null; + String issueLink = null; + final ApplicationInfo appInfo = getActivity().getApplicationInfo(); + final Set missingFeatures = XposedApp.getXposedProp() == null ? new HashSet() : XposedApp.getXposedProp().getMissingInstallerFeatures(); + final File baseDir = new File(XposedApp.BASE_DIR); + final File baseDirCanonical = getCanonicalFile(baseDir); + final File baseDirActual = new File(Build.VERSION.SDK_INT >= 24 ? appInfo.deviceProtectedDataDir : appInfo.dataDir); + final File baseDirActualCanonical = getCanonicalFile(baseDirActual); + + if (!missingFeatures.isEmpty()) { + InstallZipUtil.reportMissingFeatures(missingFeatures); + issueName = getString(R.string.installer_needs_update, getString(R.string.app_name)); + issueLink = getString(R.string.about_support); + } else if (new File("/system/framework/core.jar.jex").exists()) { + issueName = "Aliyun OS"; + issueLink = "https://forum.xda-developers.com/showpost.php?p=52289793&postcount=5"; + } else if (Build.VERSION.SDK_INT < 24 && (new File("/data/miui/DexspyInstaller.jar").exists() || checkClassExists("miui.dexspy.DexspyInstaller"))) { + issueName = "MIUI/Dexspy"; + issueLink = "https://forum.xda-developers.com/showpost.php?p=52291098&postcount=6"; + } else if (Build.VERSION.SDK_INT < 24 && new File("/system/framework/twframework.jar").exists()) { + issueName = "Samsung TouchWiz ROM"; + issueLink = "https://forum.xda-developers.com/showthread.php?t=3034811"; + } else if (!baseDirCanonical.equals(baseDirActualCanonical)) { + Log.e(XposedApp.TAG, "Base directory: " + getPathWithCanonicalPath(baseDir, baseDirCanonical)); + Log.e(XposedApp.TAG, "Expected: " + getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + issueName = getString(R.string.known_issue_wrong_base_directory, getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + } else if (!baseDir.exists()) { + issueName = getString(R.string.known_issue_missing_base_directory); + issueLink = "https://github.com/rovo89/XposedInstaller/issues/393"; + } + + if (issueName != null) { + final String issueLinkFinal = issueLink; + txtKnownIssue.setText(getString(R.string.install_known_issue, issueName)); + txtKnownIssue.setVisibility(View.VISIBLE); + txtKnownIssue.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), issueLinkFinal); + } + }); + } else { + txtKnownIssue.setVisibility(View.GONE); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (Build.VERSION.SDK_INT < 26) { + menu.findItem(R.id.dexopt_now).setVisible(false); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.dexopt_now: + new MaterialDialog.Builder(getActivity()) + .title(R.string.dexopt_now) + .content(R.string.this_may_take_a_while) + .progress(true, 0) + .cancelable(false) + .showListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + new Thread("dexopt") { + @Override + public void run() { + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + dialog.dismiss(); + NavUtil.showMessage(getActivity(), getString(R.string.root_failed)); + return; + } + + rootUtil.execute("cmd package bg-dexopt-job", new ArrayList()); + + dialog.dismiss(); + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getActivity(), R.string.done, Toast.LENGTH_LONG).show(); + } + }); + } + }.start(); + } + }).show(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + + private String getAndroidVersion() { + switch (Build.VERSION.SDK_INT) { + case 16: + case 17: + case 18: + return "Jelly Bean"; + case 19: + return "KitKat"; + case 21: + case 22: + return "Lollipop"; + case 23: + return "Marshmallow"; + case 24: + case 25: + return "Nougat"; + case 26: + case 27: + return "Oreo"; + } + return ""; + } + + private String getUIFramework() { + String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1); + if (!Build.BRAND.equals(Build.MANUFACTURER)) { + manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1); + } + manufacturer += " " + Build.MODEL + " "; + if (manufacturer.contains("Samsung")) { + manufacturer += new File("/system/framework/twframework.jar").exists() || + new File("/system/framework/samsung-services.jar").exists() + ? "(TouchWiz)" : "(AOSP-based ROM)"; + } else if (manufacturer.contains("Xiaomi")) { + manufacturer += new File("/system/framework/framework-miui-res.apk").exists() ? "(MIUI)" : "(AOSP-based ROM)"; + } + return manufacturer; + } + + private File getCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Failed to get canonical file for " + file.getAbsolutePath(), e); + return file; + } + } + + private String getPathWithCanonicalPath(File file, File canonical) { + if (file.equals(canonical)) { + return file.getAbsolutePath(); + } else { + return file.getAbsolutePath() + " \u2192 " + canonical.getAbsolutePath(); + } + } + + private int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/receivers/BootReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/receivers/BootReceiver.java new file mode 100644 index 000000000..eeed7110e --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/receivers/BootReceiver.java @@ -0,0 +1,62 @@ +package de.robv.android.xposed.installer.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONObject; + +import java.math.BigInteger; + +import de.robv.android.xposed.installer.BuildConfig; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.util.NotificationUtil; +import de.robv.android.xposed.installer.util.json.JSONUtils; + +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, Intent intent) { + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (!isOnline(context)) return; + + new CheckUpdates().execute(); + } + }, 60 * 60 * 1000 /*60 min*/); + } + + private boolean isOnline(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = cm.getActiveNetworkInfo(); + return netInfo != null && netInfo.isConnectedOrConnecting(); + } + + private class CheckUpdates extends AsyncTask { + + @Override + protected Void doInBackground(Void... params) { + try { + String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", ""); + + String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version"); + + BigInteger a = new BigInteger(BuildConfig.APP_VERSION); + BigInteger b = new BigInteger(newApkVersion); + + if (a.compareTo(b) == -1) { + NotificationUtil.showInstallerUpdateNotification(); + } + } catch (Exception e) { + Log.d(XposedApp.TAG, e.getMessage()); + } + return null; + } + + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java new file mode 100644 index 000000000..ee2ed9a35 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/receivers/DownloadReceiver.java @@ -0,0 +1,19 @@ +package de.robv.android.xposed.installer.receivers; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import de.robv.android.xposed.installer.util.DownloadsUtil; + +public class DownloadReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, final Intent intent) { + String action = intent.getAction(); + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java b/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java new file mode 100644 index 000000000..f08d69b08 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/receivers/PackageChangeReceiver.java @@ -0,0 +1,78 @@ +package de.robv.android.xposed.installer.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import de.robv.android.xposed.installer.util.ModuleUtil; +import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; +import de.robv.android.xposed.installer.util.NotificationUtil; + +public class PackageChangeReceiver extends BroadcastReceiver { + private static ModuleUtil mModuleUtil = null; + + private static String getPackageName(Intent intent) { + Uri uri = intent.getData(); + return (uri != null) ? uri.getSchemeSpecificPart() : null; + } + + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED) && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + // Ignore existing packages being removed in order to be updated + return; + + String packageName = getPackageName(intent); + if (packageName == null) + return; + + if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) { + // make sure that the change is for the complete package, not only a + // component + String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (components != null) { + boolean isForPackage = false; + for (String component : components) { + if (packageName.equals(component)) { + isForPackage = true; + break; + } + } + if (!isForPackage) + return; + } + } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + NotificationUtil.cancel(packageName, NotificationUtil.NOTIFICATION_MODULE_NOT_ACTIVATED_YET); + return; + } + + mModuleUtil = getModuleUtilInstance(); + + InstalledModule module = ModuleUtil.getInstance().reloadSingleModule(packageName); + if (module == null + || intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + // Package being removed, disable it if it was a previously active + // Xposed mod + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.setModuleEnabled(packageName, false); + mModuleUtil.updateModulesList(false); + } + return; + } + + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.updateModulesList(false); + NotificationUtil.showModulesUpdatedNotification(); + } else { + NotificationUtil.showNotActivatedNotification(packageName, module.getAppName()); + } + } + + private ModuleUtil getModuleUtilInstance() { + if (mModuleUtil == null) { + mModuleUtil = ModuleUtil.getInstance(); + } + return mModuleUtil; + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java b/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java index 8d416a4b3..0f49ee2cc 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/Module.java @@ -1,27 +1,27 @@ package de.robv.android.xposed.installer.repo; +import android.util.Pair; + import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import android.util.Pair; - public class Module { - public final Repository repository; - public String packageName; - public String name; - public String summary; - public String description; - public boolean descriptionIsHtml = false; - public String author; - public String support; - public final List> moreInfo = new LinkedList>(); - public final List versions = new ArrayList(); - public final List screenshots = new ArrayList(); - public long created = -1; - public long updated = -1; + public final Repository repository; + public final List> moreInfo = new LinkedList>(); + public final List versions = new ArrayList(); + public final List screenshots = new ArrayList(); + public String packageName; + public String name; + public String summary; + public String description; + public boolean descriptionIsHtml = false; + public String author; + public String support; + public long created = -1; + public long updated = -1; - /*package*/ Module(Repository repository) { - this.repository = repository; - } + /* package */ Module(Repository repository) { + this.repository = repository; + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java b/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java index 082fbb21a..153decf90 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/ModuleVersion.java @@ -1,18 +1,17 @@ package de.robv.android.xposed.installer.repo; - public class ModuleVersion { - public final Module module; - public String name; - public int code; - public String downloadLink; - public String md5sum; - public String changelog; - public boolean changelogIsHtml = false; - public ReleaseType relType = ReleaseType.STABLE; - public long uploaded = -1; + public final Module module; + public String name; + public int code; + public String downloadLink; + public String md5sum; + public String changelog; + public boolean changelogIsHtml = false; + public ReleaseType relType = ReleaseType.STABLE; + public long uploaded = -1; - /*package*/ ModuleVersion(Module module) { - this.module = module; - } + /* package */ ModuleVersion(Module module) { + this.module = module; + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java b/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java index 7ef1bf2d9..7ae03f7f0 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/ReleaseType.java @@ -3,40 +3,37 @@ import de.robv.android.xposed.installer.R; public enum ReleaseType { - STABLE (R.string.reltype_stable, R.string.reltype_stable_summary), - BETA (R.string.reltype_beta, R.string.reltype_beta_summary), - EXPERIMENTAL (R.string.reltype_experimental, R.string.reltype_experimental_summary); - - private static final ReleaseType[] sValuesCache = values(); - - public static ReleaseType fromString(String value) { - if (value == null || value.equals("stable")) - return STABLE; - else if (value.equals("beta")) - return BETA; - else if (value.equals("experimental")) - return EXPERIMENTAL; - else - return STABLE; - } - - public static ReleaseType fromOrdinal(int ordinal) { - return sValuesCache[ordinal]; - } - - private final int mTitleId; - private final int mSummaryId; - - private ReleaseType(int titleId, int summaryId) { - mTitleId = titleId; - mSummaryId = summaryId; - } - - public int getTitleId() { - return mTitleId; - } - - public int getSummaryId() { - return mSummaryId; - } + STABLE(R.string.reltype_stable, R.string.reltype_stable_summary), BETA(R.string.reltype_beta, R.string.reltype_beta_summary), EXPERIMENTAL(R.string.reltype_experimental, R.string.reltype_experimental_summary); + + private static final ReleaseType[] sValuesCache = values(); + private final int mTitleId; + private final int mSummaryId; + + ReleaseType(int titleId, int summaryId) { + mTitleId = titleId; + mSummaryId = summaryId; + } + + public static ReleaseType fromString(String value) { + if (value == null || value.equals("stable")) + return STABLE; + else if (value.equals("beta")) + return BETA; + else if (value.equals("experimental")) + return EXPERIMENTAL; + else + return STABLE; + } + + public static ReleaseType fromOrdinal(int ordinal) { + return sValuesCache[ordinal]; + } + + public int getTitleId() { + return mTitleId; + } + + public int getSummaryId() { + return mSummaryId; + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java index 6b6245acf..9ea31a785 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDb.java @@ -1,16 +1,21 @@ package de.robv.android.xposed.installer.repo; -import java.io.File; -import java.util.LinkedHashMap; -import java.util.Map; - import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; import android.text.TextUtils; import android.util.Pair; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +import de.robv.android.xposed.installer.BuildConfig; +import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesUpdatesColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.ModuleVersionsColumns; @@ -23,471 +28,469 @@ import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.RepoLoader; +import static android.content.Context.MODE_PRIVATE; + public final class RepoDb extends SQLiteOpenHelper { - public static final int SORT_STATUS = 0; - public static final int SORT_UPDATED = 1; - public static final int SORT_CREATED = 2; - - private static RepoDb mInstance; - private static SQLiteDatabase mDb; - private static RepoLoader mRepoLoader; - - public synchronized static void init(Context context, RepoLoader repoLoader) { - if (mInstance != null) - throw new IllegalStateException(RepoDb.class.getSimpleName() + " is already initialized"); - - mRepoLoader = repoLoader; - mInstance = new RepoDb(context); - mDb = mInstance.getWritableDatabase(); - mDb.execSQL("PRAGMA foreign_keys=ON"); - mInstance.createTempTables(mDb); - } - - private RepoDb(Context context) { - super(context, new File(context.getCacheDir(), RepoDbDefinitions.DATABASE_NAME).getPath(), - null, RepoDbDefinitions.DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES); - db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES); - db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS); - db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID); - db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO); - - mRepoLoader.clear(false); - } - - private void createTempTables(SQLiteDatabase db) { - db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES); - db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // This is only a cache, so simply drop & recreate the tables - db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME); - - db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME); - db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME); - - onCreate(db); - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onUpgrade(db, oldVersion, newVersion); - } - - public static void beginTransation() { - mDb.beginTransaction(); - } - - public static void setTransactionSuccessful() { - mDb.setTransactionSuccessful(); - } - - public static void endTransation() { - mDb.endTransaction(); - } - - private static String getString(String table, String searchColumn, String searchValue, String resultColumn) { - String[] projection = new String[] { resultColumn }; - String where = searchColumn + " = ?"; - String[] whereArgs = new String[] { searchValue }; - Cursor c = mDb.query(table, projection, where, whereArgs, null, null, null, "1"); - if (c.moveToFirst()) { - String result = c.getString(c.getColumnIndexOrThrow(resultColumn)); - c.close(); - return result; - } else { - c.close(); - throw new RowNotFoundException("Could not find " + table + "." + searchColumn - + " with value '" + searchValue + "'"); - } - } - - public static long insertRepository(String url) { - ContentValues values = new ContentValues(); - values.put(RepositoriesColumns.URL, url); - return mDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values); - } - - public static void deleteRepositories() { - if (mDb != null) - mDb.delete(RepositoriesColumns.TABLE_NAME, null, null); - } - - public static Map getRepositories() { - Map result = new LinkedHashMap(1); - - String[] projection = new String[] { - RepositoriesColumns._ID, - RepositoriesColumns.URL, - RepositoriesColumns.TITLE, - RepositoriesColumns.PARTIAL_URL, - RepositoriesColumns.VERSION, - }; - - Cursor c = mDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID); - while (c.moveToNext()) { - Repository repo = new Repository(); - long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID)); - repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL)); - repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE)); - repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL)); - repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION)); - - result.put(id, repo); - } - c.close(); - - return result; - } - - public static void updateRepository(long repoId, Repository repository) { - ContentValues values = new ContentValues(); - values.put(RepositoriesColumns.TITLE, repository.name); - values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl); - values.put(RepositoriesColumns.VERSION, repository.version); - mDb.update(RepositoriesColumns.TABLE_NAME, values, - RepositoriesColumns._ID + " = ?", - new String[] { Long.toString(repoId) }); - } - - public static void updateRepositoryVersion(long repoId, String version) { - ContentValues values = new ContentValues(); - values.put(RepositoriesColumns.VERSION, version); - mDb.update(RepositoriesColumns.TABLE_NAME, values, - RepositoriesColumns._ID + " = ?", - new String[] { Long.toString(repoId) }); - } - - public static long insertModule(long repoId, Module mod) { - ContentValues values = new ContentValues(); - values.put(ModulesColumns.REPO_ID, repoId); - values.put(ModulesColumns.PKGNAME, mod.packageName); - values.put(ModulesColumns.TITLE, mod.name); - values.put(ModulesColumns.SUMMARY, mod.summary); - values.put(ModulesColumns.DESCRIPTION, mod.description); - values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml); - values.put(ModulesColumns.AUTHOR, mod.author); - values.put(ModulesColumns.SUPPORT, mod.support); - values.put(ModulesColumns.CREATED, mod.created); - values.put(ModulesColumns.UPDATED, mod.updated); - - ModuleVersion latestVersion = mRepoLoader.getLatestVersion(mod); - - mDb.beginTransaction(); - try { - long moduleId = mDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values); - - long latestVersionId = -1; - for (ModuleVersion version : mod.versions) { - long versionId = insertModuleVersion(moduleId, version); - if (latestVersion == version) - latestVersionId = versionId; - } - - if (latestVersionId > -1) { - values = new ContentValues(); - values.put(ModulesColumns.LATEST_VERSION, latestVersionId); - mDb.update(ModulesColumns.TABLE_NAME, values, - ModulesColumns._ID + " = ?", - new String[] { Long.toString(moduleId) }); - } - - for (Pair moreInfoEntry : mod.moreInfo) { - insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second); - } - - // TODO Add mod.screenshots - - mDb.setTransactionSuccessful(); - return moduleId; - - } finally { - mDb.endTransaction(); - } - } - - private static long insertModuleVersion(long moduleId, ModuleVersion version) { - ContentValues values = new ContentValues(); - values.put(ModuleVersionsColumns.MODULE_ID, moduleId); - values.put(ModuleVersionsColumns.NAME, version.name); - values.put(ModuleVersionsColumns.CODE, version.code); - values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink); - values.put(ModuleVersionsColumns.MD5SUM, version.md5sum); - values.put(ModuleVersionsColumns.CHANGELOG, version.changelog); - values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml); - values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal()); - values.put(ModuleVersionsColumns.UPLOADED, version.uploaded); - return mDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null, values); - } - - private static long insertMoreInfo(long moduleId, String title, String value) { - ContentValues values = new ContentValues(); - values.put(MoreInfoColumns.MODULE_ID, moduleId); - values.put(MoreInfoColumns.LABEL, title); - values.put(MoreInfoColumns.VALUE, value); - return mDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values); - } - - public static void deleteAllModules(long repoId) { - mDb.delete(ModulesColumns.TABLE_NAME, - ModulesColumns.REPO_ID + " = ?", - new String[] { Long.toString(repoId) }); - } - - public static void deleteModule(long repoId, String packageName) { - mDb.delete(ModulesColumns.TABLE_NAME, - ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", - new String[] { Long.toString(repoId), packageName }); - } - - public static Module getModuleByPackageName(String packageName) { - // The module itself - String[] projection = new String[] { - ModulesColumns._ID, - ModulesColumns.REPO_ID, - ModulesColumns.PKGNAME, - ModulesColumns.TITLE, - ModulesColumns.SUMMARY, - ModulesColumns.DESCRIPTION, - ModulesColumns.DESCRIPTION_IS_HTML, - ModulesColumns.AUTHOR, - ModulesColumns.SUPPORT, - ModulesColumns.CREATED, - ModulesColumns.UPDATED, - }; - - String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?"; - String[] whereArgs = new String[] { packageName }; - - Cursor c = mDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1"); - if (!c.moveToFirst()) { - c.close(); - return null; - } - - long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID)); - long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID)); - - Module mod = new Module(mRepoLoader.getRepository(repoId)); - mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME)); - mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE)); - mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY)); - mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION)); - mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0; - mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR)); - mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT)); - mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED)); - mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED)); - - c.close(); - - - // Versions - projection = new String[] { - ModuleVersionsColumns.NAME, - ModuleVersionsColumns.CODE, - ModuleVersionsColumns.DOWNLOAD_LINK, - ModuleVersionsColumns.MD5SUM, - ModuleVersionsColumns.CHANGELOG, - ModuleVersionsColumns.CHANGELOG_IS_HTML, - ModuleVersionsColumns.RELTYPE, - ModuleVersionsColumns.UPLOADED, - }; - - where = ModuleVersionsColumns.MODULE_ID + " = ?"; - whereArgs = new String[] { Long.toString(moduleId) }; - - c = mDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null); - while (c.moveToNext()) { - ModuleVersion version = new ModuleVersion(mod); - version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME)); - version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE)); - version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK)); - version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM)); - version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG)); - version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0; - version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE))); - version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED)); - mod.versions.add(version); - } - c.close(); - - - // MoreInfo - projection = new String[] { - MoreInfoColumns.LABEL, - MoreInfoColumns.VALUE, - }; - - where = MoreInfoColumns.MODULE_ID + " = ?"; - whereArgs = new String[] { Long.toString(moduleId) }; - - c = mDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID); - while (c.moveToNext()) { - String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL)); - String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE)); - mod.moreInfo.add(new Pair(label, value)); - } - c.close(); - - return mod; - } - - public static String getModuleSupport(String packageName) { - return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT); - } - - public static void updateModuleLatestVersion(String packageName) { - int maxShownReleaseType = mRepoLoader.getMaxShownReleaseType(packageName).ordinal(); - mDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME - + " SET " + ModulesColumns.LATEST_VERSION - + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v" - + " WHERE v." + ModuleVersionsColumns.MODULE_ID - + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID - + " AND reltype <= ? LIMIT 1)" - + " WHERE " + ModulesColumns.PKGNAME + " = ?", - new Object[] { maxShownReleaseType, packageName }); - } - - public static void updateAllModulesLatestVersion() { - mDb.beginTransaction(); - try { - String[] projection = new String[] { ModulesColumns.PKGNAME }; - Cursor c = mDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null); - while (c.moveToNext()) { - updateModuleLatestVersion(c.getString(0)); - } - c.close(); - mDb.setTransactionSuccessful(); - } finally { - mDb.endTransaction(); - } - } - - public static long insertInstalledModule(InstalledModule installed) { - ContentValues values = new ContentValues(); - values.put(InstalledModulesColumns.PKGNAME, installed.packageName); - values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode); - values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName); - return mDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values); - } - - public static void deleteInstalledModule(String packageName) { - mDb.delete(InstalledModulesColumns.TABLE_NAME, - InstalledModulesColumns.PKGNAME + " = ?", - new String[] { packageName }); - } - - public static void deleteAllInstalledModules() { - mDb.delete(InstalledModulesColumns.TABLE_NAME, null, null); - } - - public static Cursor queryModuleOverview(int sortingOrder, CharSequence filterText) { - // Columns - String[] projection = new String[] { - "m." + ModulesColumns._ID, - "m." + ModulesColumns.PKGNAME, - "m." + ModulesColumns.TITLE, - "m." + ModulesColumns.SUMMARY, - "m." + ModulesColumns.CREATED, - "m." + ModulesColumns.UPDATED, - - "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION, - "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION, - - "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName() - + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK, - - "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL" - + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED, - - "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE - + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE, - }; - - // Conditions - String where = ModulesColumns.PREFERRED + " = 1"; - String whereArgs[] = null; - if (!TextUtils.isEmpty(filterText)) { - where += " AND (m." + ModulesColumns.TITLE + " LIKE ?" - + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" - + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" - + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)"; - - String filterTextArg = "%" + filterText + "%"; - whereArgs = new String[] { filterTextArg, filterTextArg, filterTextArg, filterTextArg }; - } - - // Sorting order - StringBuilder sbOrder = new StringBuilder(); - if (sortingOrder == SORT_CREATED) { - sbOrder.append(OverviewColumns.CREATED); - sbOrder.append(" DESC,"); - } else if (sortingOrder == SORT_UPDATED) { - sbOrder.append(OverviewColumns.UPDATED); - sbOrder.append(" DESC,"); - } - sbOrder.append(OverviewColumns.IS_FRAMEWORK); - sbOrder.append(" DESC, "); - sbOrder.append(OverviewColumns.HAS_UPDATE); - sbOrder.append(" DESC, "); - sbOrder.append(OverviewColumns.IS_INSTALLED); - sbOrder.append(" DESC, "); - sbOrder.append("m."); - sbOrder.append(OverviewColumns.TITLE); - sbOrder.append(" COLLATE NOCASE, "); - sbOrder.append("m."); - sbOrder.append(OverviewColumns.PKGNAME); - - // Query - Cursor c = mDb.query( - ModulesColumns.TABLE_NAME + " AS m" + - " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + - " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + - " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i" + - " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME, - projection, where, whereArgs, null, null, sbOrder.toString()); - - // Cache column indexes - OverviewColumnsIndexes.fillFromCursor(c); - - return c; - } - - public static String getFrameworkUpdateVersion() { - return getFirstUpdate(true); - } - - public static boolean hasModuleUpdates() { - return getFirstUpdate(false) != null; - } - - private static String getFirstUpdate(boolean framework) { - String[] projection = new String[] { InstalledModulesUpdatesColumns.LATEST_NAME }; - String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?"); - String[] whereArgs = new String[] { ModuleUtil.getInstance().getFrameworkPackageName() }; - Cursor c = mDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1"); - String latestVersion = null; - if (c.moveToFirst()) - latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME)); - c.close(); - return latestVersion; - } - - - public static class RowNotFoundException extends RuntimeException { - private static final long serialVersionUID = -396324186622439535L; - public RowNotFoundException(String reason) { - super(reason); - } - } + public static final int SORT_STATUS = 0; + public static final int SORT_UPDATED = 1; + public static final int SORT_CREATED = 2; + + private static Context context; + private static SQLiteDatabase sDb; + + static { + RepoDb instance = new RepoDb(XposedApp.getInstance()); + sDb = instance.getWritableDatabase(); + sDb.execSQL("PRAGMA foreign_keys=ON"); + instance.createTempTables(sDb); + } + + private RepoDb(Context context) { + super(context, getDbPath(context), null, RepoDbDefinitions.DATABASE_VERSION); + this.context = context; + } + + private static String getDbPath(Context context) { + if (Build.VERSION.SDK_INT >= 21) { + return new File(context.getNoBackupFilesDir(), RepoDbDefinitions.DATABASE_NAME).getPath(); + } else { + return RepoDbDefinitions.DATABASE_NAME; + } + } + + public static void beginTransation() { + sDb.beginTransaction(); + } + + public static void setTransactionSuccessful() { + sDb.setTransactionSuccessful(); + } + + public static void endTransation() { + sDb.endTransaction(); + } + + private static String getString(String table, String searchColumn, String searchValue, String resultColumn) { + String[] projection = new String[]{resultColumn}; + String where = searchColumn + " = ?"; + String[] whereArgs = new String[]{searchValue}; + Cursor c = sDb.query(table, projection, where, whereArgs, null, null, null, "1"); + if (c.moveToFirst()) { + String result = c.getString(c.getColumnIndexOrThrow(resultColumn)); + c.close(); + return result; + } else { + c.close(); + throw new RowNotFoundException("Could not find " + table + "." + searchColumn + " with value '" + searchValue + "'"); + } + } + + public static long insertRepository(String url) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.URL, url); + return sDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values); + } + + public static void deleteRepositories() { + if (sDb != null) + sDb.delete(RepositoriesColumns.TABLE_NAME, null, null); + } + + public static Map getRepositories() { + Map result = new LinkedHashMap(1); + + String[] projection = new String[]{ + RepositoriesColumns._ID, + RepositoriesColumns.URL, + RepositoriesColumns.TITLE, + RepositoriesColumns.PARTIAL_URL, + RepositoriesColumns.VERSION, + }; + + Cursor c = sDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID); + while (c.moveToNext()) { + Repository repo = new Repository(); + long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID)); + repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL)); + repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE)); + repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL)); + repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION)); + result.put(id, repo); + } + c.close(); + + return result; + } + + public static void updateRepository(long repoId, Repository repository) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.TITLE, repository.name); + values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl); + values.put(RepositoriesColumns.VERSION, repository.version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void updateRepositoryVersion(long repoId, String version) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.VERSION, version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static long insertModule(long repoId, Module mod) { + ContentValues values = new ContentValues(); + values.put(ModulesColumns.REPO_ID, repoId); + values.put(ModulesColumns.PKGNAME, mod.packageName); + values.put(ModulesColumns.TITLE, mod.name); + values.put(ModulesColumns.SUMMARY, mod.summary); + values.put(ModulesColumns.DESCRIPTION, mod.description); + values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml); + values.put(ModulesColumns.AUTHOR, mod.author); + values.put(ModulesColumns.SUPPORT, mod.support); + values.put(ModulesColumns.CREATED, mod.created); + values.put(ModulesColumns.UPDATED, mod.updated); + + ModuleVersion latestVersion = RepoLoader.getInstance().getLatestVersion(mod); + + sDb.beginTransaction(); + try { + long moduleId = sDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values); + + long latestVersionId = -1; + for (ModuleVersion version : mod.versions) { + long versionId = insertModuleVersion(moduleId, version); + if (latestVersion == version) + latestVersionId = versionId; + } + + if (latestVersionId > -1) { + values = new ContentValues(); + values.put(ModulesColumns.LATEST_VERSION, latestVersionId); + sDb.update(ModulesColumns.TABLE_NAME, values, ModulesColumns._ID + " = ?", new String[]{Long.toString(moduleId)}); + } + + for (Pair moreInfoEntry : mod.moreInfo) { + insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second); + } + + // TODO Add mod.screenshots + + sDb.setTransactionSuccessful(); + return moduleId; + + } finally { + sDb.endTransaction(); + } + } + + private static long insertModuleVersion(long moduleId, ModuleVersion version) { + ContentValues values = new ContentValues(); + values.put(ModuleVersionsColumns.MODULE_ID, moduleId); + values.put(ModuleVersionsColumns.NAME, version.name); + values.put(ModuleVersionsColumns.CODE, version.code); + values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink); + values.put(ModuleVersionsColumns.MD5SUM, version.md5sum); + values.put(ModuleVersionsColumns.CHANGELOG, version.changelog); + values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml); + values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal()); + values.put(ModuleVersionsColumns.UPLOADED, version.uploaded); + return sDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null, + values); + } + + private static long insertMoreInfo(long moduleId, String title, String value) { + ContentValues values = new ContentValues(); + values.put(MoreInfoColumns.MODULE_ID, moduleId); + values.put(MoreInfoColumns.LABEL, title); + values.put(MoreInfoColumns.VALUE, value); + return sDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values); + } + + public static void deleteAllModules(long repoId) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void deleteModule(long repoId, String packageName) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", new String[]{Long.toString(repoId), packageName}); + } + + public static Module getModuleByPackageName(String packageName) { + // The module itself + String[] projection = new String[]{ + ModulesColumns._ID, + ModulesColumns.REPO_ID, + ModulesColumns.PKGNAME, + ModulesColumns.TITLE, + ModulesColumns.SUMMARY, + ModulesColumns.DESCRIPTION, + ModulesColumns.DESCRIPTION_IS_HTML, + ModulesColumns.AUTHOR, + ModulesColumns.SUPPORT, + ModulesColumns.CREATED, + ModulesColumns.UPDATED, + }; + + String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?"; + String[] whereArgs = new String[]{packageName}; + + Cursor c = sDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1"); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID)); + long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID)); + + Module mod = new Module(RepoLoader.getInstance().getRepository(repoId)); + mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME)); + mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE)); + mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY)); + mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION)); + mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0; + mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR)); + mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT)); + mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED)); + mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED)); + + c.close(); + + // Versions + projection = new String[]{ + ModuleVersionsColumns.NAME, + ModuleVersionsColumns.CODE, ModuleVersionsColumns.DOWNLOAD_LINK, + ModuleVersionsColumns.MD5SUM, ModuleVersionsColumns.CHANGELOG, + ModuleVersionsColumns.CHANGELOG_IS_HTML, + ModuleVersionsColumns.RELTYPE, + ModuleVersionsColumns.UPLOADED, + }; + + where = ModuleVersionsColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null); + while (c.moveToNext()) { + ModuleVersion version = new ModuleVersion(mod); + version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME)); + version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE)); + version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK)); + version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM)); + version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG)); + version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0; + version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE))); + version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED)); + mod.versions.add(version); + } + c.close(); + + // MoreInfo + projection = new String[]{ + MoreInfoColumns.LABEL, + MoreInfoColumns.VALUE, + }; + + where = MoreInfoColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID); + while (c.moveToNext()) { + String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL)); + String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE)); + mod.moreInfo.add(new Pair<>(label, value)); + } + c.close(); + + return mod; + } + + public static String getModuleSupport(String packageName) { + return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT); + } + + public static void updateModuleLatestVersion(String packageName) { + int maxShownReleaseType = RepoLoader.getInstance().getMaxShownReleaseType(packageName).ordinal(); + sDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME + + " SET " + ModulesColumns.LATEST_VERSION + + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " WHERE v." + ModuleVersionsColumns.MODULE_ID + + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID + + " AND reltype <= ? LIMIT 1)" + + " WHERE " + ModulesColumns.PKGNAME + " = ?", + new Object[]{maxShownReleaseType, packageName}); + } + + public static void updateAllModulesLatestVersion() { + sDb.beginTransaction(); + try { + String[] projection = new String[]{ModulesColumns.PKGNAME}; + Cursor c = sDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null); + while (c.moveToNext()) { + updateModuleLatestVersion(c.getString(0)); + } + c.close(); + sDb.setTransactionSuccessful(); + } finally { + sDb.endTransaction(); + } + } + + public static long insertInstalledModule(InstalledModule installed) { + ContentValues values = new ContentValues(); + values.put(InstalledModulesColumns.PKGNAME, installed.packageName); + values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode); + values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName); + return sDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values); + } + + public static void deleteInstalledModule(String packageName) { + sDb.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName}); + } + + public static void deleteAllInstalledModules() { + sDb.delete(InstalledModulesColumns.TABLE_NAME, null, null); + } + + public static Cursor queryModuleOverview(int sortingOrder, + CharSequence filterText) { + // Columns + String[] projection = new String[]{ + "m." + ModulesColumns._ID, + "m." + ModulesColumns.PKGNAME, + "m." + ModulesColumns.TITLE, + "m." + ModulesColumns.SUMMARY, + "m." + ModulesColumns.CREATED, + "m." + ModulesColumns.UPDATED, + + "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION, + "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION, + + "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName() + + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK, + + "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL" + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED, + + "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE, + }; + + // Conditions + String where = ModulesColumns.PREFERRED + " = 1"; + String whereArgs[] = null; + if (!TextUtils.isEmpty(filterText)) { + where += " AND (m." + ModulesColumns.TITLE + " LIKE ?" + + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" + + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" + + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)"; + String filterTextArg = "%" + filterText + "%"; + whereArgs = new String[]{filterTextArg, filterTextArg, filterTextArg, filterTextArg}; + } else { + SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID + "_preferences", MODE_PRIVATE); + + if (prefs.getBoolean("ignore_chinese", false)) { + for (char ch : "的一是不了人我在有他这为中设微模块淘".toCharArray()) { + where += " AND NOT (m." + ModulesColumns.TITLE + " LIKE '%" + ch + "%'" + + " OR m." + ModulesColumns.SUMMARY + " LIKE '%" + ch + "%'" + + " OR m." + ModulesColumns.DESCRIPTION + " LIKE '%" + ch + "%')"; + } + } + } + + // Sorting order + StringBuilder sbOrder = new StringBuilder(); + if (sortingOrder == SORT_CREATED) { + sbOrder.append(OverviewColumns.CREATED); + sbOrder.append(" DESC,"); + } else if (sortingOrder == SORT_UPDATED) { + sbOrder.append(OverviewColumns.UPDATED); + sbOrder.append(" DESC,"); + } + sbOrder.append(OverviewColumns.IS_FRAMEWORK); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.HAS_UPDATE); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.IS_INSTALLED); + sbOrder.append(" DESC, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.TITLE); + sbOrder.append(" COLLATE NOCASE, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.PKGNAME); + + // Query + Cursor c = sDb.query( + ModulesColumns.TABLE_NAME + " AS m" + + " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + + " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i" + + " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME, + projection, where, whereArgs, null, null, sbOrder.toString()); + + // Cache column indexes + OverviewColumnsIndexes.fillFromCursor(c); + + return c; + } + + public static String getFrameworkUpdateVersion() { + return getFirstUpdate(true); + } + + public static boolean hasModuleUpdates() { + return getFirstUpdate(false) != null; + } + + private static String getFirstUpdate(boolean framework) { + String[] projection = new String[]{InstalledModulesUpdatesColumns.LATEST_NAME}; + String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?"); + String[] whereArgs = new String[]{ModuleUtil.getInstance().getFrameworkPackageName()}; + Cursor c = sDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1"); + String latestVersion = null; + if (c.moveToFirst()) + latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME)); + c.close(); + return latestVersion; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS); + db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO); + + RepoLoader.getInstance().clear(false); + } + + private void createTempTables(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This is only a cache, so simply drop & recreate the tables + db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME); + + db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME); + db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME); + + onCreate(db); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + public static class RowNotFoundException extends RuntimeException { + private static final long serialVersionUID = -396324186622439535L; + + public RowNotFoundException(String reason) { + super(reason); + } + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java index 9ecf99235..ab9086d20 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoDbDefinitions.java @@ -4,215 +4,213 @@ import android.provider.BaseColumns; public class RepoDbDefinitions { - public static final int DATABASE_VERSION = 4; - public static final String DATABASE_NAME = "repo_cache.db"; - - -////////////////////////////////////////////////////////////////////////// - public static interface RepositoriesColumns extends BaseColumns { - public static final String TABLE_NAME = "repositories"; - - public static final String URL = "url"; - public static final String TITLE = "title"; - public static final String PARTIAL_URL = "partial_url"; - public static final String VERSION = "version"; - } - static final String SQL_CREATE_TABLE_REPOSITORIES = - "CREATE TABLE " + RepositoriesColumns.TABLE_NAME + " (" + - RepositoriesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - RepositoriesColumns.URL + " TEXT NOT NULL, " + - RepositoriesColumns.TITLE + " TEXT, " + - RepositoriesColumns.PARTIAL_URL + " TEXT, " + - RepositoriesColumns.VERSION + " TEXT, " + - "UNIQUE (" + RepositoriesColumns.URL + ") ON CONFLICT REPLACE)"; - - -////////////////////////////////////////////////////////////////////////// - public static interface ModulesColumns extends BaseColumns { - public static final String TABLE_NAME = "modules"; - - public static final String REPO_ID = "repo_id"; - public static final String PKGNAME = "pkgname"; - public static final String TITLE = "title"; - public static final String SUMMARY = "summary"; - public static final String DESCRIPTION = "description"; - public static final String DESCRIPTION_IS_HTML = "description_is_html"; - public static final String AUTHOR = "author"; - public static final String SUPPORT = "support"; - public static final String CREATED = "created"; - public static final String UPDATED = "updated"; - - public static final String PREFERRED = "preferred"; - public static final String LATEST_VERSION = "latest_version_id"; - } - static final String SQL_CREATE_TABLE_MODULES = - "CREATE TABLE " + ModulesColumns.TABLE_NAME + " (" + - ModulesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - - ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES " - + RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, " + - ModulesColumns.PKGNAME + " TEXT NOT NULL, " + - ModulesColumns.TITLE + " TEXT NOT NULL, " + - ModulesColumns.SUMMARY + " TEXT, " + - ModulesColumns.DESCRIPTION + " TEXT, " + - ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, " + - ModulesColumns.AUTHOR + " TEXT, " + - ModulesColumns.SUPPORT + " TEXT, " + - ModulesColumns.CREATED + " INTEGER DEFAULT -1, " + - ModulesColumns.UPDATED + " INTEGER DEFAULT -1, " + - ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, " + - ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES " + ModuleVersionsColumns.TABLE_NAME + ", " + - "UNIQUE (" + ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID + ") ON CONFLICT REPLACE)"; - - -////////////////////////////////////////////////////////////////////////// - public static interface ModuleVersionsColumns extends BaseColumns { - public static final String TABLE_NAME = "module_versions"; - public static final String IDX_MODULE_ID = "module_versions_module_id_idx"; - - public static final String MODULE_ID = "module_id"; - public static final String NAME = "name"; - public static final String CODE = "code"; - public static final String DOWNLOAD_LINK = "download_link"; - public static final String MD5SUM = "md5sum"; - public static final String CHANGELOG = "changelog"; - public static final String CHANGELOG_IS_HTML = "changelog_is_html"; - public static final String RELTYPE = "reltype"; - public static final String UPLOADED = "uploaded"; - } - static final String SQL_CREATE_TABLE_MODULE_VERSIONS = - "CREATE TABLE " + ModuleVersionsColumns.TABLE_NAME + " (" + - ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " - + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + - ModuleVersionsColumns.NAME + " TEXT NOT NULL, " + - ModuleVersionsColumns.CODE + " INTEGER NOT NULL, " + - ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, " + - ModuleVersionsColumns.MD5SUM + " TEXT, " + - ModuleVersionsColumns.CHANGELOG + " TEXT, " + - ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, " + - ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, " + - ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)"; - static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = - "CREATE INDEX " + ModuleVersionsColumns.IDX_MODULE_ID + " ON " + - ModuleVersionsColumns.TABLE_NAME + " (" + - ModuleVersionsColumns.MODULE_ID + ")"; - - -////////////////////////////////////////////////////////////////////////// - public static interface MoreInfoColumns extends BaseColumns { - public static final String TABLE_NAME = "more_info"; - - public static final String MODULE_ID = "module_id"; - public static final String LABEL = "label"; - public static final String VALUE = "value"; - } - static final String SQL_CREATE_TABLE_MORE_INFO = - "CREATE TABLE " + MoreInfoColumns.TABLE_NAME + " (" + - MoreInfoColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - MoreInfoColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " - + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + - MoreInfoColumns.LABEL + " TEXT NOT NULL, " + - MoreInfoColumns.VALUE + " TEXT)"; - - -////////////////////////////////////////////////////////////////////////// - public static interface InstalledModulesColumns { - public static final String TABLE_NAME = "installed_modules"; - - public static final String PKGNAME = "pkgname"; - public static final String VERSION_CODE = "version_code"; - public static final String VERSION_NAME = "version_name"; - } - static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = - "CREATE TEMP TABLE " + InstalledModulesColumns.TABLE_NAME + " (" + - InstalledModulesColumns.PKGNAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE, " + - InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, " + - InstalledModulesColumns.VERSION_NAME + " TEXT)"; - - -////////////////////////////////////////////////////////////////////////// - public static interface InstalledModulesUpdatesColumns { - public static final String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates"; - - public static final String MODULE_ID = "module_id"; - public static final String PKGNAME = "pkgname"; - public static final String INSTALLED_CODE = "installed_code"; - public static final String INSTALLED_NAME = "installed_name"; - public static final String LATEST_ID = "latest_id"; - public static final String LATEST_CODE = "latest_code"; - public static final String LATEST_NAME = "latest_name"; - } - static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = - "CREATE TEMP VIEW " + InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + - "m." + ModulesColumns._ID + " AS " + InstalledModulesUpdatesColumns.MODULE_ID + ", " + - "i." + InstalledModulesColumns.PKGNAME + " AS " + InstalledModulesUpdatesColumns.PKGNAME + ", " + - "i." + InstalledModulesColumns.VERSION_CODE + " AS " + InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + - "i." + InstalledModulesColumns.VERSION_NAME + " AS " + InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + - "v." + ModuleVersionsColumns._ID + " AS " + InstalledModulesUpdatesColumns.LATEST_ID + ", " + - "v." + ModuleVersionsColumns.CODE + " AS " + InstalledModulesUpdatesColumns.LATEST_CODE + ", " + - "v." + ModuleVersionsColumns.NAME + " AS " + InstalledModulesUpdatesColumns.LATEST_NAME + - " FROM " + InstalledModulesColumns.TABLE_NAME + " AS i" + - " INNER JOIN " + ModulesColumns.TABLE_NAME + " AS m" + - " ON m." + ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME + - " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + - " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + - " WHERE " + InstalledModulesUpdatesColumns.LATEST_CODE - + " > " + InstalledModulesUpdatesColumns.INSTALLED_CODE - + " AND " + ModulesColumns.PREFERRED + " = 1"; - - -////////////////////////////////////////////////////////////////////////// - public interface OverviewColumns extends BaseColumns { - public static final String PKGNAME = ModulesColumns.PKGNAME; - public static final String TITLE = ModulesColumns.TITLE; - public static final String SUMMARY = ModulesColumns.SUMMARY; - public static final String CREATED = ModulesColumns.CREATED; - public static final String UPDATED = ModulesColumns.UPDATED; - - public static final String INSTALLED_VERSION = "installed_version"; - public static final String LATEST_VERSION = "latest_version"; - - public static final String IS_FRAMEWORK = "is_framework"; - public static final String IS_INSTALLED = "is_installed"; - public static final String HAS_UPDATE = "has_update"; - } - - public static class OverviewColumnsIndexes { - private OverviewColumnsIndexes() {} - - public static int PKGNAME = -1; - public static int TITLE = -1; - public static int SUMMARY = -1; - public static int CREATED = -1; - public static int UPDATED = -1; - - public static int INSTALLED_VERSION = -1; - public static int LATEST_VERSION = -1; - - public static int IS_FRAMEWORK = -1; - public static int IS_INSTALLED = -1; - public static int HAS_UPDATE = -1; - - private static boolean isFilled = false; - - public static void fillFromCursor(Cursor cursor) { - if (isFilled || cursor == null) - return; - - PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME); - TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE); - SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY); - CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED); - UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED); - INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); - LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION); - INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); - IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK); - IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED); - HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE); - - isFilled = true; - } - } -} + public static final int DATABASE_VERSION = 4; + public static final String DATABASE_NAME = "repo_cache.db"; + static final String SQL_CREATE_TABLE_REPOSITORIES = "CREATE TABLE " + + RepositoriesColumns.TABLE_NAME + " (" + RepositoriesColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + RepositoriesColumns.URL + + " TEXT NOT NULL, " + RepositoriesColumns.TITLE + " TEXT, " + + RepositoriesColumns.PARTIAL_URL + " TEXT, " + + RepositoriesColumns.VERSION + " TEXT, " + "UNIQUE (" + + RepositoriesColumns.URL + ") ON CONFLICT REPLACE)"; + static final String SQL_CREATE_TABLE_MODULES = "CREATE TABLE " + + ModulesColumns.TABLE_NAME + " (" + ModulesColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + + ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES " + + RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModulesColumns.PKGNAME + " TEXT NOT NULL, " + ModulesColumns.TITLE + + " TEXT NOT NULL, " + ModulesColumns.SUMMARY + " TEXT, " + + ModulesColumns.DESCRIPTION + " TEXT, " + + ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, " + + ModulesColumns.AUTHOR + " TEXT, " + ModulesColumns.SUPPORT + + " TEXT, " + ModulesColumns.CREATED + " INTEGER DEFAULT -1, " + + ModulesColumns.UPDATED + " INTEGER DEFAULT -1, " + + ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, " + + ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES " + + ModuleVersionsColumns.TABLE_NAME + ", " + "UNIQUE (" + + ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID + + ") ON CONFLICT REPLACE)"; + static final String SQL_CREATE_TABLE_MODULE_VERSIONS = "CREATE TABLE " + + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " + + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModuleVersionsColumns.NAME + " TEXT NOT NULL, " + + ModuleVersionsColumns.CODE + " INTEGER NOT NULL, " + + ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, " + + ModuleVersionsColumns.MD5SUM + " TEXT, " + + ModuleVersionsColumns.CHANGELOG + " TEXT, " + + ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)"; + static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = "CREATE INDEX " + + ModuleVersionsColumns.IDX_MODULE_ID + " ON " + + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns.MODULE_ID + ")"; + static final String SQL_CREATE_TABLE_MORE_INFO = "CREATE TABLE " + + MoreInfoColumns.TABLE_NAME + " (" + MoreInfoColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + MoreInfoColumns.MODULE_ID + + " INTEGER NOT NULL REFERENCES " + ModulesColumns.TABLE_NAME + + " ON DELETE CASCADE, " + MoreInfoColumns.LABEL + + " TEXT NOT NULL, " + MoreInfoColumns.VALUE + " TEXT)"; + static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = "CREATE TEMP TABLE " + + InstalledModulesColumns.TABLE_NAME + " (" + + InstalledModulesColumns.PKGNAME + + " TEXT PRIMARY KEY ON CONFLICT REPLACE, " + + InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, " + + InstalledModulesColumns.VERSION_NAME + " TEXT)"; + static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = "CREATE TEMP VIEW " + + InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + "m." + + ModulesColumns._ID + " AS " + + InstalledModulesUpdatesColumns.MODULE_ID + ", " + "i." + + InstalledModulesColumns.PKGNAME + " AS " + + InstalledModulesUpdatesColumns.PKGNAME + ", " + "i." + + InstalledModulesColumns.VERSION_CODE + " AS " + + InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + "i." + + InstalledModulesColumns.VERSION_NAME + " AS " + + InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + "v." + + ModuleVersionsColumns._ID + " AS " + + InstalledModulesUpdatesColumns.LATEST_ID + ", " + "v." + + ModuleVersionsColumns.CODE + " AS " + + InstalledModulesUpdatesColumns.LATEST_CODE + ", " + "v." + + ModuleVersionsColumns.NAME + " AS " + + InstalledModulesUpdatesColumns.LATEST_NAME + " FROM " + + InstalledModulesColumns.TABLE_NAME + " AS i" + " INNER JOIN " + + ModulesColumns.TABLE_NAME + " AS m" + " ON m." + + ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME + + " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + + ModulesColumns.LATEST_VERSION + " WHERE " + + InstalledModulesUpdatesColumns.LATEST_CODE + " > " + + InstalledModulesUpdatesColumns.INSTALLED_CODE + " AND " + + ModulesColumns.PREFERRED + " = 1"; + + ////////////////////////////////////////////////////////////////////////// + public interface RepositoriesColumns extends BaseColumns { + String TABLE_NAME = "repositories"; + + String URL = "url"; + String TITLE = "title"; + String PARTIAL_URL = "partial_url"; + String VERSION = "version"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface ModulesColumns extends BaseColumns { + String TABLE_NAME = "modules"; + + String REPO_ID = "repo_id"; + String PKGNAME = "pkgname"; + String TITLE = "title"; + String SUMMARY = "summary"; + String DESCRIPTION = "description"; + String DESCRIPTION_IS_HTML = "description_is_html"; + String AUTHOR = "author"; + String SUPPORT = "support"; + String CREATED = "created"; + String UPDATED = "updated"; + + String PREFERRED = "preferred"; + String LATEST_VERSION = "latest_version_id"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface ModuleVersionsColumns extends BaseColumns { + String TABLE_NAME = "module_versions"; + String IDX_MODULE_ID = "module_versions_module_id_idx"; + + String MODULE_ID = "module_id"; + String NAME = "name"; + String CODE = "code"; + String DOWNLOAD_LINK = "download_link"; + String MD5SUM = "md5sum"; + String CHANGELOG = "changelog"; + String CHANGELOG_IS_HTML = "changelog_is_html"; + String RELTYPE = "reltype"; + String UPLOADED = "uploaded"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface MoreInfoColumns extends BaseColumns { + String TABLE_NAME = "more_info"; + + String MODULE_ID = "module_id"; + String LABEL = "label"; + String VALUE = "value"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface InstalledModulesColumns { + String TABLE_NAME = "installed_modules"; + + String PKGNAME = "pkgname"; + String VERSION_CODE = "version_code"; + String VERSION_NAME = "version_name"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface InstalledModulesUpdatesColumns { + String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates"; + + String MODULE_ID = "module_id"; + String PKGNAME = "pkgname"; + String INSTALLED_CODE = "installed_code"; + String INSTALLED_NAME = "installed_name"; + String LATEST_ID = "latest_id"; + String LATEST_CODE = "latest_code"; + String LATEST_NAME = "latest_name"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface OverviewColumns extends BaseColumns { + String PKGNAME = ModulesColumns.PKGNAME; + String TITLE = ModulesColumns.TITLE; + String SUMMARY = ModulesColumns.SUMMARY; + String CREATED = ModulesColumns.CREATED; + String UPDATED = ModulesColumns.UPDATED; + + String INSTALLED_VERSION = "installed_version"; + String LATEST_VERSION = "latest_version"; + + String IS_FRAMEWORK = "is_framework"; + String IS_INSTALLED = "is_installed"; + String HAS_UPDATE = "has_update"; + } + + public static class OverviewColumnsIndexes { + public static int PKGNAME = -1; + public static int TITLE = -1; + public static int SUMMARY = -1; + public static int CREATED = -1; + public static int UPDATED = -1; + public static int INSTALLED_VERSION = -1; + public static int LATEST_VERSION = -1; + public static int IS_FRAMEWORK = -1; + public static int IS_INSTALLED = -1; + public static int HAS_UPDATE = -1; + private static boolean isFilled = false; + + private OverviewColumnsIndexes() { + } + + public static void fillFromCursor(Cursor cursor) { + if (isFilled || cursor == null) + return; + + PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME); + TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE); + SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY); + CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED); + UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK); + IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED); + HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE); + + isFilled = true; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java index 3447faf49..31eabf4af 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/RepoParser.java @@ -1,247 +1,341 @@ package de.robv.android.xposed.installer.repo; -import java.io.IOException; -import java.io.InputStream; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.os.AsyncTask; import android.text.Html; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.util.Log; import android.util.Pair; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStream; + +import de.robv.android.xposed.installer.R; public class RepoParser { - public final static String TAG = "XposedRepoParser"; - protected final static String NS = null; - protected final XmlPullParser parser; - protected RepoParserCallback mCallback; - private boolean mRepoEventTriggered = false; - - public interface RepoParserCallback { - public void onRepositoryMetadata(Repository repository); - public void onNewModule(Module module); - public void onRemoveModule(String packageName); - public void onCompleted(Repository repository); - } - - public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { - new RepoParser(is, callback).readRepo(); - } - - protected RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - parser = factory.newPullParser(); - parser.setInput(is, null); - parser.nextTag(); - mCallback = callback; - } - - protected void readRepo() throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, NS, "repository"); - Repository repository = new Repository(); - repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial")); - repository.partialUrl = parser.getAttributeValue(NS, "partial-url"); - repository.version = parser.getAttributeValue(NS, "version"); - - while (parser.nextTag() == XmlPullParser.START_TAG) { - String tagName = parser.getName(); - if (tagName.equals("name")) { - repository.name = parser.nextText(); - } else if (tagName.equals("module")) { - triggerRepoEvent(repository); - Module module = readModule(repository); - if (module != null) - mCallback.onNewModule(module); - } else if (tagName.equals("remove-module")) { - triggerRepoEvent(repository); - String packageName = readRemoveModule(); - if (packageName != null) - mCallback.onRemoveModule(packageName); - } else { - skip(true); - } - } - - mCallback.onCompleted(repository); - } - - private void triggerRepoEvent(Repository repository) { - if (mRepoEventTriggered) - return; - - mCallback.onRepositoryMetadata(repository); - mRepoEventTriggered = true; - } - - protected Module readModule(Repository repository) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, NS, "module"); - final int startDepth = parser.getDepth(); - - Module module = new Module(repository); - module.packageName = parser.getAttributeValue(NS, "package"); - if (module.packageName == null) { - logError("no package name defined"); - leave(startDepth); - return null; - } - - module.created = parseTimestamp("created"); - module.updated = parseTimestamp("updated"); - - while (parser.nextTag() == XmlPullParser.START_TAG) { - String tagName = parser.getName(); - if (tagName.equals("name")) { - module.name = parser.nextText(); - } else if (tagName.equals("author")) { - module.author = parser.nextText(); - } else if (tagName.equals("summary")) { - module.summary = parser.nextText(); - } else if (tagName.equals("description")) { - String isHtml = parser.getAttributeValue(NS, "html"); - if (isHtml != null && isHtml.equals("true")) - module.descriptionIsHtml = true; - module.description = parser.nextText(); - } else if (tagName.equals("screenshot")) { - module.screenshots.add(parser.nextText()); - } else if (tagName.equals("moreinfo")) { - String label = parser.getAttributeValue(NS, "label"); - String role = parser.getAttributeValue(NS, "role"); - String value = parser.nextText(); - module.moreInfo.add(new Pair(label, value)); - - if (role != null && role.contains("support")) - module.support = value; - } else if (tagName.equals("version")) { - ModuleVersion version = readModuleVersion(module); - if (version != null) - module.versions.add(version); - } else { - skip(true); - } - } - - if (module.name == null) { - logError("packages need at least a name"); - return null; - } - - return module; - } - - private long parseTimestamp(String attName) { - String value = parser.getAttributeValue(NS, attName); - if (value == null) - return -1; - try { - return Long.parseLong(value) * 1000L; - } catch (NumberFormatException ex) { - return -1; - } - } - - public static Spanned parseSimpleHtml(String source) { - source = source.replaceAll("
  • ", "\t\u0095 "); - source = source.replaceAll("
  • ", "
    "); - Spanned html = Html.fromHtml(source); - - // trim trailing newlines - int len = html.length(); - int end = len; - for (int i = len - 1; i >= 0; i--) { - if (html.charAt(i) != '\n') - break; - end = i; - } - - if (end == len) - return html; - else - return new SpannableStringBuilder(html, 0, end); - } - - protected ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, NS, "version"); - final int startDepth = parser.getDepth(); - ModuleVersion version = new ModuleVersion(module); - - version.uploaded = parseTimestamp("uploaded"); - - while (parser.nextTag() == XmlPullParser.START_TAG) { - String tagName = parser.getName(); - if (tagName.equals("name")) { - version.name = parser.nextText(); - } else if (tagName.equals("code")) { - try { - version.code = Integer.parseInt(parser.nextText()); - } catch (NumberFormatException nfe) { - logError(nfe.getMessage()); - leave(startDepth); - return null; - } - } else if (tagName.equals("reltype")) { - version.relType = ReleaseType.fromString(parser.nextText()); - } else if (tagName.equals("download")) { - version.downloadLink = parser.nextText(); - } else if (tagName.equals("md5sum")) { - version.md5sum = parser.nextText(); - } else if (tagName.equals("changelog")) { - String isHtml = parser.getAttributeValue(NS, "html"); - if (isHtml != null && isHtml.equals("true")) - version.changelogIsHtml = true; - version.changelog = parser.nextText(); - } else if (tagName.equals("branch")) { - // obsolete - skip(false); - } else { - skip(true); - } - } - - return version; - } - - protected String readRemoveModule() throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, NS, "remove-module"); - final int startDepth = parser.getDepth(); - - String packageName = parser.getAttributeValue(NS, "package"); - if (packageName == null) { - logError("no package name defined"); - leave(startDepth); - return null; - } - - return packageName; - } - - protected void skip(boolean showWarning) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, null, null); - if (showWarning) - Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription()); - int level = 1; - while (level > 0) { - int eventType = parser.next(); - if (eventType == XmlPullParser.END_TAG) { - level--; - } else if (eventType == XmlPullParser.START_TAG) { - level++; - } - } - } - - protected void leave(int targetDepth) throws XmlPullParserException, IOException { - Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription()); - while (parser.getDepth() > targetDepth) { - while (parser.next() != XmlPullParser.END_TAG) { - // do nothing - } - } - } - - protected void logError(String error) { - Log.e(TAG, parser.getPositionDescription() + ": " + error); - } -} + public final static String TAG = "XposedRepoParser"; + protected final static String NS = null; + protected final XmlPullParser parser; + protected RepoParserCallback mCallback; + private boolean mRepoEventTriggered = false; + + protected RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + parser = factory.newPullParser(); + parser.setInput(is, null); + parser.nextTag(); + mCallback = callback; + } + + public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + new RepoParser(is, callback).readRepo(); + } + + public static Spanned parseSimpleHtml(final Context c, String source, final TextView textView) { + source = source.replaceAll("
  • ", "\t\u0095 "); + source = source.replaceAll("
  • ", "
    "); + Spanned html = Html.fromHtml(source, new Html.ImageGetter() { + @Override + public Drawable getDrawable(String source) { + LevelListDrawable d = new LevelListDrawable(); + @SuppressWarnings("deprecation") + Drawable empty = c.getResources().getDrawable(R.drawable.ic_no_image); + d.addLevel(0, 0, empty); + assert empty != null; + d.setBounds(0, 0, empty.getIntrinsicWidth(), empty.getIntrinsicHeight()); + new ImageGetterAsyncTask(c, source, d).execute(textView); + + return d; + } + }, null); + + // trim trailing newlines + int len = html.length(); + int end = len; + for (int i = len - 1; i >= 0; i--) { + if (html.charAt(i) != '\n') + break; + end = i; + } + + if (end == len) + return html; + else + return new SpannableStringBuilder(html, 0, end); + } + + protected void readRepo() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "repository"); + Repository repository = new Repository(); + repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial")); + repository.partialUrl = parser.getAttributeValue(NS, "partial-url"); + repository.version = parser.getAttributeValue(NS, "version"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + repository.name = parser.nextText(); + break; + case "module": + triggerRepoEvent(repository); + Module module = readModule(repository); + if (module != null) + mCallback.onNewModule(module); + break; + case "remove-module": + triggerRepoEvent(repository); + String packageName = readRemoveModule(); + if (packageName != null) + mCallback.onRemoveModule(packageName); + break; + default: + skip(true); + break; + } + } + + mCallback.onCompleted(repository); + } + + private void triggerRepoEvent(Repository repository) { + if (mRepoEventTriggered) + return; + + mCallback.onRepositoryMetadata(repository); + mRepoEventTriggered = true; + } + + protected Module readModule(Repository repository) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "module"); + final int startDepth = parser.getDepth(); + + Module module = new Module(repository); + module.packageName = parser.getAttributeValue(NS, "package"); + if (module.packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + module.created = parseTimestamp("created"); + module.updated = parseTimestamp("updated"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + module.name = parser.nextText(); + break; + case "author": + module.author = parser.nextText(); + break; + case "summary": + module.summary = parser.nextText(); + break; + case "description": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + module.descriptionIsHtml = true; + module.description = parser.nextText(); + break; + case "screenshot": + module.screenshots.add(parser.nextText()); + break; + case "moreinfo": + String label = parser.getAttributeValue(NS, "label"); + String role = parser.getAttributeValue(NS, "role"); + String value = parser.nextText(); + module.moreInfo.add(new Pair<>(label, value)); + + if (role != null && role.contains("support")) + module.support = value; + break; + case "version": + ModuleVersion version = readModuleVersion(module); + if (version != null) + module.versions.add(version); + break; + default: + skip(true); + break; + } + } + + if (module.name == null) { + logError("packages need at least a name"); + return null; + } + + return module; + } + + private long parseTimestamp(String attName) { + String value = parser.getAttributeValue(NS, attName); + if (value == null) + return -1; + try { + return Long.parseLong(value) * 1000L; + } catch (NumberFormatException ex) { + return -1; + } + } + + protected ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "version"); + final int startDepth = parser.getDepth(); + ModuleVersion version = new ModuleVersion(module); + + version.uploaded = parseTimestamp("uploaded"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + version.name = parser.nextText(); + break; + case "code": + try { + version.code = Integer.parseInt(parser.nextText()); + } catch (NumberFormatException nfe) { + logError(nfe.getMessage()); + leave(startDepth); + return null; + } + break; + case "reltype": + version.relType = ReleaseType.fromString(parser.nextText()); + break; + case "download": + version.downloadLink = parser.nextText(); + break; + case "md5sum": + version.md5sum = parser.nextText(); + break; + case "changelog": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + version.changelogIsHtml = true; + version.changelog = parser.nextText(); + break; + case "branch": + // obsolete + skip(false); + break; + default: + skip(true); + break; + } + } + + return version; + } + + protected String readRemoveModule() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "remove-module"); + final int startDepth = parser.getDepth(); + + String packageName = parser.getAttributeValue(NS, "package"); + if (packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + return packageName; + } + + protected void skip(boolean showWarning) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, null, null); + if (showWarning) + Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription()); + int level = 1; + while (level > 0) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_TAG) { + level--; + } else if (eventType == XmlPullParser.START_TAG) { + level++; + } + } + } + + protected void leave(int targetDepth) throws XmlPullParserException, IOException { + Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription()); + while (parser.getDepth() > targetDepth) { + //noinspection StatementWithEmptyBody + while (parser.next() != XmlPullParser.END_TAG) { + // do nothing + } + } + } + + protected void logError(String error) { + Log.e(TAG, parser.getPositionDescription() + ": " + error); + } + + public interface RepoParserCallback { + void onRepositoryMetadata(Repository repository); + + void onNewModule(Module module); + + void onRemoveModule(String packageName); + + void onCompleted(Repository repository); + } + + static class ImageGetterAsyncTask extends AsyncTask { + + private LevelListDrawable levelListDrawable; + private Context context; + private String source; + private TextView t; + + public ImageGetterAsyncTask(Context context, String source, LevelListDrawable levelListDrawable) { + this.context = context; + this.source = source; + this.levelListDrawable = levelListDrawable; + } + + @Override + protected Bitmap doInBackground(TextView... params) { + t = params[0]; + try { + return Picasso.with(context).load(source).get(); + } catch (Exception e) { + return null; + } + } + + @Override + protected void onPostExecute(final Bitmap bitmap) { + try { + Drawable d = new BitmapDrawable(context.getResources(), bitmap); + Point size = new Point(); + ((Activity) context).getWindowManager().getDefaultDisplay().getSize(size); + int multiplier = size.x / bitmap.getWidth(); + if (multiplier <= 0) multiplier = 1; + levelListDrawable.addLevel(1, 1, d); + levelListDrawable.setBounds(0, 0, bitmap.getWidth() * multiplier, bitmap.getHeight() * multiplier); + levelListDrawable.setLevel(1); + t.setText(t.getText()); + } catch (Exception ignored) { /* Like a null bitmap, etc. */ + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java b/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java index a223bdd5c..70cd3e4f8 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java +++ b/app/src/main/java/de/robv/android/xposed/installer/repo/Repository.java @@ -1,12 +1,12 @@ package de.robv.android.xposed.installer.repo; - public class Repository { - public String name; - public String url; - public boolean isPartial = false; - public String partialUrl; - public String version; + public String name; + public String url; + public boolean isPartial = false; + public String partialUrl; + public String version; - /*package*/ Repository() {}; + /* package */ Repository() { + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java index 640dffbf8..c811fab67 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/AssetUtil.java @@ -1,132 +1,91 @@ package de.robv.android.xposed.installer.util; +import android.content.res.AssetManager; +import android.os.Build; +import android.os.FileUtils; +import android.util.Log; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.AssetManager; -import android.os.Build; -import android.os.FileUtils; -import android.util.Log; import de.robv.android.xposed.installer.XposedApp; public class AssetUtil { - public static final File BUSYBOX_FILE = new File(XposedApp.getInstance().getCacheDir(), "busybox-xposed"); - public static final String STATIC_BUSYBOX_PACKAGE = "de.robv.android.xposed.installer.staticbusybox"; - private static final int STATIC_BUSYBOX_REQUIRED_VERSION = 1; - private static PackageInfo mStaticBusyboxInfo = null; - - public static String getBinariesFolder() { - if (Build.CPU_ABI.startsWith("armeabi")) { - return "arm/"; - } else if (Build.CPU_ABI.startsWith("x86")) { - return "x86/"; - } else { - return null; - } - } - - public static File writeAssetToCacheFile(String name, int mode) { - return writeAssetToCacheFile(name, name, mode); - } - - public static File writeAssetToCacheFile(String assetName, String fileName, int mode) { - return writeAssetToFile(assetName, new File(XposedApp.getInstance().getCacheDir(), fileName), mode); - } - - public static File writeAssetToSdcardFile(String name, int mode) { - return writeAssetToSdcardFile(name, name, mode); - } - - public static File writeAssetToSdcardFile(String assetName, String fileName, int mode) { - File dir = XposedApp.getInstance().getExternalFilesDir(null); - return writeAssetToFile(assetName, new File(dir, fileName), mode); - } - - public static File writeAssetToFile(String assetName, File targetFile, int mode) { - return writeAssetToFile(null, assetName, targetFile, mode); - } - - public static File writeAssetToFile(AssetManager assets, String assetName, File targetFile, int mode) { - try { - if (assets == null) - assets = XposedApp.getInstance().getAssets(); - InputStream in = assets.open(assetName); - FileOutputStream out = new FileOutputStream(targetFile); - - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) > 0){ - out.write(buffer, 0, len); - } - in.close(); - out.close(); - - FileUtils.setPermissions(targetFile.getAbsolutePath(), mode, -1, -1); - - return targetFile; - } catch (IOException e) { - Log.e(XposedApp.TAG, "could not extract asset", e); - if (targetFile != null) - targetFile.delete(); - - return null; - } - } - - public synchronized static void extractBusybox() { - if (BUSYBOX_FILE.exists()) - return; - - AssetManager assets = null; - if (isStaticBusyboxAvailable()) { - try { - PackageManager pm = XposedApp.getInstance().getPackageManager(); - assets = pm.getResourcesForApplication(mStaticBusyboxInfo.applicationInfo).getAssets(); - } catch (NameNotFoundException e) { - Log.e(XposedApp.TAG, "could not load assets from " + STATIC_BUSYBOX_PACKAGE, e); - } - } - - writeAssetToFile(assets, getBinariesFolder() + "busybox-xposed", BUSYBOX_FILE, 00700); - } - - public synchronized static void removeBusybox() { - BUSYBOX_FILE.delete(); - } - - public synchronized static void checkStaticBusyboxAvailability() { - boolean wasAvailable = isStaticBusyboxAvailable(); - mStaticBusyboxInfo = null; - - PackageManager pm = XposedApp.getInstance().getPackageManager(); - try { - mStaticBusyboxInfo = pm.getPackageInfo(STATIC_BUSYBOX_PACKAGE, 0); - } catch (NameNotFoundException e) { - return; - } - - String myPackageName = ModuleUtil.getInstance().getFrameworkPackageName(); - if (pm.checkSignatures(STATIC_BUSYBOX_PACKAGE, myPackageName) != PackageManager.SIGNATURE_MATCH) { - Log.e(XposedApp.TAG, "Rejecting static Busybox package because it is signed with a different key"); - return; - } - - if (mStaticBusyboxInfo.versionCode != STATIC_BUSYBOX_REQUIRED_VERSION) { - Log.e(XposedApp.TAG, String.format("Ignoring static BusyBox package with version %d, we need version %d", - mStaticBusyboxInfo.versionCode, STATIC_BUSYBOX_REQUIRED_VERSION)); - mStaticBusyboxInfo = null; - return; - } else if (!wasAvailable) { - Log.i(XposedApp.TAG, "Detected static Busybox package"); - } - } - - public synchronized static boolean isStaticBusyboxAvailable() { - return mStaticBusyboxInfo != null; - } + public static final File BUSYBOX_FILE = new File(XposedApp.getInstance().getCacheDir(), "busybox-xposed"); + + @SuppressWarnings("deprecation") + public static String getBinariesFolder() { + if (Build.CPU_ABI.startsWith("arm")) { + return "arm/"; + } else if (Build.CPU_ABI.startsWith("x86")) { + return "x86/"; + } else { + return null; + } + } + + public static File writeAssetToCacheFile(String name, int mode) { + return writeAssetToCacheFile(name, name, mode); + } + + public static File writeAssetToCacheFile(String assetName, String fileName, int mode) { + return writeAssetToFile(assetName, new File(XposedApp.getInstance().getCacheDir(), fileName), mode); + } + + public static File writeAssetToSdcardFile(String name, int mode) { + return writeAssetToSdcardFile(name, name, mode); + } + + public static File writeAssetToSdcardFile(String assetName, String fileName, int mode) { + File dir = XposedApp.getInstance().getExternalFilesDir(null); + return writeAssetToFile(assetName, new File(dir, fileName), mode); + } + + public static File writeAssetToFile(String assetName, File targetFile, int mode) { + return writeAssetToFile(null, assetName, targetFile, mode); + } + + public static File writeAssetToFile(AssetManager assets, String assetName, File targetFile, int mode) { + try { + if (assets == null) + assets = XposedApp.getInstance().getAssets(); + InputStream in = assets.open(assetName); + writeStreamToFile(in, targetFile, mode); + return targetFile; + } catch (IOException e) { + Log.e(XposedApp.TAG, "AssetUtil -> could not extract asset", e); + if (targetFile != null) + targetFile.delete(); + + return null; + } + } + + public static void writeStreamToFile(InputStream in, File targetFile, int mode) throws IOException { + FileOutputStream out = new FileOutputStream(targetFile); + + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + + FileUtils.setPermissions(targetFile.getAbsolutePath(), mode, -1, -1); + } + + public synchronized static void extractBusybox() { + if (BUSYBOX_FILE.exists()) + return; + + AssetManager assets = null; + writeAssetToFile(assets, getBinariesFolder() + "busybox-xposed", BUSYBOX_FILE, 00700); + } + + public synchronized static void removeBusybox() { + BUSYBOX_FILE.delete(); + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java index ccc421960..0c20926e6 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/DownloadsUtil.java @@ -1,5 +1,27 @@ package de.robv.android.xposed.installer.util; +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.support.v4.content.ContextCompat; +import android.support.v4.os.EnvironmentCompat; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.MaterialDialog.SingleButtonCallback; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -13,328 +35,677 @@ import java.util.List; import java.util.Map; -import android.app.DownloadManager; -import android.app.DownloadManager.Query; -import android.app.DownloadManager.Request; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.repo.Module; +import de.robv.android.xposed.installer.repo.ModuleVersion; +import de.robv.android.xposed.installer.repo.ReleaseType; public class DownloadsUtil { - public static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; - private static final Map mCallbacks = new HashMap(); - private static final XposedApp mApp = XposedApp.getInstance(); - private static final SharedPreferences mPref = mApp.getSharedPreferences("download_cache", Context.MODE_PRIVATE); - - public static DownloadInfo add(Context context, String title, String url, DownloadFinishedCallback callback) { - removeAllForUrl(context, url); - - synchronized (mCallbacks) { - mCallbacks.put(url, callback); - } - - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - Request request = new Request(Uri.parse(url)); - request.setTitle(title); - request.setMimeType(MIME_TYPE_APK); - request.setNotificationVisibility(Request.VISIBILITY_VISIBLE); - long id = dm.enqueue(request); - - return getById(context, id); - } - - public static DownloadInfo getById(Context context, long id) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - Cursor c = dm.query(new Query().setFilterById(id)); - if (!c.moveToFirst()) - return null; - - int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); - int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); - int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); - int columnLastMod = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); - int columnFilename = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME); - int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); - int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); - int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); - int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); - - String localFilename = c.getString(columnFilename); - if (localFilename != null && !localFilename.isEmpty() && !new File(localFilename).isFile()) { - dm.remove(c.getLong(columnId)); - return null; - } - - return new DownloadInfo( - c.getLong(columnId), - c.getString(columnUri), - c.getString(columnTitle), - c.getLong(columnLastMod), - localFilename, - c.getInt(columnStatus), - c.getInt(columnTotalSize), - c.getInt(columnBytesDownloaded), - c.getInt(columnReason) - ); - } - - public static DownloadInfo getLatestForUrl(Context context, String url) { - List all = getAllForUrl(context, url); - return all.isEmpty() ? null : all.get(0); - } - - public static List getAllForUrl(Context context, String url) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - Cursor c = dm.query(new Query()); - int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); - int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); - int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); - int columnLastMod = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); - int columnFilename = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME); - int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); - int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); - int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); - int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); - - List downloads = new ArrayList(); - while (c.moveToNext()) { - if (!url.equals(c.getString(columnUri))) - continue; - - String localFilename = c.getString(columnFilename); - if (localFilename != null && !localFilename.isEmpty() && !new File(localFilename).isFile()) { - dm.remove(c.getLong(columnId)); - continue; - } - - downloads.add(new DownloadInfo( - c.getLong(columnId), - c.getString(columnUri), - c.getString(columnTitle), - c.getLong(columnLastMod), - localFilename, - c.getInt(columnStatus), - c.getInt(columnTotalSize), - c.getInt(columnBytesDownloaded), - c.getInt(columnReason) - )); - } - - Collections.sort(downloads); - return downloads; - } - - public static void removeById(Context context, long id) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - dm.remove(id); - } - - public static void removeAllForUrl(Context context, String url) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - Cursor c = dm.query(new Query()); - int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); - int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); - - List idsList = new ArrayList(); - while (c.moveToNext()) { - if (url.equals(c.getString(columnUri))) - idsList.add(c.getLong(columnId)); - } - - if (idsList.isEmpty()) - return; - - long ids[] = new long[idsList.size()]; - for (int i = 0; i < ids.length; i++) - ids[i] = idsList.get(0); - - dm.remove(ids); - } - - - public static void removeOutdated(Context context, long cutoff) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - Cursor c = dm.query(new Query()); - int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); - int columnLastMod = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); - - List idsList = new ArrayList(); - while (c.moveToNext()) { - if (c.getLong(columnLastMod) < cutoff) - idsList.add(c.getLong(columnId)); - } - - if (idsList.isEmpty()) - return; - - long ids[] = new long[idsList.size()]; - for (int i = 0; i < ids.length; i++) - ids[i] = idsList.get(0); - - dm.remove(ids); - } - - - public static void triggerDownloadFinishedCallback(Context context, long id) { - DownloadInfo info = getById(context, id); - if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL) - return; - - DownloadFinishedCallback callback = null; - synchronized (mCallbacks) { - callback = mCallbacks.get(info.url); - } - - if (callback == null) - return; - - callback.onDownloadFinished(context, info); - } - - - public static class DownloadInfo implements Comparable { - public final long id; - public final String url; - public final String title; - public final long lastModification; - public final String localFilename; - public final int status; - public final int totalSize; - public final int bytesDownloaded; - public final int reason; - - private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, - int status, int totalSize, int bytesDownloaded, int reason) { - this.id = id; - this.url = url; - this.title = title; - this.lastModification = lastModification; - this.localFilename = localFilename; - this.status = status; - this.totalSize = totalSize; - this.bytesDownloaded = bytesDownloaded; - this.reason = reason; - } - - @Override - public int compareTo(DownloadInfo another) { - int compare = (int)(another.lastModification - this.lastModification); - if (compare != 0) - return compare; - return this.url.compareTo(another.url); - } - } - - public static interface DownloadFinishedCallback { - public void onDownloadFinished(Context context, DownloadInfo info); - } - - - public static SyncDownloadInfo downloadSynchronously(String url, File target) { - // TODO Potential parameter? - final boolean useNotModifiedTags = true; - - URLConnection connection = null; - InputStream in = null; - FileOutputStream out = null; - try { - connection = new URL(url).openConnection(); - connection.setDoOutput(false); - connection.setConnectTimeout(30000); - connection.setReadTimeout(30000); - - if (connection instanceof HttpURLConnection) { - // Disable transparent gzip encoding for gzipped files - if (url.endsWith(".gz")) - connection.addRequestProperty("Accept-Encoding", "identity"); - - if (useNotModifiedTags) { - String modified = mPref.getString("download_" + url + "_modified", null); - String etag = mPref.getString("download_" + url + "_etag", null); - - if (modified != null) - connection.addRequestProperty("If-Modified-Since", modified); - if (etag != null) - connection.addRequestProperty("If-None-Match", etag); - } - } - - connection.connect(); - - if (connection instanceof HttpURLConnection) { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - int responseCode = httpConnection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { - return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null); - } else if (responseCode < 200 || responseCode >= 300) { - return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, - mApp.getString(R.string.repo_download_failed_http, url, responseCode, - httpConnection.getResponseMessage())); - } - } - - in = connection.getInputStream(); - out = new FileOutputStream(target); - byte buf[] = new byte[1024]; - int read; - while ((read = in.read(buf)) != -1) { - out.write(buf, 0, read); - } - - if (useNotModifiedTags && connection instanceof HttpURLConnection) { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - String modified = httpConnection.getHeaderField("Last-Modified"); - String etag = httpConnection.getHeaderField("ETag"); - - mPref.edit() - .putString("download_" + url + "_modified", modified) - .putString("download_" + url + "_etag", etag) - .commit(); - } - - return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null); - - } catch (Throwable t) { - return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, - mApp.getString(R.string.repo_download_failed, url, t.getMessage())); - - } finally { - if (connection != null && connection instanceof HttpURLConnection) - ((HttpURLConnection) connection).disconnect(); - if (in != null) - try { in.close(); } catch (IOException ignored) {} - if (out != null) - try { out.close(); } catch (IOException ignored) {} - } - } - - public static void clearCache(String url) { - if (url != null) { - mPref.edit() - .remove("download_" + url + "_modified") - .remove("download_" + url + "_etag") - .apply(); - } else { - mPref.edit().clear().apply(); - } - } - - - public static class SyncDownloadInfo { - public static final int STATUS_SUCCESS = 0; - public static final int STATUS_NOT_MODIFIED = 1; - public static final int STATUS_FAILED = 2; - - public final int status; - public final String errorMessage; - - private SyncDownloadInfo(int status, String errorMessage) { - this.status = status; - this.errorMessage = errorMessage; - } - } -} - + public static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; + public static final String MIME_TYPE_ZIP = "application/zip"; + private static final Map mCallbacks = new HashMap<>(); + private static final XposedApp mApp = XposedApp.getInstance(); + private static final SharedPreferences mPref = mApp + .getSharedPreferences("download_cache", Context.MODE_PRIVATE); + + public static String DOWNLOAD_FRAMEWORK = "framework"; + public static String DOWNLOAD_MODULES = "modules"; + + private static DownloadInfo add(Builder b) { + Context context = b.mContext; + removeAllForUrl(context, b.mUrl); + + if (!b.mDialog) { + synchronized (mCallbacks) { + mCallbacks.put(b.mUrl, b.mCallback); + } + } + + String savePath = "XposedInstaller"; + if (b.mModule) { + savePath += "/modules"; + } + + Request request = new Request(Uri.parse(b.mUrl)); + request.setTitle(b.mTitle); + request.setMimeType(b.mMimeType.toString()); + if (b.mSave) { + try { + request.setDestinationInExternalPublicDir(savePath, b.mTitle + b.mMimeType.getExtension()); + } catch (IllegalStateException e) { + Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } else if (b.mDestination != null) { + b.mDestination.getParentFile().mkdirs(); + removeAllForLocalFile(context, b.mDestination); + request.setDestinationUri(Uri.fromFile(b.mDestination)); + } + + request.setNotificationVisibility(Request.VISIBILITY_VISIBLE); + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + long id = dm.enqueue(request); + + if (b.mDialog) { + showDownloadDialog(b, id); + } + + return getById(context, id); + } + + public static File[] getDownloadDirs(String subDir) { + Context context = XposedApp.getInstance(); + ArrayList dirs = new ArrayList<>(2); + for (File dir : ContextCompat.getExternalCacheDirs(context)) { + if (dir != null && EnvironmentCompat.getStorageState(dir).equals(Environment.MEDIA_MOUNTED)) { + dirs.add(new File(new File(dir, "downloads"), subDir)); + } + } + dirs.add(new File(new File(context.getCacheDir(), "downloads"), subDir)); + return dirs.toArray(new File[dirs.size()]); + } + + public static File getDownloadTarget(String subDir, String filename) { + return new File(getDownloadDirs(subDir)[0], filename); + } + + public static File getDownloadTargetForUrl(String subDir, String url) { + return getDownloadTarget(subDir, Uri.parse(url).getLastPathSegment()); + } + + public static DownloadInfo addModule(Context context, String title, String url, boolean save, DownloadFinishedCallback callback) { + return new Builder(context) + .setTitle(title) + .setUrl(url) + .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_MODULES) + .setCallback(callback) + .setSave(save) + .setModule(true) + .setMimeType(MIME_TYPES.APK) + .download(); + } + + public static class Builder { + private final Context mContext; + private String mTitle = null; + private String mUrl = null; + private DownloadFinishedCallback mCallback = null; + private MIME_TYPES mMimeType = MIME_TYPES.APK; + private File mDestination = null; + private boolean mDialog = false; + public boolean mModule = false; + private boolean mSave = false; + + public Builder(Context context) { + mContext = context; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setUrl(String url) { + mUrl = url; + return this; + } + + public Builder setCallback(DownloadFinishedCallback callback) { + mCallback = callback; + return this; + } + + public Builder setMimeType(MIME_TYPES mimeType) { + mMimeType = mimeType; + return this; + } + + public Builder setDestination(File file) { + mDestination = file; + return this; + } + + public Builder setDestinationFromUrl(String subDir) { + if (mUrl == null) { + throw new IllegalStateException("URL must be set first"); + } + return setDestination(getDownloadTargetForUrl(subDir, mUrl)); + } + + public Builder setSave(boolean save) { + this.mSave = save; + return this; + } + + public Builder setModule(boolean module) { + this.mModule = module; + return this; + } + + public Builder setDialog(boolean dialog) { + mDialog = dialog; + return this; + } + + public DownloadInfo download() { + return add(this); + } + } + + private static void showDownloadDialog(final Builder b, final long id) { + final Context context = b.mContext; + final DownloadDialog dialog = new DownloadDialog(new MaterialDialog.Builder(context) + .title(b.mTitle) + .content(R.string.download_view_waiting) + .progress(false, 0, true) + .progressNumberFormat(context.getString(R.string.download_progress)) + .canceledOnTouchOutside(false) + .negativeText(R.string.download_view_cancel) + .onNegative(new SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.cancel(); + } + }) + .cancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + removeById(context, id); + } + }) + ); + dialog.setShowProcess(false); + dialog.show(); + + new Thread("DownloadDialog") { + @Override + public void run() { + while (true) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + return; + } + + final DownloadInfo info = getById(context, id); + if (info == null) { + dialog.cancel(); + return; + } else if (info.status == DownloadManager.STATUS_FAILED) { + dialog.cancel(); + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, + context.getString(R.string.download_view_failed, info.reason), + Toast.LENGTH_LONG).show(); + } + }); + return; + } else if (info.status == DownloadManager.STATUS_SUCCESSFUL) { + dialog.dismiss(); + // Hack to reset stat information. + new File(info.localFilename).setExecutable(false); + if (b.mCallback != null) { + b.mCallback.onDownloadFinished(context, info); + } + return; + } + + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + if (info.totalSize <= 0 || info.status != DownloadManager.STATUS_RUNNING) { + dialog.setContent(R.string.download_view_waiting); + dialog.setShowProcess(false); + } else { + dialog.setContent(R.string.download_running); + dialog.setProgress(info.bytesDownloaded / 1024); + dialog.setMaxProgress(info.totalSize / 1024); + dialog.setShowProcess(true); + } + } + }); + } + } + }.start(); + } + + private static class DownloadDialog extends MaterialDialog { + public DownloadDialog(Builder builder) { + super(builder); + } + + @UiThread + public void setShowProcess(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + mProgress.setVisibility(visibility); + mProgressLabel.setVisibility(visibility); + mProgressMinMax.setVisibility(visibility); + } + } + + public static ModuleVersion getStableVersion(Module m) { + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + return mvTemp; + } + } + return null; + } + + public static DownloadInfo getById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query().setFilterById(id)); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + int status = c.getInt(columnStatus); + String localFilename; + try { + localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(id); + c.close(); + return null; + } + + DownloadInfo info = new DownloadInfo(id, c.getString(columnUri), + c.getString(columnTitle), c.getLong(columnLastMod), + localFilename, status, + c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded), + c.getInt(columnReason)); + c.close(); + return info; + } + + public static DownloadInfo getLatestForUrl(Context context, String url) { + List all; + try { + all = getAllForUrl(context, url); + } catch (Throwable throwable) { + return null; + } + return all.isEmpty() ? null : all.get(0); + } + + public static List getAllForUrl(Context context, String url)throws Throwable { + DownloadManager dm = (DownloadManager) context + .getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + List downloads = new ArrayList<>(); + while (c.moveToNext()) { + if (!url.equals(c.getString(columnUri))) + continue; + + int status = c.getInt(columnStatus); + String localFilename; + try { + localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(c.getLong(columnId)); + continue; + } + + downloads.add(new DownloadInfo(c.getLong(columnId), + c.getString(columnUri), c.getString(columnTitle), + c.getLong(columnLastMod), localFilename, + status, c.getInt(columnTotalSize), + c.getInt(columnBytesDownloaded), c.getInt(columnReason))); + } + c.close(); + + Collections.sort(downloads); + return downloads; + } + + public static void removeById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + dm.remove(id); + } + + public static void removeAllForUrl(Context context, String url) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + if (url.equals(c.getString(columnUri))) + idsList.add(c.getLong(columnId)); + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + + public static void removeAllForLocalFile(Context context, File file) { + file.delete(); + + String filename; + try { + filename = file.getCanonicalPath(); + } catch (IOException e) { + Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e); + return; + } + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + String itemFilename; + try { + itemFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + itemFilename = null; + } + if (itemFilename != null) { + if (filename.equals(itemFilename)) { + idsList.add(c.getLong(columnId)); + } else { + try { + if (filename.equals(new File(itemFilename).getCanonicalPath())) { + idsList.add(c.getLong(columnId)); + } + } catch (IOException ignored) { + } + } + } + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + + public static void removeOutdated(Context context, long cutoff) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + + List idsList = new ArrayList<>(); + while (c.moveToNext()) { + if (c.getLong(columnLastMod) < cutoff) + idsList.add(c.getLong(columnId)); + } + c.close(); + + if (idsList.isEmpty()) + return; + + long ids[] = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(0); + + dm.remove(ids); + } + + public static void triggerDownloadFinishedCallback(Context context, long id) { + DownloadInfo info = getById(context, id); + if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL) + return; + + DownloadFinishedCallback callback; + synchronized (mCallbacks) { + callback = mCallbacks.get(info.url); + } + + if (callback == null) + return; + + // Hack to reset stat information. + new File(info.localFilename).setExecutable(false); + callback.onDownloadFinished(context, info); + } + + private static String getFilenameFromUri(String uriString) { + if (uriString == null) { + return null; + } + Uri uri = Uri.parse(uriString); + if (uri.getScheme().equals("file")) { + return uri.getPath(); + } else if (uri.getScheme().equals("content")) { + Context context = XposedApp.getInstance(); + Cursor c = null; + try { + c = context.getContentResolver().query(uri, new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null); + c.moveToFirst(); + return c.getString(c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); + } finally { + if (c != null) { + c.close(); + } + } + } else { + throw new UnsupportedOperationException("Unexpected URI: " + uriString); + } + } + + public static SyncDownloadInfo downloadSynchronously(String url, File target) { + final boolean useNotModifiedTags = target.exists(); + + URLConnection connection = null; + InputStream in = null; + FileOutputStream out = null; + try { + connection = new URL(url).openConnection(); + connection.setDoOutput(false); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + + if (connection instanceof HttpURLConnection) { + // Disable transparent gzip encoding for gzipped files + if (url.endsWith(".gz")) { + connection.addRequestProperty("Accept-Encoding", "identity"); + } + + if (useNotModifiedTags) { + String modified = mPref.getString("download_" + url + "_modified", null); + String etag = mPref.getString("download_" + url + "_etag", null); + + if (modified != null) { + connection.addRequestProperty("If-Modified-Since", modified); + } + if (etag != null) { + connection.addRequestProperty("If-None-Match", etag); + } + } + } + + connection.connect(); + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null); + } else if (responseCode < 200 || responseCode >= 300) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed_http, + url, responseCode, + httpConnection.getResponseMessage())); + } + } + + in = connection.getInputStream(); + out = new FileOutputStream(target); + byte buf[] = new byte[1024]; + int read; + while ((read = in.read(buf)) != -1) { + out.write(buf, 0, read); + } + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + String modified = httpConnection.getHeaderField("Last-Modified"); + String etag = httpConnection.getHeaderField("ETag"); + + mPref.edit() + .putString("download_" + url + "_modified", modified) + .putString("download_" + url + "_etag", etag).apply(); + } + + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null); + + } catch (Throwable t) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed, url, + t.getMessage())); + + } finally { + if (connection != null && connection instanceof HttpURLConnection) + ((HttpURLConnection) connection).disconnect(); + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + if (out != null) + try { + out.close(); + } catch (IOException ignored) { + } + } + } + + public static void clearCache(String url) { + if (url != null) { + mPref.edit().remove("download_" + url + "_modified") + .remove("download_" + url + "_etag").apply(); + } else { + mPref.edit().clear().apply(); + } + } + + public enum MIME_TYPES { + APK { + public String toString() { + return MIME_TYPE_APK; + } + + public String getExtension() { + return ".apk"; + } + }, + ZIP { + public String toString() { + return MIME_TYPE_ZIP; + } + + public String getExtension() { + return ".zip"; + } + }; + + public String getExtension() { + return null; + } + } + + public interface DownloadFinishedCallback { + void onDownloadFinished(Context context, DownloadInfo info); + } + + public static class DownloadInfo implements Comparable { + public final long id; + public final String url; + public final String title; + public final long lastModification; + public final String localFilename; + public final int status; + public final int totalSize; + public final int bytesDownloaded; + public final int reason; + + private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) { + this.id = id; + this.url = url; + this.title = title; + this.lastModification = lastModification; + this.localFilename = localFilename; + this.status = status; + this.totalSize = totalSize; + this.bytesDownloaded = bytesDownloaded; + this.reason = reason; + } + + @Override + public int compareTo(@NonNull DownloadInfo another) { + int compare = (int) (another.lastModification + - this.lastModification); + if (compare != 0) + return compare; + return this.url.compareTo(another.url); + } + } + + public static class SyncDownloadInfo { + public static final int STATUS_SUCCESS = 0; + public static final int STATUS_NOT_MODIFIED = 1; + public static final int STATUS_FAILED = 2; + + public final int status; + public final String errorMessage; + + private SyncDownloadInfo(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java index 5daaf5413..08007b6fa 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/HashUtil.java @@ -8,59 +8,57 @@ import java.security.NoSuchAlgorithmException; public class HashUtil { - public static final String hash(String input, String algorithm) { - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - byte[] messageDigest = md.digest(input.getBytes()); - return toHexString(messageDigest); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException(e); - } - } + public static final String hash(String input, String algorithm) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] messageDigest = md.digest(input.getBytes()); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } - public static final String md5(String input) { - return hash(input, "MD5"); - } + public static final String md5(String input) { + return hash(input, "MD5"); + } - public static final String sha1(String input) { - return hash(input, "SHA-1"); - } + public static final String sha1(String input) { + return hash(input, "SHA-1"); + } + public static final String hash(File file, String algorithm) throws IOException { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + InputStream is = new FileInputStream(file); + byte[] buffer = new byte[8192]; + int read = 0; + while ((read = is.read(buffer)) > 0) { + md.update(buffer, 0, read); + } + is.close(); + byte[] messageDigest = md.digest(); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } - public static final String hash(File file, String algorithm) throws IOException { - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - InputStream is = new FileInputStream(file); - byte[] buffer = new byte[8192]; - int read = 0; - while ((read = is.read(buffer)) > 0) { - md.update(buffer, 0, read); - } - is.close(); - byte[] messageDigest = md.digest(); - return toHexString(messageDigest); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException(e); - } - } + public static final String md5(File input) throws IOException { + return hash(input, "MD5"); + } - public static final String md5(File input) throws IOException { - return hash(input, "MD5"); - } + public static final String sha1(File input) throws IOException { + return hash(input, "SHA-1"); + } - public static final String sha1(File input) throws IOException { - return hash(input, "SHA-1"); - } - - - private static String toHexString(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - int unsignedB = b & 0xff; - if (unsignedB < 0x10) - sb.append("0"); - sb.append(Integer.toHexString(unsignedB)); - } - return sb.toString(); - } -} + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + int unsignedB = b & 0xff; + if (unsignedB < 0x10) + sb.append("0"); + sb.append(Integer.toHexString(unsignedB)); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/InstallApkUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/InstallApkUtil.java new file mode 100644 index 000000000..ee13bb7bc --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/InstallApkUtil.java @@ -0,0 +1,108 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.support.v4.content.FileProvider; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; + +public class InstallApkUtil extends AsyncTask { + + private static final int ERROR_ROOT_NOT_GRANTED = -99; + + private final DownloadsUtil.DownloadInfo info; + private final Context context; + private RootUtil mRootUtil; + private boolean isApkRootInstallOn; + private List output = new LinkedList<>(); + + public InstallApkUtil(Context context, DownloadsUtil.DownloadInfo info) { + this.context = context; + this.info = info; + + mRootUtil = new RootUtil(); + } + + public static void installApkNormally(Context context, String localFilename) { + Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri; + if (Build.VERSION.SDK_INT >= 24) { + uri = FileProvider.getUriForFile(context, "de.robv.android.xposed.installer.fileprovider", new File(localFilename)); + installIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else { + uri = Uri.fromFile(new File(localFilename)); + } + installIntent.setDataAndType(uri, DownloadsUtil.MIME_TYPE_APK); + installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName); + context.startActivity(installIntent); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + SharedPreferences prefs = XposedApp.getPreferences(); + isApkRootInstallOn = prefs.getBoolean("install_with_su", false); + + if (isApkRootInstallOn) { + NotificationUtil.showModuleInstallingNotification(info.title); + mRootUtil.startShell(); + } + } + + @Override + protected Integer doInBackground(Void... params) { + int returnCode = 0; + if (isApkRootInstallOn) { + try { + returnCode = mRootUtil.execute("pm install -r \"" + info.localFilename + "\"", output); + } catch (IllegalStateException e) { + returnCode = ERROR_ROOT_NOT_GRANTED; + } + } + return returnCode; + } + + @Override + protected void onPostExecute(Integer result) { + super.onPostExecute(result); + + if (isApkRootInstallOn) { + NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLING); + + if (result.equals(ERROR_ROOT_NOT_GRANTED)) { + NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.root_failed, info.localFilename); + return; + } + + StringBuilder out = new StringBuilder(); + for (Object o : output) { + out.append(o.toString()); + out.append("\n"); + } + + Pattern failurePattern = Pattern.compile("(?m)^Failure\\s+\\[(.*?)\\]$"); + Matcher failureMatcher = failurePattern.matcher(out); + + if (result.equals(0)) { + NotificationUtil.showModuleInstallNotification(R.string.installation_successful, R.string.installation_successful_message, info.localFilename, info.title); + } else if (failureMatcher.find()) { + NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.installation_error_message, info.localFilename, info.title, failureMatcher.group(1)); + } + } else { + installApkNormally(context, info.localFilename); + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java new file mode 100644 index 000000000..f66bb8318 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java @@ -0,0 +1,222 @@ +package de.robv.android.xposed.installer.util; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.robv.android.xposed.installer.BuildConfig; +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.installation.FlashCallback; +import de.robv.android.xposed.installer.installation.StatusInstallerFragment; + +public final class InstallZipUtil { + private static final Set FEATURES = new HashSet<>(); + + static { + FEATURES.add("fbe_aware"); // BASE_DIR in /data/user_de/0 on SDK24+ + } + + private InstallZipUtil() {} + + public static ZipFile getZip(String path) { + try { + return new ZipFile(path); + } catch (IOException e) { + return null; + } + } + + public static ZipCheckResult checkZip(ZipFile zip) { + ZipCheckResult result = new ZipCheckResult(zip); + + // Check for update-binary. + if (zip.getEntry("META-INF/com/google/android/update-binary") == null) { + return result; + } + + result.mValidZip = true; + + // Check whether the file can be flashed directly in the app. + if (zip.getEntry("META-INF/com/google/android/flash-script.sh") != null) { + result.mFlashableInApp = true; + } + + ZipEntry xposedPropEntry = zip.getEntry("system/xposed.prop"); + if (xposedPropEntry != null) { + try { + result.mXposedProp = parseXposedProp(zip.getInputStream(xposedPropEntry)); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Failed to read system/xposed.prop from " + zip.getName(), e); + } + } + + return result; + } + + public static XposedProp parseXposedProp(InputStream is) throws IOException { + XposedProp prop = new XposedProp(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split("=", 2); + if (parts.length != 2) { + continue; + } + + String key = parts[0].trim(); + if (key.charAt(0) == '#') { + continue; + } + + String value = parts[1].trim(); + + if (key.equals("version")) { + prop.mVersion = value; + prop.mVersionInt = ModuleUtil.extractIntPart(value); + } else if (key.equals("arch")) { + prop.mArch = value; + } else if (key.equals("minsdk")) { + prop.mMinSdk = Integer.parseInt(value); + } else if (key.equals("maxsdk")) { + prop.mMaxSdk = Integer.parseInt(value); + } else if (key.startsWith("requires:")) { + prop.mRequires.add(key.substring(9)); + } + } + reader.close(); + return prop.isComplete() ? prop : null; + } + + public static String messageForError(int code, Object... args) { + Context context = XposedApp.getInstance(); + switch (code) { + case FlashCallback.ERROR_TIMEOUT: + return context.getString(R.string.flash_error_timeout); + + case FlashCallback.ERROR_SHELL_DIED: + return context.getString(R.string.flash_error_shell_died); + + case FlashCallback.ERROR_NO_ROOT_ACCESS: + return context.getString(R.string.root_failed); + + case FlashCallback.ERROR_INVALID_ZIP: + String message = context.getString(R.string.flash_error_invalid_zip); + if (args.length > 0) { + message += "\n" + args[0]; + } + return message; + + case FlashCallback.ERROR_NOT_FLASHABLE_IN_APP: + return context.getString(R.string.flash_error_not_flashable_in_app); + + case FlashCallback.ERROR_INSTALLER_NEEDS_UPDATE: + Resources res = context.getResources(); + return res.getString(R.string.installer_needs_update, res.getString(R.string.app_name)); + + default: + return context.getString(R.string.flash_error_default, code); + } + } + + public static void triggerError(FlashCallback callback, int code, Object... args) { + callback.onError(code, messageForError(code, args)); + } + + public static void closeSilently(ZipFile z) { + try { + z.close(); + } catch (IOException ignored) {} + } + + public static void reportMissingFeatures(Set missingFeatures) { + Log.e(XposedApp.TAG, "Installer version: " + BuildConfig.VERSION_NAME); + Log.e(XposedApp.TAG, "Missing installer features: " + missingFeatures); + } + + public static class ZipCheckResult { + private final ZipFile mZip; + private boolean mValidZip = false; + private boolean mFlashableInApp = false; + private XposedProp mXposedProp = null; + + private ZipCheckResult(ZipFile zip) { + mZip = zip; + } + + public ZipFile getZip() { + return mZip; + } + + public boolean isValidZip() { + return mValidZip; + } + + public boolean isFlashableInApp() { + return mFlashableInApp; + } + + public boolean hasXposedProp() { + return mXposedProp != null; + } + + public XposedProp getXposedProp() { + return mXposedProp; + } + } + + public static class XposedProp { + private String mVersion = null; + private int mVersionInt = 0; + private String mArch = null; + private int mMinSdk = 0; + private int mMaxSdk = 0; + private Set mRequires = new HashSet<>(); + + private boolean isComplete() { + return mVersion != null + && mVersionInt > 0 + && mArch != null + && mMinSdk > 0 + && mMaxSdk > 0; + } + + public String getVersion() { + return mVersion; + } + + public int getVersionInt() { + return mVersionInt; + } + + public boolean isArchCompatible() { + return StatusInstallerFragment.ARCH.equals(mArch); + } + + public boolean isSdkCompatible() { + return mMinSdk <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT <= mMaxSdk; + } + + public Set getMissingInstallerFeatures() { + Set missing = new TreeSet<>(mRequires); + missing.removeAll(FEATURES); + return missing; + } + + public boolean isCompatible() { + return isSdkCompatible() && isArchCompatible(); + } + + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java index 6f47960b1..d493a469b 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/ModuleUtil.java @@ -1,333 +1,384 @@ package de.robv.android.xposed.installer.util; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.FileUtils; import android.util.Log; import android.widget.Toast; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.robv.android.xposed.installer.ModulesFragment; import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.installation.StatusInstallerFragment; import de.robv.android.xposed.installer.repo.ModuleVersion; import de.robv.android.xposed.installer.repo.RepoDb; - public final class ModuleUtil { - private static ModuleUtil mInstance = null; - private final XposedApp mApp; - private SharedPreferences mPref; - private final PackageManager mPm; - private final String mFrameworkPackageName; - private InstalledModule mFramework = null; - private Map mInstalledModules; - private boolean mIsReloading = false; - private final List mListeners = new CopyOnWriteArrayList(); - - public static int MIN_MODULE_VERSION = 2; // reject modules with xposedminversion below this - private static final String MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/modules.list"; - - private ModuleUtil() { - mApp = XposedApp.getInstance(); - mPref = mApp.getSharedPreferences("enabled_modules", Context.MODE_PRIVATE); - mPm = mApp.getPackageManager(); - mFrameworkPackageName = mApp.getPackageName(); - } - - public static synchronized ModuleUtil getInstance() { - if (mInstance == null) { - mInstance = new ModuleUtil(); - mInstance.reloadInstalledModules(); - } - return mInstance; - } - - public void reloadInstalledModules() { - synchronized (this) { - if (mIsReloading) - return; - mIsReloading = true; - } - mApp.updateProgressIndicator(); - - Map modules = new HashMap(); - RepoDb.beginTransation(); - try { - RepoDb.deleteAllInstalledModules(); - - for (PackageInfo pkg : mPm.getInstalledPackages(PackageManager.GET_META_DATA)) { - ApplicationInfo app = pkg.applicationInfo; - if (!app.enabled) - continue; - - InstalledModule installed = null; - if (app.metaData != null && app.metaData.containsKey("xposedmodule")) { - installed = new InstalledModule(pkg, false); - modules.put(pkg.packageName, installed); - } else if (isFramework(pkg.packageName)) { - mFramework = installed = new InstalledModule(pkg, true); - } - - if (installed != null) - RepoDb.insertInstalledModule(installed); - } - - RepoDb.setTransactionSuccessful(); - } finally { - RepoDb.endTransation(); - } - - mInstalledModules = modules; - synchronized (this) { - mIsReloading = false; - } - mApp.updateProgressIndicator(); - for (ModuleListener listener : mListeners) { - listener.onInstalledModulesReloaded(mInstance); - } - } - - public InstalledModule reloadSingleModule(String packageName) { - PackageInfo pkg; - try { - pkg = mPm.getPackageInfo(packageName, PackageManager.GET_META_DATA); - } catch (NameNotFoundException e) { - RepoDb.deleteInstalledModule(packageName); - InstalledModule old = mInstalledModules.remove(packageName); - if (old != null) { - for (ModuleListener listener : mListeners) { - listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); - } - } - return null; - } - - ApplicationInfo app = pkg.applicationInfo; - if (app.enabled && app.metaData != null && app.metaData.containsKey("xposedmodule")) { - InstalledModule module = new InstalledModule(pkg, false); - RepoDb.insertInstalledModule(module); - mInstalledModules.put(packageName, module); - for (ModuleListener listener : mListeners) { - listener.onSingleInstalledModuleReloaded(mInstance, packageName, module); - } - return module; - } else { - RepoDb.deleteInstalledModule(packageName); - InstalledModule old = mInstalledModules.remove(packageName); - if (old != null) { - for (ModuleListener listener : mListeners) { - listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); - } - } - return null; - } - } - - public synchronized boolean isLoading() { - return mIsReloading; - } - - public InstalledModule getFramework() { - return mFramework; - } - - public String getFrameworkPackageName() { - return mFrameworkPackageName; - } - - public boolean isFramework(String packageName) { - return mFrameworkPackageName.equals(packageName); - } - - public boolean isInstalled(String packageName) { - return mInstalledModules.containsKey(packageName) || isFramework(packageName); - } - - public InstalledModule getModule(String packageName) { - return mInstalledModules.get(packageName); - } - - public Map getModules() { - return mInstalledModules; - } - - public void setModuleEnabled(String packageName, boolean enabled) { - if (enabled) - mPref.edit().putInt(packageName, 1).commit(); - else - mPref.edit().remove(packageName).commit(); - } - - public boolean isModuleEnabled(String packageName) { - return mPref.contains(packageName); - } - - public List getEnabledModules() { - LinkedList result = new LinkedList(); - - for (String packageName : mPref.getAll().keySet()) { - InstalledModule module = getModule(packageName); - if (module != null) - result.add(module); - else - setModuleEnabled(packageName, false); - } - - return result; - } - - public synchronized void updateModulesList(boolean showToast) { - try { - Log.i(XposedApp.TAG, "updating modules.list"); - int installedXposedVersion = XposedApp.getActiveXposedVersion(); - if (installedXposedVersion <= 0) { - Toast.makeText(mApp, "The Xposed framework is not installed", Toast.LENGTH_SHORT).show(); - return; - } - - PrintWriter modulesList = new PrintWriter(MODULES_LIST_FILE); - List enabledModules = getEnabledModules(); - for (InstalledModule module : enabledModules) { - if (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION) - continue; - - modulesList.println(module.app.sourceDir); - } - modulesList.close(); - - FileUtils.setPermissions(MODULES_LIST_FILE, 00664, -1, -1); - - if (showToast) - Toast.makeText(mApp, R.string.xposed_module_list_updated, Toast.LENGTH_SHORT).show(); - } catch (IOException e) { - Log.e(XposedApp.TAG, "cannot write " + MODULES_LIST_FILE, e); - Toast.makeText(mApp, "cannot write " + MODULES_LIST_FILE, Toast.LENGTH_SHORT).show(); - } - } - - public static int extractIntPart(String str) { - int result = 0, length = str.length(); - for (int offset = 0; offset < length; offset++) { - char c = str.charAt(offset); - if ('0' <= c && c <= '9') - result = result * 10 + (c - '0'); - else - break; - } - return result; - } - - - - public class InstalledModule { - public ApplicationInfo app; - public final String packageName; - public final boolean isFramework; - public final String versionName; - public final int versionCode; - public final int minVersion; - - private String appName; // loaded lazyily - private String description; // loaded lazyily - - private Drawable.ConstantState iconCache = null; - - private InstalledModule(PackageInfo pkg, boolean isFramework) { - this.app = pkg.applicationInfo; - this.packageName = pkg.packageName; - this.isFramework = isFramework; - this.versionName = pkg.versionName; - this.versionCode = pkg.versionCode; - - if (isFramework) { - this.minVersion = 0; - this.description = ""; - } else { - Object minVersionRaw = app.metaData.get("xposedminversion"); - if (minVersionRaw instanceof Integer) { - this.minVersion = (Integer) minVersionRaw; - } else if (minVersionRaw instanceof String) { - this.minVersion = extractIntPart((String) minVersionRaw); - } else { - this.minVersion = 0; - } - } - } - - public String getAppName() { - if (appName == null) - appName = app.loadLabel(mPm).toString(); - return appName; - } - - public String getDescription() { - if (this.description == null) { - Object descriptionRaw = app.metaData.get("xposeddescription"); - String descriptionTmp = null; - if (descriptionRaw instanceof String) { - descriptionTmp = ((String) descriptionRaw).trim(); - } else if (descriptionRaw instanceof Integer) { - try { - int resId = (Integer) descriptionRaw; - if (resId != 0) - descriptionTmp = mPm.getResourcesForApplication(app).getString(resId).trim(); - } catch (Exception ignored) {} - } - this.description = (descriptionTmp != null) ? descriptionTmp : ""; - } - return this.description; - } - - public boolean isUpdate(ModuleVersion version) { - return (version != null) ? version.code > versionCode : false; - } - - public Drawable getIcon() { - if (iconCache != null) - return iconCache.newDrawable(); - - Drawable result = app.loadIcon(mPm); - iconCache = result.getConstantState(); - return result; - } - - @Override - public String toString() { - return getAppName(); - } - } - - - - public void addListener(ModuleListener listener) { - if (!mListeners.contains(listener)) - mListeners.add(listener); - } - - public void removeListener(ModuleListener listener) { - mListeners.remove(listener); - } - - public interface ModuleListener { - /** - * Called whenever one (previously or now) installed module has been reloaded - */ - public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module); - - /** - * Called whenever all installed modules have been reloaded - */ - public void onInstalledModulesReloaded(ModuleUtil moduleUtil); - } + // xposedminversion below this + private static final String MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/modules.list"; + private static final String PLAY_STORE_PACKAGE = "com.android.vending"; + public static int MIN_MODULE_VERSION = 2; // reject modules with + private static ModuleUtil mInstance = null; + private final XposedApp mApp; + private final PackageManager mPm; + private final String mFrameworkPackageName; + private final List mListeners = new CopyOnWriteArrayList(); + private SharedPreferences mPref; + private InstalledModule mFramework = null; + private Map mInstalledModules; + private boolean mIsReloading = false; + private Toast mToast; + + private ModuleUtil() { + mApp = XposedApp.getInstance(); + mPref = mApp.getSharedPreferences("enabled_modules", Context.MODE_PRIVATE); + mPm = mApp.getPackageManager(); + mFrameworkPackageName = mApp.getPackageName(); + } + + public static synchronized ModuleUtil getInstance() { + if (mInstance == null) { + mInstance = new ModuleUtil(); + mInstance.reloadInstalledModules(); + } + return mInstance; + } + + public static int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } + + public void reloadInstalledModules() { + synchronized (this) { + if (mIsReloading) + return; + mIsReloading = true; + } + + Map modules = new HashMap(); + RepoDb.beginTransation(); + try { + RepoDb.deleteAllInstalledModules(); + + for (PackageInfo pkg : mPm.getInstalledPackages(PackageManager.GET_META_DATA)) { + ApplicationInfo app = pkg.applicationInfo; + if (!app.enabled) + continue; + + InstalledModule installed = null; + if (app.metaData != null && app.metaData.containsKey("xposedmodule")) { + installed = new InstalledModule(pkg, false); + modules.put(pkg.packageName, installed); + } else if (isFramework(pkg.packageName)) { + mFramework = installed = new InstalledModule(pkg, true); + } + + if (installed != null) + RepoDb.insertInstalledModule(installed); + } + + RepoDb.setTransactionSuccessful(); + } finally { + RepoDb.endTransation(); + } + + mInstalledModules = modules; + synchronized (this) { + mIsReloading = false; + } + for (ModuleListener listener : mListeners) { + listener.onInstalledModulesReloaded(mInstance); + } + } + + public InstalledModule reloadSingleModule(String packageName) { + PackageInfo pkg; + try { + pkg = mPm.getPackageInfo(packageName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + + ApplicationInfo app = pkg.applicationInfo; + if (app.enabled && app.metaData != null && app.metaData.containsKey("xposedmodule")) { + InstalledModule module = new InstalledModule(pkg, false); + RepoDb.insertInstalledModule(module); + mInstalledModules.put(packageName, module); + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, + module); + } + return module; + } else { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + } + + public synchronized boolean isLoading() { + return mIsReloading; + } + + public InstalledModule getFramework() { + return mFramework; + } + + public String getFrameworkPackageName() { + return mFrameworkPackageName; + } + + public boolean isFramework(String packageName) { + return mFrameworkPackageName.equals(packageName); + } + + public boolean isInstalled(String packageName) { + return mInstalledModules.containsKey(packageName) || isFramework(packageName); + } + + public InstalledModule getModule(String packageName) { + return mInstalledModules.get(packageName); + } + + public Map getModules() { + return mInstalledModules; + } + + public void setModuleEnabled(String packageName, boolean enabled) { + if (enabled) + mPref.edit().putInt(packageName, 1).apply(); + else + mPref.edit().remove(packageName).apply(); + } + + public boolean isModuleEnabled(String packageName) { + return mPref.contains(packageName); + } + + public List getEnabledModules() { + LinkedList result = new LinkedList<>(); + + for (String packageName : mPref.getAll().keySet()) { + InstalledModule module = getModule(packageName); + if (module != null) + result.add(module); + else + setModuleEnabled(packageName, false); + } + + return result; + } + + public synchronized void updateModulesList(boolean showToast) { + try { + Log.i(XposedApp.TAG, "ModuleUtil -> updating modules.list"); + int installedXposedVersion = XposedApp.getXposedVersion(); + boolean disabled = StatusInstallerFragment.DISABLE_FILE.exists(); + if (!disabled && installedXposedVersion <= 0) { + Toast.makeText(mApp, "The Xposed framework is not installed", Toast.LENGTH_SHORT).show(); + return; + } + + PrintWriter modulesList = new PrintWriter(MODULES_LIST_FILE); + PrintWriter enabledModulesList = new PrintWriter(XposedApp.ENABLED_MODULES_LIST_FILE); + List enabledModules = getEnabledModules(); + for (InstalledModule module : enabledModules) { + if (!disabled && (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION)) { + Toast.makeText(mApp, "The Xposed framework is not installed", Toast.LENGTH_SHORT).show(); + continue; + } + + modulesList.println(module.app.sourceDir); + + try { + String installer = mPm.getInstallerPackageName(module.app.packageName); + if (!PLAY_STORE_PACKAGE.equals(installer)) + enabledModulesList.println(module.app.packageName); + } catch (Exception ignored) { + } + } + modulesList.close(); + enabledModulesList.close(); + + FileUtils.setPermissions(MODULES_LIST_FILE, 00664, -1, -1); + FileUtils.setPermissions(XposedApp.ENABLED_MODULES_LIST_FILE, 00664, -1, -1); + + if (showToast) + showToast(R.string.xposed_module_list_updated); + } catch (IOException e) { + Log.e(XposedApp.TAG, "ModuleUtil -> cannot write " + MODULES_LIST_FILE, e); + Toast.makeText(mApp, "cannot write " + MODULES_LIST_FILE + e, Toast.LENGTH_SHORT).show(); + } + } + + private void showToast(int message) { + if (mToast != null) { + mToast.cancel(); + mToast = null; + } + mToast = Toast.makeText(mApp, mApp.getString(message), Toast.LENGTH_SHORT); + mToast.show(); + } + + public void addListener(ModuleListener listener) { + if (!mListeners.contains(listener)) + mListeners.add(listener); + } + + public void removeListener(ModuleListener listener) { + mListeners.remove(listener); + } + + public interface ModuleListener { + /** + * Called whenever one (previously or now) installed module has been + * reloaded + */ + void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module); + + /** + * Called whenever all installed modules have been reloaded + */ + void onInstalledModulesReloaded(ModuleUtil moduleUtil); + } + + public class InstalledModule { + private static final int FLAG_FORWARD_LOCK = 1 << 29; + public final String packageName; + public final boolean isFramework; + public final String versionName; + public final int versionCode; + public final int minVersion; + public ApplicationInfo app; + private String appName; // loaded lazyily + private String description; // loaded lazyily + + private Drawable.ConstantState iconCache = null; + + private InstalledModule(PackageInfo pkg, boolean isFramework) { + this.app = pkg.applicationInfo; + this.packageName = pkg.packageName; + this.isFramework = isFramework; + this.versionName = pkg.versionName; + this.versionCode = pkg.versionCode; + + if (isFramework) { + this.minVersion = 0; + this.description = ""; + } else { + int version = XposedApp.getXposedVersion(); + if (version > 0 && XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + this.minVersion = version; + } else { + Object minVersionRaw = app.metaData.get("xposedminversion"); + if (minVersionRaw instanceof Integer) { + this.minVersion = (Integer) minVersionRaw; + } else if (minVersionRaw instanceof String) { + this.minVersion = extractIntPart((String) minVersionRaw); + } else { + this.minVersion = 0; + } + } + } + } + + public boolean isInstalledOnExternalStorage() { + return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; + } + + /** + * @hide + */ + public boolean isForwardLocked() { + return (app.flags & FLAG_FORWARD_LOCK) != 0; + } + + public String getAppName() { + if (appName == null) + appName = app.loadLabel(mPm).toString(); + return appName; + } + + public String getDescription() { + if (this.description == null) { + Object descriptionRaw = app.metaData.get("xposeddescription"); + String descriptionTmp = null; + if (descriptionRaw instanceof String) { + descriptionTmp = ((String) descriptionRaw).trim(); + } else if (descriptionRaw instanceof Integer) { + try { + int resId = (Integer) descriptionRaw; + if (resId != 0) + descriptionTmp = mPm.getResourcesForApplication(app).getString(resId).trim(); + } catch (Exception ignored) { + } + } + this.description = (descriptionTmp != null) ? descriptionTmp : ""; + } + return this.description; + } + + public boolean isUpdate(ModuleVersion version) { + return (version != null) && version.code > versionCode; + } + + public Drawable getIcon() { + if (iconCache != null) + return iconCache.newDrawable(); + + Intent mIntent = new Intent(Intent.ACTION_MAIN); + mIntent.addCategory(ModulesFragment.SETTINGS_CATEGORY); + mIntent.setPackage(app.packageName); + List ris = mPm.queryIntentActivities(mIntent, 0); + + Drawable result; + if (ris == null || ris.size() <= 0) + result = app.loadIcon(mPm); + else + result = ris.get(0).activityInfo.loadIcon(mPm); + iconCache = result.getConstantState(); + + return result; + } + + @Override + public String toString() { + return getAppName(); + } + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java index 6b15a0b49..ce2408c9b 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/NavUtil.java @@ -1,57 +1,63 @@ package de.robv.android.xposed.installer.util; import android.app.Activity; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.Browser; +import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsIntent; import android.text.Spannable; import android.text.SpannableString; import android.text.style.URLSpan; import android.text.util.Linkify; -import de.robv.android.xposed.installer.R; -import de.robv.android.xposed.installer.XposedBaseActivity; + +import com.afollestad.materialdialogs.MaterialDialog; + +import de.robv.android.xposed.installer.XposedApp; public final class NavUtil { - public static final String FINISH_ON_UP_NAVIGATION = "finish_on_up_navigation"; - public static final Uri EXAMPLE_URI = Uri.fromParts("http", "//example.org", null); - - public static void setTransitionSlideEnter(Activity activity) { - activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); - - if (activity instanceof XposedBaseActivity) - ((XposedBaseActivity) activity).setLeftWithSlideAnim(true); - } - - public static void setTransitionSlideLeave(Activity activity) { - activity.overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right); - } - - public static Uri parseURL(String str) { - if (str == null || str.isEmpty()) - return null; - - Spannable spannable = new SpannableString(str); - Linkify.addLinks(spannable, Linkify.ALL); - URLSpan spans[] = spannable.getSpans(0, spannable.length(), URLSpan.class); - return (spans.length > 0) ? Uri.parse(spans[0].getURL()) : null; - } - - public static void startURL(Context context, Uri uri) { - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); - - if ("http".equals(uri.getScheme()) && "repo.xposed.info".equals(uri.getHost())) { - Intent browser = new Intent(Intent.ACTION_VIEW, EXAMPLE_URI); - ComponentName browserApp = browser.resolveActivity(context.getPackageManager()); - intent.setComponent(browserApp); - } - - context.startActivity(intent); - } - - public static void startURL(Context context, String url) { - startURL(context, parseURL(url)); - } -} + + public static Uri parseURL(String str) { + if (str == null || str.isEmpty()) + return null; + + Spannable spannable = new SpannableString(str); + Linkify.addLinks(spannable, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + + URLSpan spans[] = spannable.getSpans(0, spannable.length(), URLSpan.class); + return (spans.length > 0) ? Uri.parse(spans[0].getURL()) : null; + } + + public static void startURL(Activity activity, Uri uri) { + if (!XposedApp.getPreferences().getBoolean("chrome_tabs", true)) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); + activity.startActivity(intent); + return; + } + + CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder(); + customTabsIntent.setShowTitle(true); + customTabsIntent.setToolbarColor(XposedApp.getColor(activity)); + customTabsIntent.build().launchUrl(activity, uri); + } + + public static void startURL(Activity activity, String url) { + startURL(activity, parseURL(url)); + } + + @AnyThread + public static void showMessage(final @NonNull Context context, final CharSequence message) { + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + new MaterialDialog.Builder(context) + .content(message) + .positiveText(android.R.string.ok) + .show(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java index 6e8ee408a..6746dd6b9 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/NotificationUtil.java @@ -1,166 +1,318 @@ package de.robv.android.xposed.installer.util; -import java.util.LinkedList; -import java.util.List; - +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; +import android.support.annotation.StringRes; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; + +import java.util.LinkedList; +import java.util.List; + import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.WelcomeActivity; import de.robv.android.xposed.installer.XposedApp; -import de.robv.android.xposed.installer.XposedBaseActivity; public final class NotificationUtil { - private static Context sContext = null; - private static NotificationManager sNotificationManager; - - public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0; - public static final int NOTIFICATION_MODULES_UPDATED = 1; - - private static final int PENDING_INTENT_OPEN_MODULES = 0; - private static final int PENDING_INTENT_OPEN_INSTALL = 1; - private static final int PENDING_INTENT_SOFT_REBOOT = 2; - private static final int PENDING_INTENT_REBOOT = 3; - private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4; - - public static void init() { - if (sContext != null) - throw new IllegalStateException("NotificationUtil has already been initialized"); - - sContext = XposedApp.getInstance(); - sNotificationManager = (NotificationManager) sContext.getSystemService(Context.NOTIFICATION_SERVICE); - } - - public static void cancel(int id) { - sNotificationManager.cancel(id); - } - - public static void cancelAll() { - sNotificationManager.cancelAll(); - } - - public static void showNotActivatedNotification(String packageName, String appName) { - Intent iModulesTab = new Intent(sContext, XposedBaseActivity.class); - //iModulesTab.putExtra(XposedInstallerActivity.EXTRA_SECTION, XposedInstallerActivity.TAB_MODULES); - iModulesTab.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - PendingIntent pModulesTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_MODULES, - iModulesTab, PendingIntent.FLAG_UPDATE_CURRENT); - - String title = sContext.getString(R.string.module_is_not_activated_yet); - NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext) - .setContentTitle(title) - .setContentText(appName) - .setTicker(title) - .setContentIntent(pModulesTab) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_notification); - - if (Build.VERSION.SDK_INT >= 16) { - Intent iActivateAndReboot = new Intent(sContext, RebootReceiver.class); - iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); - PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT, - iActivateAndReboot, PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); - notiStyle.setBigContentTitle(title); - notiStyle.bigText(sContext.getString(R.string.module_is_not_activated_yet_detailed, appName)); - builder.setStyle(notiStyle); - - // Only show the quick activation button if any module has been enabled before, - // to ensure that the user know the way to disable the module later. - if (!ModuleUtil.getInstance().getEnabledModules().isEmpty()) - builder.addAction(R.drawable.ic_menu_refresh, sContext.getString(R.string.activate_and_reboot), pActivateAndReboot); - } - - sNotificationManager.notify(packageName, NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build()); - } - - public static void showModulesUpdatedNotification() { - Intent iInstallTab = new Intent(sContext, XposedBaseActivity.class); - //iInstallTab.putExtra(XposedInstallerActivity.EXTRA_SECTION, XposedInstallerActivity.TAB_INSTALL); - iInstallTab.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL, - iInstallTab, PendingIntent.FLAG_UPDATE_CURRENT); - - String title = sContext.getString(R.string.xposed_module_updated_notification_title); - String message = sContext.getString(R.string.xposed_module_updated_notification); - NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext) - .setContentTitle(title) - .setContentText(message) - .setTicker(title) - .setContentIntent(pInstallTab) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_notification); - - if (Build.VERSION.SDK_INT >= 16) { - Intent iSoftReboot = new Intent(sContext, RebootReceiver.class); - iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true); - PendingIntent pSoftReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_SOFT_REBOOT, - iSoftReboot, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent iReboot = new Intent(sContext, RebootReceiver.class); - PendingIntent pReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_REBOOT, - iReboot, PendingIntent.FLAG_UPDATE_CURRENT); - - builder.addAction(0, sContext.getString(R.string.reboot), pReboot); - builder.addAction(0, sContext.getString(R.string.soft_reboot), pSoftReboot); - } - - sNotificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build()); - } - - public static class RebootReceiver extends BroadcastReceiver { - public static String EXTRA_SOFT_REBOOT = "soft"; - public static String EXTRA_ACTIVATE_MODULE = "activate_module"; - - @Override - public void onReceive(Context context, Intent intent) { - /* - * Close the notification bar in order to see the toast - * that module was enabled successfully. - * Furthermore, if SU permissions haven't been granted yet, - * the SU dialog will be prompted behind the expanded notification - * panel and is therefore not visible to the user. - */ - sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - cancelAll(); - - if (intent.hasExtra(EXTRA_ACTIVATE_MODULE)) { - String packageName = intent.getStringExtra(EXTRA_ACTIVATE_MODULE); - ModuleUtil moduleUtil = ModuleUtil.getInstance(); - moduleUtil.setModuleEnabled(packageName, true); - moduleUtil.updateModulesList(false); - Toast.makeText(sContext, R.string.module_activated, Toast.LENGTH_SHORT).show(); - } - - RootUtil rootUtil = new RootUtil(); - if (!rootUtil.startShell()) { - Log.e(XposedApp.TAG, "Could not start root shell"); - return; - } - - List messages = new LinkedList(); - boolean isSoftReboot = intent.getBooleanExtra(EXTRA_SOFT_REBOOT, false); - int returnCode = isSoftReboot ? - rootUtil.execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", messages) - : rootUtil.executeWithBusybox("reboot", messages); - - if (returnCode != 0) { - Log.e(XposedApp.TAG, "Could not reboot:"); - for (String line : messages) { - Log.e(XposedApp.TAG, line); - } - } - - rootUtil.dispose(); - AssetUtil.removeBusybox(); - } - } -} + + public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0; + private static final int NOTIFICATION_MODULES_UPDATED = 1; + private static final int NOTIFICATION_INSTALLER_UPDATE = 2; + private static final int NOTIFICATION_MODULE_INSTALLATION = 3; + public static final int NOTIFICATION_MODULE_INSTALLING = 4; + + private static final int PENDING_INTENT_OPEN_MODULES = 0; + private static final int PENDING_INTENT_OPEN_INSTALL = 1; + private static final int PENDING_INTENT_SOFT_REBOOT = 2; + private static final int PENDING_INTENT_REBOOT = 3; + private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4; + private static final int PENDING_INTENT_ACTIVATE_MODULE = 5; + private static final int PENDING_INTENT_INSTALL_APK = 6; + + private static final String COLORED_NOTIFICATION = "colored_notification"; + private static final String HEADS_UP = "heads_up"; + private static final String FRAGMENT_ID = "fragment"; + + private static final String NOTIFICATION_UPDATE_CHANNEL = "app_update_channel"; + private static final String NOTIFICATION_MODULES_CHANNEL = "modules_channel"; + + private static Context sContext = null; + private static NotificationManager sNotificationManager; + private static SharedPreferences prefs; + + public static void init() { + if (sContext != null) return; + + sContext = XposedApp.getInstance(); + prefs = XposedApp.getPreferences(); + sNotificationManager = (NotificationManager) sContext.getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(NOTIFICATION_UPDATE_CHANNEL, sContext.getString(R.string.download_section_update_available), NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_MODULES_CHANNEL, sContext.getString(R.string.nav_item_modules), NotificationManager.IMPORTANCE_DEFAULT); + sNotificationManager.createNotificationChannel(channel); + sNotificationManager.createNotificationChannel(channel1); + } + } + + public static void cancel(int id) { + sNotificationManager.cancel(id); + } + + public static void cancel(String tag, int id) { + sNotificationManager.cancel(tag, id); + } + + public static void cancelAll() { + sNotificationManager.cancelAll(); + } + + public static void showNotActivatedNotification(String packageName, String appName) { + Intent intent = new Intent(sContext, WelcomeActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(FRAGMENT_ID, 1); + PendingIntent pModulesTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_MODULES, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext.getString(R.string.module_is_not_activated_yet); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(appName) + .setTicker(title).setContentIntent(pModulesTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + Intent iActivateAndReboot = new Intent(sContext, RebootReceiver.class); + iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT, + iActivateAndReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iActivate = new Intent(sContext, RebootReceiver.class); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE_AND_RETURN, true); + PendingIntent pActivate = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE, + iActivate, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(sContext.getString(R.string.module_is_not_activated_yet_detailed, appName)); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + // Only show the quick activation button if any module has been + // enabled before, + // to ensure that the user know the way to disable the module later. + if (!ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_menu_refresh, sContext.getString(R.string.activate_and_reboot), pActivateAndReboot).build()); + builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_save, sContext.getString(R.string.activate_only), pActivate).build()); + } + + sNotificationManager.notify(packageName, NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build()); + } + + public static void showModulesUpdatedNotification() { + Intent intent = new Intent(sContext, WelcomeActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(FRAGMENT_ID, 0); + + PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext + .getString(R.string.xposed_module_updated_notification_title); + String message = sContext + .getString(R.string.xposed_module_updated_notification); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setTicker(title).setContentIntent(pInstallTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + Intent iSoftReboot = new Intent(sContext, RebootReceiver.class); + iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true); + PendingIntent pSoftReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_SOFT_REBOOT, + iSoftReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iReboot = new Intent(sContext, RebootReceiver.class); + PendingIntent pReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_REBOOT, + iReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.reboot), pReboot).build()); + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.soft_reboot), pSoftReboot).build()); + builder.setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build()); + } + + static void showModuleInstallNotification(@StringRes int title, @StringRes int message, String path, Object... args) { + showModuleInstallNotification(sContext.getString(title), sContext.getString(message, args), path, title == R.string.installation_error); + } + + private static void showModuleInstallNotification(String title, String message, String path, boolean error) { + NotificationCompat.Builder builder = new NotificationCompat.Builder( + sContext).setContentTitle(title).setContentText(message) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (error) { + Intent iInstallApk = new Intent(sContext, ApkReceiver.class); + iInstallApk.putExtra(ApkReceiver.EXTRA_APK_PATH, path); + PendingIntent pInstallApk = PendingIntent.getBroadcast(sContext, PENDING_INTENT_INSTALL_APK, iInstallApk, PendingIntent.FLAG_UPDATE_CURRENT); + + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.installation_apk_normal), pInstallApk).build()); + } + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLATION, builder.build()); + + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + cancel(NOTIFICATION_MODULE_INSTALLATION); + } + }, 10 * 1000); + } + + public static void showModuleInstallingNotification(String appName) { + String title = sContext.getString(R.string.install_load); + String message = sContext.getString(R.string.install_load_apk, appName); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setVibrate(new long[]{0}).setProgress(0, 0, true) + .setSmallIcon(R.drawable.ic_notification).setOngoing(true); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLING, builder.build()); + } + + public static void showInstallerUpdateNotification() { + Intent intent = new Intent(sContext, WelcomeActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(FRAGMENT_ID, 0); + + PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext.getString(R.string.app_name); + String message = sContext.getString(R.string.newVersion); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setTicker(title).setContentIntent(pInstallTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_UPDATE_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_INSTALLER_UPDATE, builder.build()); + } + + public static class RebootReceiver extends BroadcastReceiver { + public static String EXTRA_SOFT_REBOOT = "soft"; + public static String EXTRA_ACTIVATE_MODULE = "activate_module"; + public static String EXTRA_ACTIVATE_MODULE_AND_RETURN = "activate_module_and_return"; + + @Override + public void onReceive(Context context, Intent intent) { + /* + * Close the notification bar in order to see the toast that module + * was enabled successfully. Furthermore, if SU permissions haven't + * been granted yet, the SU dialog will be prompted behind the + * expanded notification panel and is therefore not visible to the + * user. + */ + sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + cancelAll(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE)) { + String packageName = intent.getStringExtra(EXTRA_ACTIVATE_MODULE); + ModuleUtil moduleUtil = ModuleUtil.getInstance(); + moduleUtil.setModuleEnabled(packageName, true); + moduleUtil.updateModulesList(false); + Toast.makeText(sContext, R.string.module_activated, Toast.LENGTH_SHORT).show(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE_AND_RETURN)) return; + } + + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + Log.e(XposedApp.TAG, "NotificationUtil -> Could not start root shell"); + return; + } + + List messages = new LinkedList<>(); + boolean isSoftReboot = intent.getBooleanExtra(EXTRA_SOFT_REBOOT, + false); + int returnCode = isSoftReboot + ? rootUtil.execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", messages) + : rootUtil.executeWithBusybox("reboot", messages); + + if (returnCode != 0) { + Log.e(XposedApp.TAG, "NotificationUtil -> Could not reboot:"); + for (String line : messages) { + Log.e(XposedApp.TAG, line); + } + } + + rootUtil.dispose(); + AssetUtil.removeBusybox(); + } + } + + public static class ApkReceiver extends BroadcastReceiver { + public static final String EXTRA_APK_PATH = "path"; + + @Override + public void onReceive(Context context, Intent intent) { + /* + * Close the notification bar in order to see the toast that module + * was enabled successfully. Furthermore, if SU permissions haven't + * been granted yet, the SU dialog will be prompted behind the + * expanded notification panel and is therefore not visible to the + * user. + */ + sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + if (intent.hasExtra(EXTRA_APK_PATH)) { + String path = intent.getStringExtra(EXTRA_APK_PATH); + InstallApkUtil.installApkNormally(context, path); + } + NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLATION); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java b/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java index 990adf25d..a88c2088a 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/PrefixedSharedPreferences.java @@ -1,161 +1,160 @@ package de.robv.android.xposed.installer.util; +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import android.annotation.SuppressLint; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - public class PrefixedSharedPreferences implements SharedPreferences { - private final SharedPreferences mBase; - private final String mPrefix; - - public PrefixedSharedPreferences(SharedPreferences base, String prefix) { - mBase = base; - mPrefix = prefix + "_"; - } - - public static void injectToPreferenceManager(PreferenceManager manager, String prefix) { - SharedPreferences prefixedPrefs = new PrefixedSharedPreferences(manager.getSharedPreferences(), prefix); - - try { - Field fieldSharedPref = PreferenceManager.class.getDeclaredField("mSharedPreferences"); - fieldSharedPref.setAccessible(true); - fieldSharedPref.set(manager, prefixedPrefs); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - @Override - public Map getAll() { - Map baseResult = mBase.getAll(); - Map prefixedResult = new HashMap(baseResult); - for (Entry entry : baseResult.entrySet()) { - prefixedResult.put(mPrefix + entry.getKey(), entry.getValue()); - } - return prefixedResult; - } - - @Override - public String getString(String key, String defValue) { - return mBase.getString(mPrefix + key, defValue); - } - - @Override - public Set getStringSet(String key, Set defValues) { - return mBase.getStringSet(mPrefix + key, defValues); - } - - @Override - public int getInt(String key, int defValue) { - return mBase.getInt(mPrefix + key, defValue); - } - - @Override - public long getLong(String key, long defValue) { - return mBase.getLong(mPrefix + key, defValue); - } - - @Override - public float getFloat(String key, float defValue) { - return mBase.getFloat(mPrefix + key, defValue); - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - return mBase.getBoolean(mPrefix + key, defValue); - } - - @Override - public boolean contains(String key) { - return mBase.contains(mPrefix + key); - } - - @SuppressLint("CommitPrefEdits") - @Override - public Editor edit() { - return new EditorImpl(mBase.edit()); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - throw new UnsupportedOperationException("listeners are not supported in this implementation"); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - throw new UnsupportedOperationException("listeners are not supported in this implementation"); - } - - - private class EditorImpl implements Editor { - private final Editor mEditorBase; - - public EditorImpl(Editor base) { - mEditorBase = base; - } - - @Override - public Editor putString(String key, String value) { - mEditorBase.putString(mPrefix + key, value); - return this; - } - - @Override - public Editor putStringSet(String key, Set values) { - mEditorBase.putStringSet(mPrefix + key, values); - return this; - } - - @Override - public Editor putInt(String key, int value) { - mEditorBase.putInt(mPrefix + key, value); - return this; - } - - @Override - public Editor putLong(String key, long value) { - mEditorBase.putLong(mPrefix + key, value); - return this; - } - - @Override - public Editor putFloat(String key, float value) { - mEditorBase.putFloat(mPrefix + key, value); - return this; - } - - @Override - public Editor putBoolean(String key, boolean value) { - mEditorBase.putBoolean(mPrefix + key, value); - return this; - } - - @Override - public Editor remove(String key) { - mEditorBase.remove(mPrefix + key); - return this; - } - - @Override - public Editor clear() { - mEditorBase.clear(); - return this; - } - - @Override - public boolean commit() { - return mEditorBase.commit(); - } - - @Override - public void apply() { - mEditorBase.apply(); - } - } -} + private final SharedPreferences mBase; + private final String mPrefix; + + public PrefixedSharedPreferences(SharedPreferences base, String prefix) { + mBase = base; + mPrefix = prefix + "_"; + } + + public static void injectToPreferenceManager(PreferenceManager manager, String prefix) { + SharedPreferences prefixedPrefs = new PrefixedSharedPreferences(manager.getSharedPreferences(), prefix); + + try { + Field fieldSharedPref = PreferenceManager.class.getDeclaredField("mSharedPreferences"); + fieldSharedPref.setAccessible(true); + fieldSharedPref.set(manager, prefixedPrefs); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public Map getAll() { + Map baseResult = mBase.getAll(); + Map prefixedResult = new HashMap(baseResult); + for (Entry entry : baseResult.entrySet()) { + prefixedResult.put(mPrefix + entry.getKey(), entry.getValue()); + } + return prefixedResult; + } + + @Override + public String getString(String key, String defValue) { + return mBase.getString(mPrefix + key, defValue); + } + + @Override + public Set getStringSet(String key, Set defValues) { + return mBase.getStringSet(mPrefix + key, defValues); + } + + @Override + public int getInt(String key, int defValue) { + return mBase.getInt(mPrefix + key, defValue); + } + + @Override + public long getLong(String key, long defValue) { + return mBase.getLong(mPrefix + key, defValue); + } + + @Override + public float getFloat(String key, float defValue) { + return mBase.getFloat(mPrefix + key, defValue); + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return mBase.getBoolean(mPrefix + key, defValue); + } + + @Override + public boolean contains(String key) { + return mBase.contains(mPrefix + key); + } + + @SuppressLint("CommitPrefEdits") + @Override + public Editor edit() { + return new EditorImpl(mBase.edit()); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + private class EditorImpl implements Editor { + private final Editor mEditorBase; + + public EditorImpl(Editor base) { + mEditorBase = base; + } + + @Override + public Editor putString(String key, String value) { + mEditorBase.putString(mPrefix + key, value); + return this; + } + + @Override + public Editor putStringSet(String key, Set values) { + mEditorBase.putStringSet(mPrefix + key, values); + return this; + } + + @Override + public Editor putInt(String key, int value) { + mEditorBase.putInt(mPrefix + key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + mEditorBase.putLong(mPrefix + key, value); + return this; + } + + @Override + public Editor putFloat(String key, float value) { + mEditorBase.putFloat(mPrefix + key, value); + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + mEditorBase.putBoolean(mPrefix + key, value); + return this; + } + + @Override + public Editor remove(String key) { + mEditorBase.remove(mPrefix + key); + return this; + } + + @Override + public Editor clear() { + mEditorBase.clear(); + return this; + } + + @Override + public boolean commit() { + return mEditorBase.commit(); + } + + @Override + public void apply() { + mEditorBase.apply(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java b/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java index 7b7111be4..5c61309b3 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/RepoLoader.java @@ -1,5 +1,22 @@ package de.robv.android.xposed.installer.util; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.annotation.NonNull; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -15,14 +32,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.GZIPInputStream; -import android.content.Context; -import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; +import de.robv.android.xposed.installer.DownloadFragment; import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.WelcomeActivity; import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.repo.ModuleVersion; @@ -34,375 +46,399 @@ import de.robv.android.xposed.installer.util.DownloadsUtil.SyncDownloadInfo; public class RepoLoader { - private static RepoLoader mInstance = null; - private XposedApp mApp = null; - private SharedPreferences mPref; - private SharedPreferences mModulePref; - private ConnectivityManager mConMgr; - - private boolean mIsLoading = false; - private boolean mReloadTriggeredOnce = false; - private final List mListeners = new CopyOnWriteArrayList(); - - private static final int UPDATE_FREQUENCY = 24 * 60 * 60 * 1000; - private static final String DEFAULT_REPOSITORIES = "http://dl.xposed.info/repo/full.xml.gz"; - private Map mRepositories = null; - - private ReleaseType mGlobalReleaseType; - private final Map mLocalReleaseTypesCache = new HashMap(); - - private RepoLoader() { - mInstance = this; - mApp = XposedApp.getInstance(); - mPref = mApp.getSharedPreferences("repo", Context.MODE_PRIVATE); - mModulePref = mApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE); - mConMgr = (ConnectivityManager) mApp.getSystemService(Context.CONNECTIVITY_SERVICE); - mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences() - .getString("release_type_global", "stable")); - - RepoDb.init(mApp, this); - refreshRepositories(); - } - - public static synchronized RepoLoader getInstance() { - if (mInstance == null) - new RepoLoader(); - return mInstance; - } - - public boolean refreshRepositories() { - mRepositories = RepoDb.getRepositories(); - - // Unlikely case (usually only during initial load): DB state doesn't fit to configuration - boolean needReload = false; - String[] config = mPref.getString("repositories", DEFAULT_REPOSITORIES).split("\\|"); - if (mRepositories.size() != config.length) { - needReload = true; - } else { - int i = 0; - for (Repository repo : mRepositories.values()) { - if (!repo.url.equals(config[i++])) { - needReload = true; - break; - } - } - } - - if (!needReload) - return false; - - clear(false); - for (String url : config) { - RepoDb.insertRepository(url); - } - mRepositories = RepoDb.getRepositories(); - return true; - } - - public void setReleaseTypeGlobal(String relTypeString) { - ReleaseType relType = ReleaseType.fromString(relTypeString); - if (mGlobalReleaseType == relType) - return; - - mGlobalReleaseType = relType; - - // Updating the latest version for all modules takes a moment - new Thread("DBUpdate") { - @Override - public void run() { - RepoDb.updateAllModulesLatestVersion(); - notifyListeners(); - } - }.start(); - } - - public void setReleaseTypeLocal(String packageName, String relTypeString) { - ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) - ? ReleaseType.fromString(relTypeString) : null; - - if (getReleaseTypeLocal(packageName) == relType) - return; - - synchronized (mLocalReleaseTypesCache) { - mLocalReleaseTypesCache.put(packageName, relType); - } - - RepoDb.updateModuleLatestVersion(packageName); - notifyListeners(); - } - - private ReleaseType getReleaseTypeLocal(String packageName) { - synchronized (mLocalReleaseTypesCache) { - if (mLocalReleaseTypesCache.containsKey(packageName)) - return mLocalReleaseTypesCache.get(packageName); - - String value = mModulePref.getString(packageName + "_release_type", null); - ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null; - mLocalReleaseTypesCache.put(packageName, result); - return result; - } - } - - public Repository getRepository(long repoId) { - return mRepositories.get(repoId); - } - - public Module getModule(String packageName) { - return RepoDb.getModuleByPackageName(packageName); - } - - public ModuleVersion getLatestVersion(Module module) { - if (module == null || module.versions.isEmpty()) - return null; - - for (ModuleVersion version : module.versions) { - if (version.downloadLink != null && isVersionShown(version)) - return version; - } - return null; - } - - public boolean isVersionShown(ModuleVersion version) { - return version.relType.ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal(); - } - - public ReleaseType getMaxShownReleaseType(String packageName) { - ReleaseType localSetting = getReleaseTypeLocal(packageName); - if (localSetting != null) - return localSetting; - else - return mGlobalReleaseType; - } - - public void triggerReload(final boolean force) { - mReloadTriggeredOnce = true; - - if (!mApp.areDownloadsEnabled()) - return; - - if (force) { - resetLastUpdateCheck(); - } else { - long lastUpdateCheck = mPref.getLong("last_update_check", 0); - if (System.currentTimeMillis() < lastUpdateCheck + UPDATE_FREQUENCY) - return; - } - - NetworkInfo netInfo = mConMgr.getActiveNetworkInfo(); - if (netInfo == null || !netInfo.isConnected()) - return; - - synchronized (this) { - if (mIsLoading) - return; - mIsLoading = true; - } - mApp.updateProgressIndicator(); - - new Thread("RepositoryReload") { - public void run() { - final List messages = new LinkedList(); - boolean hasChanged = downloadAndParseFiles(messages); - - mPref.edit().putLong("last_update_check", System.currentTimeMillis()).commit(); - - if (!messages.isEmpty()) { - XposedApp.runOnUiThread(new Runnable() { - public void run() { - for (String message : messages) { - Toast.makeText(mApp, message, Toast.LENGTH_LONG).show(); - } - } - }); - } - - if (hasChanged) - notifyListeners(); - - synchronized (this) { - mIsLoading = false; - } - mApp.updateProgressIndicator(); - } - }.start(); - } - - public void triggerFirstLoadIfNecessary() { - if (!mReloadTriggeredOnce) - triggerReload(false); - } - - public void resetLastUpdateCheck() { - mPref.edit().remove("last_update_check").commit(); - } - - public synchronized boolean isLoading() { - return mIsLoading; - } - - public void clear(boolean notify) { - synchronized (this) { - // TODO Stop reloading repository when it should be cleared - if (mIsLoading) - return; - - RepoDb.deleteRepositories(); - mRepositories = new LinkedHashMap(0); - DownloadsUtil.clearCache(null); - resetLastUpdateCheck(); - } - - if (notify) - notifyListeners(); - } - - public void setRepositories(String... repos) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < repos.length; i++) { - if (i > 0) - sb.append("|"); - sb.append(repos[i]); - } - mPref.edit().putString("repositories", sb.toString()).commit(); - if (refreshRepositories()) - triggerReload(true); - } - - public boolean hasModuleUpdates() { - if (!mApp.areDownloadsEnabled()) - return false; - - return RepoDb.hasModuleUpdates(); - } - - public String getFrameworkUpdateVersion() { - if (!mApp.areDownloadsEnabled()) - return null; - - return RepoDb.getFrameworkUpdateVersion(); - } - - private File getRepoCacheFile(String repo) { - String filename = "repo_" + HashUtil.md5(repo) + ".xml"; - if (repo.endsWith(".gz")) - filename += ".gz"; - return new File(mApp.getCacheDir(), filename); - } - - private boolean downloadAndParseFiles(List messages) { - // These variables don't need to be atomic, just mutable - final AtomicBoolean hasChanged = new AtomicBoolean(false); - final AtomicInteger insertCounter = new AtomicInteger(); - final AtomicInteger deleteCounter = new AtomicInteger(); - - for (Entry repoEntry : mRepositories.entrySet()) { - final long repoId = repoEntry.getKey(); - final Repository repo = repoEntry.getValue(); - - String url = (repo.partialUrl != null && repo.version != null) - ? String.format(repo.partialUrl, repo.version) : repo.url; - - File cacheFile = getRepoCacheFile(url); - SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url, cacheFile); - - Log.i(XposedApp.TAG, String.format("Downloaded %s with status %d (error: %s), size %d bytes", - url, info.status, info.errorMessage, cacheFile.length())); - - if (info.status != SyncDownloadInfo.STATUS_SUCCESS) { - if (info.errorMessage != null) - messages.add(info.errorMessage); - continue; - } - - InputStream in = null; - RepoDb.beginTransation(); - try { - in = new FileInputStream(cacheFile); - if (url.endsWith(".gz")) - in = new GZIPInputStream(in); - - RepoParser.parse(in, new RepoParserCallback() { - @Override - public void onRepositoryMetadata(Repository repository) { - if (!repository.isPartial) { - RepoDb.deleteAllModules(repoId); - hasChanged.set(true); - } - } - - @Override - public void onNewModule(Module module) { - RepoDb.insertModule(repoId, module); - hasChanged.set(true); - insertCounter.incrementAndGet(); - } - - @Override - public void onRemoveModule(String packageName) { - RepoDb.deleteModule(repoId, packageName); - hasChanged.set(true); - deleteCounter.decrementAndGet(); - } - - @Override - public void onCompleted(Repository repository) { - if (!repository.isPartial) { - RepoDb.updateRepository(repoId, repository); - repo.name = repository.name; - repo.partialUrl = repository.partialUrl; - repo.version = repository.version; - } else { - RepoDb.updateRepositoryVersion(repoId, repository.version); - repo.version = repository.version; - } - - Log.i(XposedApp.TAG, String.format("Updated repository %s to version %s (%d new / %d removed modules)", - repo.url, repo.version, insertCounter.get(), deleteCounter.get())); - } - }); - - RepoDb.setTransactionSuccessful(); - - } catch (Throwable t) { - Log.e(XposedApp.TAG, "Cannot load repository from " + url, t); - messages.add(mApp.getString(R.string.repo_load_failed, url, t.getMessage())); - DownloadsUtil.clearCache(url); - - } finally { - if (in != null) - try { in.close(); } catch (IOException ignored) {} - cacheFile.delete(); - RepoDb.endTransation(); - } - } - - // TODO Set ModuleColumns.PREFERRED for modules which appear in multiple repositories - return hasChanged.get(); - } - - - public void addListener(RepoListener listener, boolean triggerImmediately) { - if (!mListeners.contains(listener)) - mListeners.add(listener); - - if (triggerImmediately) - listener.onRepoReloaded(this); - } - - public void removeListener(RepoListener listener) { - mListeners.remove(listener); - } - - private void notifyListeners() { - for (RepoListener listener : mListeners) { - listener.onRepoReloaded(mInstance); - } - } - - public interface RepoListener { - /** - * Called whenever the list of modules from repositories has been successfully reloaded - */ - public void onRepoReloaded(RepoLoader loader); - } + private static final int UPDATE_FREQUENCY = 24 * 60 * 60 * 1000; + private static final String DEFAULT_REPOSITORIES = "https://dl-xda.xposed.info/repo/full.xml.gz"; + private static RepoLoader mInstance = null; + private final List mListeners = new CopyOnWriteArrayList<>(); + private final Map mLocalReleaseTypesCache = new HashMap<>(); + private XposedApp mApp = null; + private SharedPreferences mPref; + private SharedPreferences mModulePref; + private ConnectivityManager mConMgr; + private boolean mIsLoading = false; + private boolean mReloadTriggeredOnce = false; + private Map mRepositories = null; + private ReleaseType mGlobalReleaseType; + private SwipeRefreshLayout mSwipeRefreshLayout; + + private RepoLoader() { + mInstance = this; + mApp = XposedApp.getInstance(); + mPref = mApp.getSharedPreferences("repo", Context.MODE_PRIVATE); + mModulePref = mApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE); + mConMgr = (ConnectivityManager) mApp.getSystemService(Context.CONNECTIVITY_SERVICE); + mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences().getString("release_type_global", "stable")); + refreshRepositories(); + } + + public static synchronized RepoLoader getInstance() { + if (mInstance == null) + new RepoLoader(); + return mInstance; + } + + public boolean refreshRepositories() { + mRepositories = RepoDb.getRepositories(); + + // Unlikely case (usually only during initial load): DB state doesn't + // fit to configuration + boolean needReload = false; + String[] config = mPref.getString("repositories", DEFAULT_REPOSITORIES).split("\\|"); + if (mRepositories.size() != config.length) { + needReload = true; + } else { + int i = 0; + for (Repository repo : mRepositories.values()) { + if (!repo.url.equals(config[i++])) { + needReload = true; + break; + } + } + } + + if (!needReload) + return false; + + clear(false); + for (String url : config) { + RepoDb.insertRepository(url); + } + mRepositories = RepoDb.getRepositories(); + return true; + } + + public void setReleaseTypeGlobal(String relTypeString) { + ReleaseType relType = ReleaseType.fromString(relTypeString); + if (mGlobalReleaseType == relType) + return; + + mGlobalReleaseType = relType; + + // Updating the latest version for all modules takes a moment + new Thread("DBUpdate") { + @Override + public void run() { + RepoDb.updateAllModulesLatestVersion(); + notifyListeners(); + } + }.start(); + } + + public void setReleaseTypeLocal(String packageName, String relTypeString) { + ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) ? ReleaseType.fromString(relTypeString) : null; + + if (getReleaseTypeLocal(packageName) == relType) + return; + + synchronized (mLocalReleaseTypesCache) { + mLocalReleaseTypesCache.put(packageName, relType); + } + + RepoDb.updateModuleLatestVersion(packageName); + notifyListeners(); + } + + private ReleaseType getReleaseTypeLocal(String packageName) { + synchronized (mLocalReleaseTypesCache) { + if (mLocalReleaseTypesCache.containsKey(packageName)) + return mLocalReleaseTypesCache.get(packageName); + + String value = mModulePref.getString(packageName + "_release_type", + null); + ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null; + mLocalReleaseTypesCache.put(packageName, result); + return result; + } + } + + public Repository getRepository(long repoId) { + return mRepositories.get(repoId); + } + + public Module getModule(String packageName) { + return RepoDb.getModuleByPackageName(packageName); + } + + public ModuleVersion getLatestVersion(Module module) { + if (module == null || module.versions.isEmpty()) + return null; + + for (ModuleVersion version : module.versions) { + if (version.downloadLink != null && isVersionShown(version)) + return version; + } + return null; + } + + public boolean isVersionShown(ModuleVersion version) { + return version.relType + .ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal(); + } + + public ReleaseType getMaxShownReleaseType(String packageName) { + ReleaseType localSetting = getReleaseTypeLocal(packageName); + if (localSetting != null) + return localSetting; + else + return mGlobalReleaseType; + } + + public void triggerReload(final boolean force) { + mReloadTriggeredOnce = true; + + if (force) { + resetLastUpdateCheck(); + } else { + long lastUpdateCheck = mPref.getLong("last_update_check", 0); + if (System.currentTimeMillis() < lastUpdateCheck + UPDATE_FREQUENCY) + return; + } + + NetworkInfo netInfo = mConMgr.getActiveNetworkInfo(); + if (netInfo == null || !netInfo.isConnected()) + return; + + synchronized (this) { + if (mIsLoading) + return; + mIsLoading = true; + } + mApp.updateProgressIndicator(mSwipeRefreshLayout); + + new Thread("RepositoryReload") { + public void run() { + final List messages = new LinkedList<>(); + boolean hasChanged = downloadAndParseFiles(messages); + + mPref.edit().putLong("last_update_check", System.currentTimeMillis()).apply(); + + if (!messages.isEmpty()) { + XposedApp.runOnUiThread(new Runnable() { + public void run() { + for (String message : messages) { + Toast.makeText(mApp, message, Toast.LENGTH_LONG).show(); + } + } + }); + } + + if (hasChanged) + notifyListeners(); + + synchronized (this) { + mIsLoading = false; + } + mApp.updateProgressIndicator(mSwipeRefreshLayout); + } + }.start(); + } + + public void setSwipeRefreshLayout(SwipeRefreshLayout mSwipeRefreshLayout) { + this.mSwipeRefreshLayout = mSwipeRefreshLayout; + } + + public void triggerFirstLoadIfNecessary() { + if (!mReloadTriggeredOnce) + triggerReload(false); + } + + public void resetLastUpdateCheck() { + mPref.edit().remove("last_update_check").apply(); + } + + public synchronized boolean isLoading() { + return mIsLoading; + } + + public void clear(boolean notify) { + synchronized (this) { + // TODO Stop reloading repository when it should be cleared + if (mIsLoading) + return; + + RepoDb.deleteRepositories(); + mRepositories = new LinkedHashMap(0); + DownloadsUtil.clearCache(null); + resetLastUpdateCheck(); + } + + if (notify) + notifyListeners(); + } + + public void setRepositories(String... repos) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < repos.length; i++) { + if (i > 0) + sb.append("|"); + sb.append(repos[i]); + } + mPref.edit().putString("repositories", sb.toString()).apply(); + if (refreshRepositories()) + triggerReload(true); + } + + public boolean hasModuleUpdates() { + return RepoDb.hasModuleUpdates(); + } + + public String getFrameworkUpdateVersion() { + return RepoDb.getFrameworkUpdateVersion(); + } + + private File getRepoCacheFile(String repo) { + String filename = "repo_" + HashUtil.md5(repo) + ".xml"; + if (repo.endsWith(".gz")) + filename += ".gz"; + return new File(mApp.getCacheDir(), filename); + } + + private boolean downloadAndParseFiles(List messages) { + // These variables don't need to be atomic, just mutable + final AtomicBoolean hasChanged = new AtomicBoolean(false); + final AtomicInteger insertCounter = new AtomicInteger(); + final AtomicInteger deleteCounter = new AtomicInteger(); + + for (Entry repoEntry : mRepositories.entrySet()) { + final long repoId = repoEntry.getKey(); + final Repository repo = repoEntry.getValue(); + + String url = (repo.partialUrl != null && repo.version != null) ? String.format(repo.partialUrl, repo.version) : repo.url; + + File cacheFile = getRepoCacheFile(url); + SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url, + cacheFile); + + Log.i(XposedApp.TAG, String.format( + "RepoLoader -> Downloaded %s with status %d (error: %s), size %d bytes", + url, info.status, info.errorMessage, cacheFile.length())); + + if (info.status != SyncDownloadInfo.STATUS_SUCCESS) { + if (info.errorMessage != null) + messages.add(info.errorMessage); + continue; + } + + InputStream in = null; + RepoDb.beginTransation(); + try { + in = new FileInputStream(cacheFile); + if (url.endsWith(".gz")) + in = new GZIPInputStream(in); + + RepoParser.parse(in, new RepoParserCallback() { + @Override + public void onRepositoryMetadata(Repository repository) { + if (!repository.isPartial) { + RepoDb.deleteAllModules(repoId); + hasChanged.set(true); + } + } + + @Override + public void onNewModule(Module module) { + RepoDb.insertModule(repoId, module); + hasChanged.set(true); + insertCounter.incrementAndGet(); + } + + @Override + public void onRemoveModule(String packageName) { + RepoDb.deleteModule(repoId, packageName); + hasChanged.set(true); + deleteCounter.decrementAndGet(); + } + + @Override + public void onCompleted(Repository repository) { + if (!repository.isPartial) { + RepoDb.updateRepository(repoId, repository); + repo.name = repository.name; + repo.partialUrl = repository.partialUrl; + repo.version = repository.version; + } else { + RepoDb.updateRepositoryVersion(repoId, repository.version); + repo.version = repository.version; + } + + Log.i(XposedApp.TAG, String.format( + "RepoLoader -> Updated repository %s to version %s (%d new / %d removed modules)", + repo.url, repo.version, insertCounter.get(), + deleteCounter.get())); + } + }); + + RepoDb.setTransactionSuccessful(); + } catch (SQLiteException e) { + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + new MaterialDialog.Builder(DownloadFragment.sActivity) + .title(R.string.restart_needed) + .content(R.string.cache_cleaned) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + Intent i = new Intent(DownloadFragment.sActivity, WelcomeActivity.class); + i.putExtra("fragment", 2); + + PendingIntent pi = PendingIntent.getActivity(DownloadFragment.sActivity, 0, i, PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager mgr = (AlarmManager) mApp.getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pi); + System.exit(0); + } + }) + .positiveText(android.R.string.ok) + .canceledOnTouchOutside(false) + .show(); + } + }); + + DownloadsUtil.clearCache(url); + } catch (Throwable t) { + Log.e(XposedApp.TAG, "RepoLoader -> Cannot load repository from " + url, t); + messages.add(mApp.getString(R.string.repo_load_failed, url, t.getMessage())); + DownloadsUtil.clearCache(url); + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + cacheFile.delete(); + RepoDb.endTransation(); + } + } + + // TODO Set ModuleColumns.PREFERRED for modules which appear in multiple + // repositories + return hasChanged.get(); + } + + public void addListener(RepoListener listener, boolean triggerImmediately) { + if (!mListeners.contains(listener)) + mListeners.add(listener); + + if (triggerImmediately) + listener.onRepoReloaded(this); + } + + public void removeListener(RepoListener listener) { + mListeners.remove(listener); + } + + private void notifyListeners() { + for (RepoListener listener : mListeners) { + listener.onRepoReloaded(mInstance); + } + } + + public interface RepoListener { + /** + * Called whenever the list of modules from repositories has been + * successfully reloaded + */ + void onRepoReloaded(RepoLoader loader); + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java index 646681e21..492d7cb0f 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/RootUtil.java @@ -1,124 +1,349 @@ package de.robv.android.xposed.installer.util; -import java.util.List; - +import android.content.Context; import android.os.Handler; import android.os.HandlerThread; +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.installer.R; +import de.robv.android.xposed.installer.XposedApp; +import de.robv.android.xposed.installer.installation.FlashCallback; import eu.chainfire.libsuperuser.Shell; import eu.chainfire.libsuperuser.Shell.OnCommandResultListener; +import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; + public class RootUtil { - private Shell.Interactive mShell = null; - private HandlerThread mCallbackThread = null; - - private boolean mCommandRunning = false; - private int mLastExitCode = -1; - private List mLastOutput = null; - - private OnCommandResultListener commandResultListener = new OnCommandResultListener() { - @Override - public void onCommandResult(int commandCode, int exitCode, List output) { - mLastExitCode = exitCode; - mLastOutput = output; - synchronized (mCallbackThread) { - mCommandRunning = false; - mCallbackThread.notifyAll(); - } - } - }; - - private void waitForCommandFinished() { - synchronized (mCallbackThread) { - while (mCommandRunning) { - try { - mCallbackThread.wait(); - } catch (InterruptedException ignored) {} - } - } - - if (mLastExitCode == OnCommandResultListener.WATCHDOG_EXIT - || mLastExitCode == OnCommandResultListener.SHELL_DIED) - dispose(); - } - - /** - * Starts an interactive shell with root permissions. - * Does nothing if already running. - * - * @return true if root access is available, false otherwise - */ - public synchronized boolean startShell() { - if (mShell != null) { - if (mShell.isRunning()) - return true; - else - dispose(); - } - - mCallbackThread = new HandlerThread("su callback listener"); - mCallbackThread.start(); - - mCommandRunning = true; - mShell = new Shell.Builder() - .useSU() - .setHandler(new Handler(mCallbackThread.getLooper())) - .setWantSTDERR(true) - .setWatchdogTimeout(10) - .open(commandResultListener); - - waitForCommandFinished(); - - if (mLastExitCode != OnCommandResultListener.SHELL_RUNNING) { - dispose(); - return false; - } - - return true; - } - - /** - * Closes all resources related to the shell. - */ - public synchronized void dispose() { - if (mShell == null) - return; - - try { - mShell.close(); - } catch (Exception ignored) {} - mShell = null; - - mCallbackThread.quit(); - mCallbackThread = null; - } - - /** - * Executes a single command, waits for its termination and returns the result - */ - public synchronized int execute(String command, List output) { - if (mShell == null) - throw new IllegalStateException("shell is not running"); - - mCommandRunning = true; - mShell.addCommand(command, 0, commandResultListener); - waitForCommandFinished(); - - if (output != null) - output.addAll(mLastOutput); - - return mLastExitCode; - } - - /** - * Executes a single command via the bundled BusyBox executable - */ - public int executeWithBusybox(String command, List output) { - AssetUtil.extractBusybox(); - return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command, output); - } - - @Override - protected void finalize() throws Throwable { - dispose(); - } + private Shell.Interactive mShell = null; + private HandlerThread mCallbackThread = null; + + private boolean mCommandRunning = false; + private int mLastExitCode = -1; + private LineCallback mCallback = null; + + private static final String EMULATED_STORAGE_SOURCE; + private static final String EMULATED_STORAGE_TARGET; + + static { + EMULATED_STORAGE_SOURCE = getEmulatedStorageVariable("EMULATED_STORAGE_SOURCE"); + EMULATED_STORAGE_TARGET = getEmulatedStorageVariable("EMULATED_STORAGE_TARGET"); + } + + public interface LineCallback { + void onLine(String line); + void onErrorLine(String line); + } + + public static class CollectingLineCallback implements LineCallback { + protected List mLines = new LinkedList<>(); + + @Override + public void onLine(String line) { + mLines.add(line); + } + + @Override + public void onErrorLine(String line) { + mLines.add(line); + } + + public List getLines() { + return mLines; + } + + @Override + public String toString() { + return TextUtils.join("\n", mLines); + } + } + + private static String getEmulatedStorageVariable(String variable) { + String result = System.getenv(variable); + if (result != null) { + result = getCanonicalPath(new File(result)); + if (!result.endsWith("/")) { + result += "/"; + } + } + return result; + } + + + private final Shell.OnCommandResultListener mOpenListener = new Shell.OnCommandResultListener() { + @Override + public void onCommandResult(int commandCode, int exitCode, List output) { + mStdoutListener.onCommandResult(commandCode, exitCode); + } + }; + + private final Shell.OnCommandLineListener mStdoutListener = new Shell.OnCommandLineListener() { + public void onLine(String line) { + if (mCallback != null) { + mCallback.onLine(line); + } + } + + @Override + public void onCommandResult(int commandCode, int exitCode) { + mLastExitCode = exitCode; + synchronized (mCallbackThread) { + mCommandRunning = false; + mCallbackThread.notifyAll(); + } + } + }; + + private final Shell.OnCommandLineListener mStderrListener = new Shell.OnCommandLineListener() { + @Override + public void onLine(String line) { + if (mCallback != null) { + mCallback.onErrorLine(line); + } + } + + @Override + public void onCommandResult(int commandCode, int exitCode) { + // Not called for STDERR listener. + } + }; + + private void waitForCommandFinished() { + synchronized (mCallbackThread) { + while (mCommandRunning) { + try { + mCallbackThread.wait(); + } catch (InterruptedException ignored) { + } + } + } + + if (mLastExitCode == OnCommandResultListener.WATCHDOG_EXIT || mLastExitCode == OnCommandResultListener.SHELL_DIED) { + dispose(); + } + } + + /** + * Starts an interactive shell with root permissions. Does nothing if + * already running. + * + * @return true if root access is available, false otherwise + */ + public synchronized boolean startShell() { + if (mShell != null) { + if (mShell.isRunning()) { + return true; + } else { + dispose(); + } + } + + mCallbackThread = new HandlerThread("su callback listener"); + mCallbackThread.start(); + + mCommandRunning = true; + mShell = new Shell.Builder().useSU() + .setHandler(new Handler(mCallbackThread.getLooper())) + .setOnSTDERRLineListener(mStderrListener) + .open(mOpenListener); + + waitForCommandFinished(); + + if (mLastExitCode != OnCommandResultListener.SHELL_RUNNING) { + dispose(); + return false; + } + + return true; + } + + public boolean startShell(FlashCallback flashCallback) { + if (!startShell()) { + triggerError(flashCallback, FlashCallback.ERROR_NO_ROOT_ACCESS); + return false; + } + return true; + } + + /** + * Closes all resources related to the shell. + */ + public synchronized void dispose() { + if (mShell == null) { + return; + } + + try { + mShell.close(); + } catch (Exception ignored) { + } + mShell = null; + + mCallbackThread.quit(); + mCallbackThread = null; + } + + public synchronized int execute(String command, LineCallback callback) { + if (mShell == null) { + throw new IllegalStateException("shell is not running"); + } + + mCallback = callback; + mCommandRunning = true; + mShell.addCommand(command, 0, mStdoutListener); + waitForCommandFinished(); + + return mLastExitCode; + } + + /** + * Executes a single command, waits for its termination and returns the + * result + */ + public synchronized int execute(String command, final List output) { + return execute(command, new LineCallback() { + @Override + public void onLine(String line) { + output.add(line); + } + + @Override + public void onErrorLine(String line) { + output.add(line); + } + }); + } + + public synchronized int execute(String command) { + return execute(command, (LineCallback) null); + } + + public int executeWithBusybox(String command, LineCallback callback) { + AssetUtil.extractBusybox(); + return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command, callback); + } + + /** + * Executes a single command via the bundled BusyBox executable + */ + public int executeWithBusybox(String command, List output) { + AssetUtil.extractBusybox(); + return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command, output); + } + + public int executeWithBusybox(String command) { + AssetUtil.extractBusybox(); + return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command); + } + + private static String getCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + Log.w(XposedApp.TAG, "Could not get canonical path for " + file); + return file.getAbsolutePath(); + } + } + + public static String getShellPath(File file) { + return getShellPath(getCanonicalPath(file)); + } + + public static String getShellPath(String path) { + if (EMULATED_STORAGE_SOURCE != null && EMULATED_STORAGE_TARGET != null + && path.startsWith(EMULATED_STORAGE_TARGET)) { + path = EMULATED_STORAGE_SOURCE + path.substring(EMULATED_STORAGE_TARGET.length()); + } + return path; + } + + @Override + protected void finalize() throws Throwable { + dispose(); + } + + public enum RebootMode { + NORMAL(R.string.reboot), + SOFT(R.string.soft_reboot), + RECOVERY(R.string.reboot_recovery); + + public final int titleRes; + + RebootMode(@StringRes int titleRes) { + this.titleRes = titleRes; + } + + public static RebootMode fromId(@IdRes int id) { + switch (id) { + case R.id.reboot: + return NORMAL; + case R.id.soft_reboot: + return SOFT; + case R.id.reboot_recovery: + return RECOVERY; + default: + throw new IllegalArgumentException(); + } + } + } + + public static boolean reboot(RebootMode mode, @NonNull Context context) { + RootUtil rootUtil = new RootUtil(); + if (!rootUtil.startShell()) { + NavUtil.showMessage(context, context.getString(R.string.root_failed)); + return false; + } + + LineCallback callback = new CollectingLineCallback(); + if (!rootUtil.reboot(mode, callback)) { + StringBuilder message = new StringBuilder(callback.toString()); + if (message.length() > 0) { + message.append("\n\n"); + } + message.append(context.getString(R.string.reboot_failed)); + NavUtil.showMessage(context, message); + return false; + } + + return true; + } + + public boolean reboot(RebootMode mode, LineCallback callback) { + switch (mode) { + case NORMAL: + return reboot(callback); + case SOFT: + return softReboot(callback); + case RECOVERY: + return rebootToRecovery(callback); + default: + throw new IllegalArgumentException(); + } + } + + private boolean reboot(LineCallback callback) { + return executeWithBusybox("reboot", callback) == 0; + } + + private boolean softReboot(LineCallback callback) { + return execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", callback) == 0; + } + + private boolean rebootToRecovery(LineCallback callback) { + // Create a flag used by some kernels to boot into recovery. + if (execute("ls /cache/recovery") != 0) { + executeWithBusybox("mkdir /cache/recovery", callback); + } + executeWithBusybox("touch /cache/recovery/boot", callback); + + return executeWithBusybox("reboot recovery", callback) == 0; + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java index 0e82c83a8..01e35d56f 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java +++ b/app/src/main/java/de/robv/android/xposed/installer/util/ThemeUtil.java @@ -3,40 +3,41 @@ import android.content.Context; import android.content.res.Resources.Theme; import android.content.res.TypedArray; + import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.XposedBaseActivity; public final class ThemeUtil { - private ThemeUtil() {}; - - private static int[] THEMES = new int[] { - R.style.Theme_XposedInstaller_Light, - R.style.Theme_XposedInstaller_Dark, - R.style.Theme_XposedInstaller_Dark_Black, - }; - - public static int getSelectTheme() { - int theme = XposedApp.getPreferences().getInt("theme", 0); - return (theme >= 0 && theme < THEMES.length) ? theme : 0; - } - - public static void setTheme(XposedBaseActivity activity) { - activity.mTheme = getSelectTheme(); - activity.setTheme(THEMES[activity.mTheme]); - } - - public static void reloadTheme(XposedBaseActivity activity) { - int theme = getSelectTheme(); - if (theme != activity.mTheme) - activity.recreate(); - } - - public static int getThemeColor(Context context, int id) { - Theme theme = context.getTheme(); - TypedArray a = theme.obtainStyledAttributes(new int[] {id}); - int result = a.getColor(0, 0); - a.recycle(); - return result; - } + private static int[] THEMES = new int[]{ + R.style.Theme_XposedInstaller_Light, + R.style.Theme_XposedInstaller_Dark, + R.style.Theme_XposedInstaller_Dark_Black,}; + + private ThemeUtil() { + } + + public static int getSelectTheme() { + int theme = XposedApp.getPreferences().getInt("theme", 0); + return (theme >= 0 && theme < THEMES.length) ? theme : 0; + } + + public static void setTheme(XposedBaseActivity activity) { + activity.mTheme = getSelectTheme(); + activity.setTheme(THEMES[activity.mTheme]); + } + + public static void reloadTheme(XposedBaseActivity activity) { + int theme = getSelectTheme(); + if (theme != activity.mTheme) + activity.recreate(); + } + + public static int getThemeColor(Context context, int id) { + Theme theme = context.getTheme(); + TypedArray a = theme.obtainStyledAttributes(new int[]{id}); + int result = a.getColor(0, 0); + a.recycle(); + return result; + } } diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/UIUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/UIUtil.java deleted file mode 100644 index 81187db85..000000000 --- a/app/src/main/java/de/robv/android/xposed/installer/util/UIUtil.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.robv.android.xposed.installer.util; - -import android.os.Build; - -public class UIUtil { - public static boolean isLollipop() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } -} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java new file mode 100644 index 000000000..b990733ca --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabActivityHelper.java @@ -0,0 +1,164 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; + +import java.util.List; + +/** + * This is a helper class to manage the connection to the Custom Tabs Service. + */ +public class CustomTabActivityHelper implements ServiceConnectionCallback { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening + * it on a WebView. + * + * @param activity The host activity. + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available. + * @param uri the Uri to be opened. + * @param fallback a CustomTabFallback to be used if Custom Tabs is not + * available. + */ + public static void openCustomTab(Activity activity, CustomTabsIntent customTabsIntent, Uri uri, CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + // If we cant find a package name, it means theres no browser that + // supports + // Chrome Custom Tabs installed. So, we fallback to the webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Unbinds the Activity from the Custom Tabs Service. + * + * @param activity the activity that is connected to the service. + */ + public void unbindCustomTabsService(Activity activity) { + if (mConnection == null) + return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + mConnection = null; + } + + /** + * Creates or retrieves an exiting CustomTabsSession. + * + * @return a CustomTabsSession. + */ + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(null); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the + * Custom Tabs Service. + * + * @param connectionCallback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service. + * + * @param activity the activity to be binded to the service. + */ + public void bindCustomTabsService(Activity activity) { + if (mClient != null) + return; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) + return; + + mConnection = new ServiceConnection(this); + CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + /** + * @return true if call to mayLaunchUrl was accepted. + * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}. + */ + public boolean mayLaunchUrl(Uri uri, Bundle extras, + List otherLikelyBundles) { + if (mClient == null) + return false; + + CustomTabsSession session = getSession(); + if (session == null) + return false; + + return session.mayLaunchUrl(uri, extras, otherLikelyBundles); + } + + @Override + public void onServiceConnected(CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) + mConnectionCallback.onCustomTabsConnected(); + } + + @Override + public void onServiceDisconnected() { + mClient = null; + mCustomTabsSession = null; + if (mConnectionCallback != null) + mConnectionCallback.onCustomTabsDisconnected(); + } + + /** + * A Callback for when the service is connected or disconnected. Use those + * callbacks to handle UI changes when the service is connected or + * disconnected. + */ + public interface ConnectionCallback { + /** + * Called when the service is connected. + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected. + */ + void onCustomTabsDisconnected(); + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not + * available. + */ + public interface CustomTabFallback { + /** + * @param activity The Activity that wants to open the Uri. + * @param uri The uri to be opened by the fallback. + */ + void openUri(Activity activity, Uri uri); + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java new file mode 100644 index 000000000..86beabf36 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsHelper.java @@ -0,0 +1,128 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. + */ +public class CustomTabsHelper { + static final String STABLE_PACKAGE = "com.android.chrome"; + static final String BETA_PACKAGE = "com.chrome.beta"; + static final String DEV_PACKAGE = "com.chrome.dev"; + static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + private static final String TAG = "CustomTabsHelper"; + private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE"; + private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() { + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. + * Picks the one chosen by the user if there is one, otherwise makes a best + * effort to return a valid package name. + *

    + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs + * related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) + return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle + // both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE; + } + return sPackageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, + Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) + continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) + continue; + if (resolveInfo.activityInfo == null) + continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs + * feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java new file mode 100644 index 000000000..ff5c4493b --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/CustomTabsURLSpan.java @@ -0,0 +1,26 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.text.style.URLSpan; +import android.view.View; + +import de.robv.android.xposed.installer.util.NavUtil; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class CustomTabsURLSpan extends URLSpan { + + private Activity activity; + + public CustomTabsURLSpan(Activity activity, String url) { + super(url); + this.activity = activity; + } + + @Override + public void onClick(View widget) { + String url = getURL(); + NavUtil.startURL(activity, url); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java new file mode 100644 index 000000000..589d8d23a --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/LinkTransformationMethod.java @@ -0,0 +1,50 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.app.Activity; +import android.graphics.Rect; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.TransformationMethod; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.view.View; +import android.widget.TextView; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class LinkTransformationMethod implements TransformationMethod { + + private Activity activity; + + public LinkTransformationMethod(Activity activity) { + this.activity = activity; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + Linkify.addLinks(textView, Linkify.WEB_URLS); + if (textView.getText() == null || !(textView.getText() instanceof Spannable)) { + return source; + } + Spannable text = (Spannable) textView.getText(); + URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class); + for (int i = spans.length - 1; i >= 0; i--) { + URLSpan oldSpan = spans[i]; + int start = text.getSpanStart(oldSpan); + int end = text.getSpanEnd(oldSpan); + String url = oldSpan.getURL(); + text.removeSpan(oldSpan); + text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return text; + } + return source; + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) { + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java new file mode 100644 index 000000000..f90a1e0f6 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnection.java @@ -0,0 +1,34 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.content.ComponentName; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsServiceConnection; + +import java.lang.ref.WeakReference; + +/** + * Implementation for the CustomTabsServiceConnection that avoids leaking the + * ServiceConnectionCallback + */ +public class ServiceConnection extends CustomTabsServiceConnection { + // A weak reference to the ServiceConnectionCallback to avoid leaking it. + private WeakReference mConnectionCallback; + + public ServiceConnection(ServiceConnectionCallback connectionCallback) { + mConnectionCallback = new WeakReference<>(connectionCallback); + } + + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) + connectionCallback.onServiceConnected(client); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); + if (connectionCallback != null) + connectionCallback.onServiceDisconnected(); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java new file mode 100644 index 000000000..6e92a44f9 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/chrome/ServiceConnectionCallback.java @@ -0,0 +1,21 @@ +package de.robv.android.xposed.installer.util.chrome; + +import android.support.customtabs.CustomTabsClient; + +/** + * Callback for events when connecting and disconnecting from Custom Tabs + * Service. + */ +public interface ServiceConnectionCallback { + /** + * Called when the service is connected. + * + * @param client a CustomTabsClient + */ + void onServiceConnected(CustomTabsClient client); + + /** + * Called when the service is disconnected. + */ + void onServiceDisconnected(); +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/json/JSONUtils.java b/app/src/main/java/de/robv/android/xposed/installer/util/json/JSONUtils.java new file mode 100644 index 000000000..c4c03fa5f --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/json/JSONUtils.java @@ -0,0 +1,103 @@ +package de.robv.android.xposed.installer.util.json; + +import android.os.Build; + +import com.google.gson.Gson; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class JSONUtils { + + public static final String JSON_LINK = "https://raw.githubusercontent.com/DVDAndroid/XposedInstaller/material/app/xposed_list_v2.json"; + + public static String getFileContent(String url) throws IOException { + HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection(); + c.setRequestMethod("GET"); + c.setInstanceFollowRedirects(false); + c.setDoOutput(false); + c.connect(); + + BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + br.close(); + + return sb.toString(); + } + + private static String getLatestVersion() throws IOException { + String site = getFileContent("http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/arm/"); + + Pattern pattern = Pattern.compile("(href=\")([^\\?\"]*)\\.zip"); + Matcher matcher = pattern.matcher(site); + String last = ""; + while (matcher.find()) { + if (matcher.group().contains("test")) continue; + last = matcher.group(); + } + last = last.replace("href=\"", ""); + String[] file = last.split("-"); + + return file[1].replace("v", ""); + } + + public static String listZip() { + String latest; + try { + latest = getLatestVersion(); + } catch (IOException e) { + // Got 404 response; no official Xposed zips available + return ""; + } + + String newJson = ",\"" + Build.VERSION.SDK_INT + "\": ["; + String[] arch = new String[]{ + "arm", + "arm64", + "x86" + }; + + for (String a : arch) { + newJson += installerToString(latest, a) + ","; + } + + newJson = newJson.substring(0, newJson.length() - 1); + newJson += "]"; + + return newJson; + } + + private static String installerToString(String latest, String architecture) { + String filename = "xposed-v" + latest + "-sdk" + Build.VERSION.SDK_INT + "-" + architecture; + + XposedZip installer = new XposedZip(); + installer.name = filename; + installer.version = latest; + installer.architecture = architecture; + installer.link = "http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/" + architecture + "/" + filename + ".zip"; + + return new Gson().toJson(installer); + } + + public class XposedJson { + public List tabs; + public ApkRelease apk; + } + + public class ApkRelease { + public String version; + public String changelog; + public String link; + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedTab.java b/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedTab.java new file mode 100644 index 000000000..eec385074 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedTab.java @@ -0,0 +1,76 @@ +package de.robv.android.xposed.installer.util.json; + + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class XposedTab implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public XposedTab createFromParcel(Parcel in) { + return new XposedTab(in); + } + + @Override + public XposedTab[] newArray(int size) { + return new XposedTab[size]; + } + }; + + public String name = ""; + public List sdks = new ArrayList<>(); + public String author = ""; + public boolean stable = true; + + public HashMap compatibility = new HashMap<>(); + public HashMap incompatibility = new HashMap<>(); + public HashMap support = new HashMap<>(); + public HashMap> installers = new HashMap<>(); + public List uninstallers = new ArrayList<>(); + + public XposedTab() { } + + protected XposedTab(Parcel in) { + name = in.readString(); + author = in.readString(); + stable = in.readByte() != 0; + } + + public String getCompatibility() { + if (compatibility == null) return ""; + return compatibility.get(Integer.toString(Build.VERSION.SDK_INT)); + } + + public String getIncompatibility() { + if (incompatibility == null) return ""; + return incompatibility.get(Integer.toString(Build.VERSION.SDK_INT)); + } + + public String getSupport() { + if (support == null) return ""; + return support.get(Integer.toString(Build.VERSION.SDK_INT)); + } + + public List getInstallers() { + if (support == null) return new ArrayList<>(); + return installers.get(Integer.toString(Build.VERSION.SDK_INT)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(author); + dest.writeByte((byte) (stable ? 1 : 0)); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedZip.java b/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedZip.java new file mode 100644 index 000000000..806510096 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/json/XposedZip.java @@ -0,0 +1,65 @@ +package de.robv.android.xposed.installer.util.json; + +import android.app.Activity; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +public class XposedZip { + + public String name; + public String link; + public String version; + public String architecture; + + public static class MyAdapter extends ArrayAdapter { + + private final Context context; + List list; + + public MyAdapter(Context context, List objects) { + super(context, android.R.layout.simple_dropdown_item_1line, objects); + this.context = context; + this.list = objects; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getMyView(parent, position); + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getMyView(parent, position); + } + + private View getMyView(ViewGroup parent, int position) { + View row; + ItemHolder holder = new ItemHolder(); + + LayoutInflater inflater = ((Activity) context).getLayoutInflater(); + row = inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false); + + holder.name = (TextView) row.findViewById(android.R.id.text1); + + row.setTag(holder); + + holder.name.setText(list.get(position).name); + return row; + } + + private class ItemHolder { + TextView name; + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java b/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java index e8cdb472f..51263ca27 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/DownloadView.java @@ -1,7 +1,11 @@ package de.robv.android.xposed.installer.widget; +import android.Manifest; import android.app.DownloadManager; import android.content.Context; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -9,197 +13,242 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; + import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.util.DownloadsUtil; import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadFinishedCallback; import de.robv.android.xposed.installer.util.DownloadsUtil.DownloadInfo; +import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; + public class DownloadView extends LinearLayout { - private DownloadInfo mInfo = null; - private String mUrl = null; - private String mTitle = null; - private DownloadFinishedCallback mCallback = null; - - private final Button btnDownload; - private final Button btnDownloadCancel; - private final Button btnInstall; - private final ProgressBar progressBar; - private final TextView txtInfo; - - public DownloadView(Context context, AttributeSet attrs) { - super(context, attrs); - setFocusable(false); - setOrientation(LinearLayout.VERTICAL); - - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.download_view, this, true); - - btnDownload = (Button) findViewById(R.id.btnDownload); - btnDownloadCancel = (Button) findViewById(R.id.btnDownloadCancel); - btnInstall = (Button) findViewById(R.id.btnInstall); - - btnDownload.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mInfo = DownloadsUtil.add(getContext(), mTitle, mUrl, mCallback); - refreshViewFromUiThread(); - - if (mInfo != null) - new DownloadMonitor().start(); - } - }); - - btnDownloadCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mInfo == null) - return; - - DownloadsUtil.removeById(getContext(), mInfo.id); - // UI update will happen automatically by the DownloadMonitor - } - }); - - btnInstall.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mCallback == null) - return; - - mCallback.onDownloadFinished(getContext(), mInfo); - } - }); - - progressBar = (ProgressBar) findViewById(R.id.progress); - txtInfo = (TextView) findViewById(R.id.txtInfo); - - refreshViewFromUiThread(); - } - - private void refreshViewFromUiThread() { - refreshViewRunnable.run(); - } - - private void refreshView() { - post(refreshViewRunnable); - } - - private final Runnable refreshViewRunnable = new Runnable() { - @Override - public void run() { - if (mUrl == null) { - btnDownload.setVisibility(View.GONE); - btnDownloadCancel.setVisibility(View.GONE); - btnInstall.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtInfo.setVisibility(View.VISIBLE); - txtInfo.setText(R.string.download_view_no_url); - return; - } else if (mInfo == null) { - btnDownload.setVisibility(View.VISIBLE); - btnDownloadCancel.setVisibility(View.GONE); - btnInstall.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtInfo.setVisibility(View.GONE); - } else { - switch (mInfo.status) { - case DownloadManager.STATUS_PENDING: - case DownloadManager.STATUS_PAUSED: - case DownloadManager.STATUS_RUNNING: - btnDownload.setVisibility(View.GONE); - btnDownloadCancel.setVisibility(View.VISIBLE); - btnInstall.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - txtInfo.setVisibility(View.VISIBLE); - if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) { - progressBar.setIndeterminate(true); - txtInfo.setText(R.string.download_view_waiting); - } else { - progressBar.setIndeterminate(false); - progressBar.setMax(mInfo.totalSize); - progressBar.setProgress(mInfo.bytesDownloaded); - txtInfo.setText(getContext().getString(R.string.download_view_running, - mInfo.bytesDownloaded / 1024, mInfo.totalSize / 1024)); - } - break; - - case DownloadManager.STATUS_FAILED: - btnDownload.setVisibility(View.VISIBLE); - btnDownloadCancel.setVisibility(View.GONE); - btnInstall.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtInfo.setVisibility(View.VISIBLE); - txtInfo.setText(getContext().getString(R.string.download_view_failed, mInfo.reason)); - break; - - case DownloadManager.STATUS_SUCCESSFUL: - btnDownload.setVisibility(View.GONE); - btnDownloadCancel.setVisibility(View.GONE); - btnInstall.setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); - txtInfo.setVisibility(View.VISIBLE); - txtInfo.setText(R.string.download_view_successful); - break; - } - } - } - }; - - public void setUrl(String url) { - mUrl = url; - - if (mUrl != null) - mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl); - else - mInfo = null; - - refreshView(); - } - - public String getUrl() { - return mUrl; - } - - public void setTitle(String title) { - this.mTitle = title; - } - - public String getTitle() { - return mTitle; - } - - public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) { - this.mCallback = downloadFinishedCallback; - } - - public DownloadFinishedCallback getDownloadFinishedCallback() { - return mCallback; - } - - private class DownloadMonitor extends Thread { - public DownloadMonitor() { - super("DownloadMonitor"); - } - - @Override - public void run() { - while (true) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - return; - } - - mInfo = DownloadsUtil.getById(getContext(), mInfo.id); - refreshView(); - if (mInfo == null) - return; - - if (mInfo.status != DownloadManager.STATUS_PENDING - && mInfo.status != DownloadManager.STATUS_PAUSED - && mInfo.status != DownloadManager.STATUS_RUNNING) - return; - } - } - } -} + public static Button mClickedButton; + private final Button btnDownload; + private final Button btnDownloadCancel; + private final Button btnInstall; + private final Button btnSave; + private final ProgressBar progressBar; + private final TextView txtInfo; + public Fragment fragment; + private DownloadInfo mInfo = null; + private String mUrl = null; + private final Runnable refreshViewRunnable = new Runnable() { + @Override + public void run() { + if (mUrl == null) { + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_no_url); + } else if (mInfo == null) { + btnDownload.setVisibility(View.VISIBLE); + btnSave.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.GONE); + } else { + switch (mInfo.status) { + case DownloadManager.STATUS_PENDING: + case DownloadManager.STATUS_PAUSED: + case DownloadManager.STATUS_RUNNING: + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.VISIBLE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtInfo.setVisibility(View.VISIBLE); + if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) { + progressBar.setIndeterminate(true); + txtInfo.setText(R.string.download_view_waiting); + } else { + progressBar.setIndeterminate(false); + progressBar.setMax(mInfo.totalSize); + progressBar.setProgress(mInfo.bytesDownloaded); + txtInfo.setText(getContext().getString( + R.string.download_view_running, + mInfo.bytesDownloaded / 1024, + mInfo.totalSize / 1024)); + } + break; + + case DownloadManager.STATUS_FAILED: + btnDownload.setVisibility(View.VISIBLE); + btnSave.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(getContext().getString( + R.string.download_view_failed, mInfo.reason)); + break; + + case DownloadManager.STATUS_SUCCESSFUL: + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnInstall.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_successful); + break; + } + } + } + }; + private String mTitle = null; + private DownloadFinishedCallback mCallback = null; + + public DownloadView(Context context, final AttributeSet attrs) { + super(context, attrs); + setFocusable(false); + setOrientation(LinearLayout.VERTICAL); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.download_view, this, true); + + btnDownload = findViewById(R.id.btnDownload); + btnDownloadCancel = findViewById(R.id.btnDownloadCancel); + btnInstall = findViewById(R.id.btnInstall); + btnSave = findViewById(R.id.save); + + btnDownload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mClickedButton = btnDownload; + + mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, false, mCallback); + refreshViewFromUiThread(); + + if (mInfo != null) + new DownloadMonitor().start(); + } + }); + + btnSave.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mClickedButton = btnSave; + + if (checkPermissions()) + return; + + mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, true, new DownloadFinishedCallback() { + @Override + public void onDownloadFinished(Context context, DownloadInfo info) { + Toast.makeText(context, context.getString(R.string.module_saved, info.localFilename), Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + btnDownloadCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mInfo == null) + return; + + DownloadsUtil.removeById(getContext(), mInfo.id); + // UI update will happen automatically by the DownloadMonitor + } + }); + + btnInstall.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback == null) + return; + + mCallback.onDownloadFinished(getContext(), mInfo); + } + }); + + progressBar = findViewById(R.id.progress); + txtInfo = findViewById(R.id.txtInfo); + + refreshViewFromUiThread(); + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(this.getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + fragment.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + private void refreshViewFromUiThread() { + refreshViewRunnable.run(); + } + + private void refreshView() { + post(refreshViewRunnable); + } + + public String getUrl() { + return mUrl; + } + + public void setUrl(String url) { + mUrl = url; + + if (mUrl != null) + mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl); + else + mInfo = null; + + refreshView(); + } + + public String getTitle() { + return mTitle; + } + + public void setTitle(String title) { + this.mTitle = title; + } + + public DownloadFinishedCallback getDownloadFinishedCallback() { + return mCallback; + } + + public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) { + this.mCallback = downloadFinishedCallback; + } + + private class DownloadMonitor extends Thread { + public DownloadMonitor() { + super("DownloadMonitor"); + } + + @Override + public void run() { + while (true) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + return; + } + + try { + mInfo = DownloadsUtil.getById(getContext(), mInfo.id); + } catch (NullPointerException ignored) { + } + + refreshView(); + if (mInfo == null) + return; + + if (mInfo.status != DownloadManager.STATUS_PENDING + && mInfo.status != DownloadManager.STATUS_PAUSED + && mInfo.status != DownloadManager.STATUS_RUNNING) + return; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java b/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java new file mode 100644 index 000000000..c7bf72ad3 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/IconListPreference.java @@ -0,0 +1,118 @@ +package de.robv.android.xposed.installer.widget; + +/* +* Copyright (C) 2013 The Android Open Source Project +* +* 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. +*/ + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; +import android.widget.ImageView; +import android.widget.ListAdapter; + +import java.util.ArrayList; +import java.util.List; + +import de.robv.android.xposed.installer.R; + +public class IconListPreference extends ListPreference { + + private List mEntryDrawables = new ArrayList<>(); + + public IconListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0); + + CharSequence[] drawables; + + try { + drawables = a.getTextArray(R.styleable.IconListPreference_icons); + } finally { + a.recycle(); + } + + for (CharSequence drawable : drawables) { + int resId = context.getResources().getIdentifier(drawable.toString(), "mipmap", context.getPackageName()); + + Drawable d = context.getResources().getDrawable(resId); + + mEntryDrawables.add(d); + } + + setWidgetLayoutResource(R.layout.color_icon_preview); + } + + protected ListAdapter createListAdapter() { + final String selectedValue = getValue(); + int selectedIndex = findIndexOfValue(selectedValue); + return new AppArrayAdapter(getContext(), R.layout.icon_preference_item, getEntries(), mEntryDrawables, selectedIndex); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + String selectedValue = getValue(); + int selectedIndex = findIndexOfValue(selectedValue); + + Drawable drawable = mEntryDrawables.get(selectedIndex); + + ((ImageView) view.findViewById(R.id.preview)).setImageDrawable(drawable); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + builder.setAdapter(createListAdapter(), this); + super.onPrepareDialogBuilder(builder); + } + + public class AppArrayAdapter extends ArrayAdapter { + private List mImageDrawables = null; + private int mSelectedIndex = 0; + + public AppArrayAdapter(Context context, int textViewResourceId, + CharSequence[] objects, List imageDrawables, + int selectedIndex) { + super(context, textViewResourceId, objects); + mSelectedIndex = selectedIndex; + mImageDrawables = imageDrawables; + } + + @Override + @SuppressLint("ViewHolder") + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater(); + View view = inflater.inflate(R.layout.icon_preference_item, parent, false); + CheckedTextView textView = view.findViewById(R.id.label); + textView.setText(getItem(position)); + textView.setChecked(position == mSelectedIndex); + + ImageView imageView = view.findViewById(R.id.icon); + imageView.setImageDrawable(mImageDrawables.get(position)); + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java b/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java index d31b19673..1b61e7d69 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/IntegerListPreference.java @@ -2,62 +2,61 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.ListPreference; import android.util.AttributeSet; -public class IntegerListPreference extends ListPreference { - public IntegerListPreference(Context context) { - super(context); - } - - public IntegerListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setValue(String value) { - super.setValue(value); - notifyChanged(); - } - - @Override - protected boolean persistString(String value) { - if (value == null) - return false; - - return persistInt(getIntValue(value)); - } - - @Override - protected String getPersistedString(String defaultReturnValue) { - SharedPreferences pref = getPreferenceManager().getSharedPreferences(); - String key = getKey(); - if (!shouldPersist() || !pref.contains(key)) - return defaultReturnValue; - - return String.valueOf(pref.getInt(key, 0)); - } - - @Override - public int findIndexOfValue(String value) { - CharSequence[] entryValues = getEntryValues(); - int intValue = getIntValue(value); - if (value != null && entryValues != null) { - for (int i = entryValues.length - 1; i >= 0; i--) { - if (getIntValue(entryValues[i].toString()) == intValue) { - return i; - } - } - } - return -1; - } - - public static int getIntValue(String value) { - if (value == null) - return 0; - - return (int)((value.startsWith("0x")) - ? Long.parseLong(value.substring(2), 16) - : Long.parseLong(value)); - } -} +import com.afollestad.materialdialogs.prefs.MaterialListPreference; + +public class IntegerListPreference extends MaterialListPreference { + public IntegerListPreference(Context context) { + super(context); + } + + public IntegerListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public static int getIntValue(String value) { + if (value == null) + return 0; + + return (int) ((value.startsWith("0x")) + ? Long.parseLong(value.substring(2), 16) + : Long.parseLong(value)); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } + + @Override + protected boolean persistString(String value) { + return value != null && persistInt(getIntValue(value)); + + } + + @Override + protected String getPersistedString(String defaultReturnValue) { + SharedPreferences pref = getPreferenceManager().getSharedPreferences(); + String key = getKey(); + if (!shouldPersist() || !pref.contains(key)) + return defaultReturnValue; + + return String.valueOf(pref.getInt(key, 0)); + } + + @Override + public int findIndexOfValue(String value) { + CharSequence[] entryValues = getEntryValues(); + int intValue = getIntValue(value); + if (value != null && entryValues != null) { + for (int i = entryValues.length - 1; i >= 0; i--) { + if (getIntValue(entryValues[i].toString()) == intValue) { + return i; + } + } + } + return -1; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java b/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java index 3a0981572..c7b154e2d 100644 --- a/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java +++ b/app/src/main/java/de/robv/android/xposed/installer/widget/ListPreferenceSummaryFix.java @@ -1,21 +1,22 @@ package de.robv.android.xposed.installer.widget; import android.content.Context; -import android.preference.ListPreference; import android.util.AttributeSet; -public class ListPreferenceSummaryFix extends ListPreference { - public ListPreferenceSummaryFix(Context context) { - super(context); - } +import com.afollestad.materialdialogs.prefs.MaterialListPreference; - public ListPreferenceSummaryFix(Context context, AttributeSet attrs) { - super(context, attrs); - } +public class ListPreferenceSummaryFix extends MaterialListPreference { + public ListPreferenceSummaryFix(Context context) { + super(context); + } - @Override - public void setValue(String value) { - super.setValue(value); - notifyChanged(); - } -} + public ListPreferenceSummaryFix(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 000000000..f1d8eed54 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 000000000..a5f9254e1 --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml deleted file mode 100644 index 6b75c9f1f..000000000 --- a/app/src/main/res/anim/slide_in_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml deleted file mode 100644 index fca3c630a..000000000 --- a/app/src/main/res/anim/slide_in_right.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml deleted file mode 100644 index cb58b58b3..000000000 --- a/app/src/main/res/anim/slide_out_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml deleted file mode 100644 index f39f52e9b..000000000 --- a/app/src/main/res/anim/slide_out_right.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_action_share.xml b/app/src/main/res/drawable-anydpi/ic_action_share.xml new file mode 100644 index 000000000..d404e01bf --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_action_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_action_thumb_down.xml b/app/src/main/res/drawable-anydpi/ic_action_thumb_down.xml new file mode 100644 index 000000000..0c959dd08 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_action_thumb_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_action_thumb_up.xml b/app/src/main/res/drawable-anydpi/ic_action_thumb_up.xml new file mode 100644 index 000000000..5a3a45701 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_action_thumb_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_android.xml b/app/src/main/res/drawable-anydpi/ic_android.xml new file mode 100644 index 000000000..475ece58a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_android.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_bookmark.xml b/app/src/main/res/drawable-anydpi/ic_bookmark.xml new file mode 100644 index 000000000..0a84b01a0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml b/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml new file mode 100644 index 000000000..38d373467 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_bookmark_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_check_circle.xml b/app/src/main/res/drawable-anydpi/ic_check_circle.xml new file mode 100644 index 000000000..6ab5a1757 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_chip.xml b/app/src/main/res/drawable-anydpi/ic_chip.xml new file mode 100644 index 000000000..fa986516d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_chip.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_close.xml b/app/src/main/res/drawable-anydpi/ic_close.xml new file mode 100644 index 000000000..ff5c6f1f8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_delete.xml b/app/src/main/res/drawable-anydpi/ic_delete.xml new file mode 100644 index 000000000..684d9e14e --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_description.xml b/app/src/main/res/drawable-anydpi/ic_description.xml new file mode 100644 index 000000000..47fe87009 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_description.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_donate.xml b/app/src/main/res/drawable-anydpi/ic_donate.xml new file mode 100644 index 000000000..2d64a828d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_donate.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_error.xml b/app/src/main/res/drawable-anydpi/ic_error.xml new file mode 100644 index 000000000..b45b40fa5 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_error.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_github.xml b/app/src/main/res/drawable-anydpi/ic_github.xml new file mode 100644 index 000000000..83be6c04e --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_github.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_help.xml b/app/src/main/res/drawable-anydpi/ic_help.xml new file mode 100644 index 000000000..3902588ff --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_help.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_history.xml b/app/src/main/res/drawable-anydpi/ic_history.xml new file mode 100644 index 000000000..6b9f91a37 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_info.xml b/app/src/main/res/drawable-anydpi/ic_info.xml new file mode 100644 index 000000000..2d66b0bf6 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_language.xml b/app/src/main/res/drawable-anydpi/ic_language.xml new file mode 100644 index 000000000..85b37743a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_language.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_manufacturer.xml b/app/src/main/res/drawable-anydpi/ic_manufacturer.xml new file mode 100644 index 000000000..aeb75271f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_manufacturer.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml b/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml new file mode 100644 index 000000000..0ca6efe7b --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu_search.xml b/app/src/main/res/drawable-anydpi/ic_menu_search.xml new file mode 100644 index 000000000..3063609d8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu_sort.xml b/app/src/main/res/drawable-anydpi/ic_menu_sort.xml new file mode 100644 index 000000000..ae9823151 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu_sort.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_about.xml b/app/src/main/res/drawable-anydpi/ic_nav_about.xml new file mode 100644 index 000000000..57ab7a702 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_about.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml b/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml new file mode 100644 index 000000000..4518e0e33 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_downloads.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_install.xml b/app/src/main/res/drawable-anydpi/ic_nav_install.xml new file mode 100644 index 000000000..bf0085b6f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_install.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_logs.xml b/app/src/main/res/drawable-anydpi/ic_nav_logs.xml new file mode 100644 index 000000000..fc4c9f32b --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_logs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_modules.xml b/app/src/main/res/drawable-anydpi/ic_nav_modules.xml new file mode 100644 index 000000000..80d25de23 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_modules.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_settings.xml b/app/src/main/res/drawable-anydpi/ic_nav_settings.xml new file mode 100644 index 000000000..5372b524f --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_nav_support.xml b/app/src/main/res/drawable-anydpi/ic_nav_support.xml new file mode 100644 index 000000000..3ba8726d1 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_nav_support.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_no_connection.xml b/app/src/main/res/drawable-anydpi/ic_no_connection.xml new file mode 100644 index 000000000..0c234a818 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_no_connection.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_no_image.xml b/app/src/main/res/drawable-anydpi/ic_no_image.xml new file mode 100644 index 000000000..53e0a6de2 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_no_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_notification.xml b/app/src/main/res/drawable-anydpi/ic_notification.xml new file mode 100644 index 000000000..9e0699c21 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_person.xml b/app/src/main/res/drawable-anydpi/ic_person.xml new file mode 100644 index 000000000..234d73d8d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_save.xml b/app/src/main/res/drawable-anydpi/ic_save.xml new file mode 100644 index 000000000..a71d98393 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_scroll_down.xml b/app/src/main/res/drawable-anydpi/ic_scroll_down.xml new file mode 100644 index 000000000..29f6ca4bc --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_scroll_down.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_scroll_top.xml b/app/src/main/res/drawable-anydpi/ic_scroll_top.xml new file mode 100644 index 000000000..08f75dc51 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_scroll_top.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_send.xml b/app/src/main/res/drawable-anydpi/ic_send.xml new file mode 100644 index 000000000..890d89494 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_update.xml b/app/src/main/res/drawable-anydpi/ic_update.xml new file mode 100644 index 000000000..2ff7fa26c --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_warning.xml b/app/src/main/res/drawable-anydpi/ic_warning.xml new file mode 100644 index 000000000..9d06ad046 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_warning_grey.xml b/app/src/main/res/drawable-anydpi/ic_warning_grey.xml new file mode 100644 index 000000000..70c96d861 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_warning_grey.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_xda.xml b/app/src/main/res/drawable-anydpi/ic_xda.xml new file mode 100644 index 000000000..918a5497b --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_xda.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/shortcut_ic_downloads.xml b/app/src/main/res/drawable-anydpi/shortcut_ic_downloads.xml new file mode 100644 index 000000000..b89097fbd --- /dev/null +++ b/app/src/main/res/drawable-anydpi/shortcut_ic_downloads.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable-anydpi/shortcut_ic_modules.xml b/app/src/main/res/drawable-anydpi/shortcut_ic_modules.xml new file mode 100644 index 000000000..38a7a5cdd --- /dev/null +++ b/app/src/main/res/drawable-anydpi/shortcut_ic_modules.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_menu_refresh.png b/app/src/main/res/drawable-hdpi/ic_menu_refresh.png deleted file mode 100644 index ffa7be933..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_search.png b/app/src/main/res/drawable-hdpi/ic_menu_search.png deleted file mode 100644 index bbfbc96cb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_sort.png b/app/src/main/res/drawable-hdpi/ic_menu_sort.png deleted file mode 100644 index 55a429b6f..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_sort.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_about.png b/app/src/main/res/drawable-hdpi/ic_nav_about.png deleted file mode 100644 index bc904fa1b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_downloads.png b/app/src/main/res/drawable-hdpi/ic_nav_downloads.png deleted file mode 100644 index d79e8040b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_downloads.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_install.png b/app/src/main/res/drawable-hdpi/ic_nav_install.png deleted file mode 100644 index 9db798a1e..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_install.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_logs.png b/app/src/main/res/drawable-hdpi/ic_nav_logs.png deleted file mode 100644 index 34dc85b17..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_logs.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_moduals.png b/app/src/main/res/drawable-hdpi/ic_nav_moduals.png deleted file mode 100644 index a402ef58e..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_moduals.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_settings.png b/app/src/main/res/drawable-hdpi/ic_nav_settings.png deleted file mode 100644 index 91da65afd..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_nav_support.png b/app/src/main/res/drawable-hdpi/ic_nav_support.png deleted file mode 100644 index 2e0d40798..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_nav_support.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png deleted file mode 100644 index 5f9217fe7..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/ic_launcher.png b/app/src/main/res/drawable-ldpi/ic_launcher.png deleted file mode 100644 index 90b4bc27b..000000000 Binary files a/app/src/main/res/drawable-ldpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/ic_notification.png b/app/src/main/res/drawable-ldpi/ic_notification.png deleted file mode 100644 index 49cddecbc..000000000 Binary files a/app/src/main/res/drawable-ldpi/ic_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png deleted file mode 100644 index f937d2151..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png b/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png deleted file mode 100644 index 1989184b1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_search.png b/app/src/main/res/drawable-xhdpi/ic_menu_search.png deleted file mode 100644 index bfc3e3939..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_menu_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_sort.png b/app/src/main/res/drawable-xhdpi/ic_menu_sort.png deleted file mode 100644 index 6d4af1bcb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_menu_sort.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_about.png b/app/src/main/res/drawable-xhdpi/ic_nav_about.png deleted file mode 100644 index d145f80c2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_downloads.png b/app/src/main/res/drawable-xhdpi/ic_nav_downloads.png deleted file mode 100644 index b2772eeed..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_downloads.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_install.png b/app/src/main/res/drawable-xhdpi/ic_nav_install.png deleted file mode 100644 index d7c62cde8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_install.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_logs.png b/app/src/main/res/drawable-xhdpi/ic_nav_logs.png deleted file mode 100644 index 1f74cdbbe..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_logs.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_moduals.png b/app/src/main/res/drawable-xhdpi/ic_nav_moduals.png deleted file mode 100644 index f237b570e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_moduals.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_settings.png b/app/src/main/res/drawable-xhdpi/ic_nav_settings.png deleted file mode 100644 index a19fc5c31..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_nav_support.png b/app/src/main/res/drawable-xhdpi/ic_nav_support.png deleted file mode 100644 index da89e131c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_nav_support.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png deleted file mode 100644 index 69e265a0c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png deleted file mode 100644 index 1692d8a24..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_search.png b/app/src/main/res/drawable-xxhdpi/ic_menu_search.png deleted file mode 100644 index abbb98951..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_sort.png b/app/src/main/res/drawable-xxhdpi/ic_menu_sort.png deleted file mode 100644 index b8ef1050e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_sort.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_about.png b/app/src/main/res/drawable-xxhdpi/ic_nav_about.png deleted file mode 100644 index 0fd6831b2..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_downloads.png b/app/src/main/res/drawable-xxhdpi/ic_nav_downloads.png deleted file mode 100644 index e1db47032..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_downloads.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_install.png b/app/src/main/res/drawable-xxhdpi/ic_nav_install.png deleted file mode 100644 index ad6ce37cd..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_install.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_logs.png b/app/src/main/res/drawable-xxhdpi/ic_nav_logs.png deleted file mode 100644 index ad5c250e6..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_logs.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_moduals.png b/app/src/main/res/drawable-xxhdpi/ic_nav_moduals.png deleted file mode 100644 index c80fb8880..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_moduals.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_settings.png b/app/src/main/res/drawable-xxhdpi/ic_nav_settings.png deleted file mode 100644 index bbf8fb94b..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nav_support.png b/app/src/main/res/drawable-xxhdpi/ic_nav_support.png deleted file mode 100644 index 92481e2dd..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_nav_support.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png deleted file mode 100644 index 58c7e5a10..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable/background_card_black.xml b/app/src/main/res/drawable/background_card_black.xml index 5f1648458..63cc99e37 100644 --- a/app/src/main/res/drawable/background_card_black.xml +++ b/app/src/main/res/drawable/background_card_black.xml @@ -1,7 +1,7 @@ - - + + diff --git a/app/src/main/res/drawable/background_card_dark.xml b/app/src/main/res/drawable/background_card_dark.xml index 87ebd7666..4c88a51d1 100644 --- a/app/src/main/res/drawable/background_card_dark.xml +++ b/app/src/main/res/drawable/background_card_dark.xml @@ -1,7 +1,7 @@ - - + + diff --git a/app/src/main/res/drawable/background_card_light.xml b/app/src/main/res/drawable/background_card_light.xml index 18a239158..026baa95f 100644 --- a/app/src/main/res/drawable/background_card_light.xml +++ b/app/src/main/res/drawable/background_card_light.xml @@ -1,7 +1,7 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_card_normal_black.xml b/app/src/main/res/drawable/background_card_normal_black.xml index 36fc1a0bb..0f59ec586 100644 --- a/app/src/main/res/drawable/background_card_normal_black.xml +++ b/app/src/main/res/drawable/background_card_normal_black.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/app/src/main/res/drawable/background_card_normal_dark.xml b/app/src/main/res/drawable/background_card_normal_dark.xml index e4752ebda..8817501fa 100644 --- a/app/src/main/res/drawable/background_card_normal_dark.xml +++ b/app/src/main/res/drawable/background_card_normal_dark.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/app/src/main/res/drawable/background_card_normal_light.xml b/app/src/main/res/drawable/background_card_normal_light.xml index 171173b20..4740f5a91 100644 --- a/app/src/main/res/drawable/background_card_normal_light.xml +++ b/app/src/main/res/drawable/background_card_normal_light.xml @@ -3,8 +3,9 @@ - + @@ -14,14 +15,16 @@ - + - diff --git a/app/src/main/res/drawable/background_card_pressed_black.xml b/app/src/main/res/drawable/background_card_pressed_black.xml index 5ad08b4c7..a9dfaf498 100644 --- a/app/src/main/res/drawable/background_card_pressed_black.xml +++ b/app/src/main/res/drawable/background_card_pressed_black.xml @@ -3,28 +3,31 @@ - + - + - + - + - + - + diff --git a/app/src/main/res/drawable/background_card_pressed_dark.xml b/app/src/main/res/drawable/background_card_pressed_dark.xml index f930a6bb8..37d662742 100644 --- a/app/src/main/res/drawable/background_card_pressed_dark.xml +++ b/app/src/main/res/drawable/background_card_pressed_dark.xml @@ -3,28 +3,31 @@ - + - + - + - + - + - + diff --git a/app/src/main/res/drawable/background_card_pressed_light.xml b/app/src/main/res/drawable/background_card_pressed_light.xml index 41b90227a..471984f04 100644 --- a/app/src/main/res/drawable/background_card_pressed_light.xml +++ b/app/src/main/res/drawable/background_card_pressed_light.xml @@ -3,28 +3,31 @@ - + - + - + - + - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_flash.xml b/app/src/main/res/drawable/ic_flash.xml new file mode 100644 index 000000000..a3c81cc38 --- /dev/null +++ b/app/src/main/res/drawable/ic_flash.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml new file mode 100644 index 000000000..d136718a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/toolbar_shadow.xml b/app/src/main/res/drawable/toolbar_shadow.xml new file mode 100644 index 000000000..7a7a56833 --- /dev/null +++ b/app/src/main/res/drawable/toolbar_shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v23/list_item_download.xml b/app/src/main/res/layout-v23/list_item_download.xml new file mode 100644 index 000000000..48e3d2a6a --- /dev/null +++ b/app/src/main/res/layout-v23/list_item_download.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_container.xml b/app/src/main/res/layout/activity_container.xml index ddf6adcb2..593b14b38 100644 --- a/app/src/main/res/layout/activity_container.xml +++ b/app/src/main/res/layout/activity_container.xml @@ -1,23 +1,16 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:windowBackground" + android:orientation="vertical"> - + + android:layout_weight="1"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download_details.xml b/app/src/main/res/layout/activity_download_details.xml index 46fbe6ae0..386c97312 100644 --- a/app/src/main/res/layout/activity_download_details.xml +++ b/app/src/main/res/layout/activity_download_details.xml @@ -1,52 +1,46 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:layout_height="wrap_content"> + app:theme="@style/Theme.XposedInstaller.Toolbar"/> + app:tabMode="scrollable" + app:tabTextAppearance="@style/TextAppearance.Design.Tab"/> + + + app:layout_behavior="@string/appbar_scrolling_view_behavior"/> - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download_details_not_found.xml b/app/src/main/res/layout/activity_download_details_not_found.xml index fe33e010b..cfb8b4a05 100644 --- a/app/src/main/res/layout/activity_download_details_not_found.xml +++ b/app/src/main/res/layout/activity_download_details_not_found.xml @@ -1,12 +1,16 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:padding="8dp" + tools:context=".DownloadDetailsActivity"> + + + android:textAppearance="?android:attr/textAppearanceMedium"/>