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
+===============
+[](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"/>
+ android:text="@string/menuReload"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_installation.xml b/app/src/main/res/layout/activity_installation.xml
new file mode 100644
index 000000000..41e016833
--- /dev/null
+++ b/app/src/main/res/layout/activity_installation.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml
index cea70521a..2bda9c7d7 100644
--- a/app/src/main/res/layout/activity_welcome.xml
+++ b/app/src/main/res/layout/activity_welcome.xml
@@ -1,34 +1,27 @@
-
+ tools:context=".WelcomeActivity">
+ android:orientation="vertical">
+ android:layout_height="match_parent"/>
-
+
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/card_authors.xml b/app/src/main/res/layout/card_authors.xml
new file mode 100644
index 000000000..5951c6342
--- /dev/null
+++ b/app/src/main/res/layout/card_authors.xml
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/color_icon_preview.xml b/app/src/main/res/layout/color_icon_preview.xml
new file mode 100644
index 000000000..d0247c6ef
--- /dev/null
+++ b/app/src/main/res/layout/color_icon_preview.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_install_warning.xml b/app/src/main/res/layout/dialog_install_warning.xml
index ac6754cfc..d75faceec 100644
--- a/app/src/main/res/layout/dialog_install_warning.xml
+++ b/app/src/main/res/layout/dialog_install_warning.xml
@@ -1,26 +1,26 @@
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="12dp">
+ android:orientation="vertical">
+ android:textSize="16sp"/>
-
+ android:text="@string/dont_show_again"/>
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_details.xml b/app/src/main/res/layout/download_details.xml
index de879ab9f..7cfc10495 100644
--- a/app/src/main/res/layout/download_details.xml
+++ b/app/src/main/res/layout/download_details.xml
@@ -1,23 +1,23 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".DownloadDetailsFragment">
+ android:paddingRight="?android:attr/scrollbarSize">
+ android:textStyle="bold"/>
+ android:textStyle="italic"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ android:orientation="vertical">
diff --git a/app/src/main/res/layout/download_moreinfo.xml b/app/src/main/res/layout/download_moreinfo.xml
index f04c649ad..6fd0a45ff 100644
--- a/app/src/main/res/layout/download_moreinfo.xml
+++ b/app/src/main/res/layout/download_moreinfo.xml
@@ -1,35 +1,35 @@
-
+ card_view:cardUseCompatPadding="true">
+ android:orientation="vertical"
+ android:padding="8dp">
+ android:textStyle="bold"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_view.xml b/app/src/main/res/layout/download_view.xml
index c40b9bef0..af2940c8d 100644
--- a/app/src/main/res/layout/download_view.xml
+++ b/app/src/main/res/layout/download_view.xml
@@ -1,16 +1,17 @@
-
+
+ android:layout_height="wrap_content">
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+
+ tools:ignore="ButtonOrder"/>
+ android:visibility="gone"/>
@@ -45,6 +54,6 @@
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/icon_preference_item.xml b/app/src/main/res/layout/icon_preference_item.xml
new file mode 100644
index 000000000..e029273b2
--- /dev/null
+++ b/app/src/main/res/layout/icon_preference_item.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_fragment.xml b/app/src/main/res/layout/list_fragment.xml
new file mode 100644
index 000000000..4590f2800
--- /dev/null
+++ b/app/src/main/res/layout/list_fragment.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_download.xml b/app/src/main/res/layout/list_item_download.xml
index b517e1fed..13a63821f 100644
--- a/app/src/main/res/layout/list_item_download.xml
+++ b/app/src/main/res/layout/list_item_download.xml
@@ -1,21 +1,21 @@
-
+ android:foreground="?android:attr/selectableItemBackground">
+ android:padding="16dip">
+ android:textSize="16sp"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ android:visibility="gone"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@android:color/tertiary_text_dark"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_module.xml b/app/src/main/res/layout/list_item_module.xml
index 7fc3ab8b2..8c9cd79e0 100644
--- a/app/src/main/res/layout/list_item_module.xml
+++ b/app/src/main/res/layout/list_item_module.xml
@@ -1,44 +1,45 @@
-
+ android:foreground="?android:attr/selectableItemBackground"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ card_view:cardBackgroundColor="?attr/list_download_item_color"
+ card_view:cardCornerRadius="2dp"
+ card_view:cardElevation="2dp">
+ android:orientation="horizontal"
+ android:padding="8dp">
+ android:layout_height="48dip"/>
+ android:layout_marginRight="4dip"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ android:layout_height="wrap_content">
+
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textIsSelectable="false"/>
+
+ android:textColor="@android:color/tertiary_text_dark"
+ android:textIsSelectable="false"/>
+
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textIsSelectable="false"/>
+
+ android:visibility="gone"/>
+ android:padding="3dp"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_version.xml b/app/src/main/res/layout/list_item_version.xml
index 33c972739..095ef9a57 100644
--- a/app/src/main/res/layout/list_item_version.xml
+++ b/app/src/main/res/layout/list_item_version.xml
@@ -1,30 +1,30 @@
-
+ android:foreground="?selectableItemBackground"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ card_view:cardBackgroundColor="?attr/list_download_item_color"
+ card_view:cardCornerRadius="2dip">
+ android:padding="8dip">
+ android:baselineAligned="false">
+ android:orientation="vertical">
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ android:textIsSelectable="false"/>
+ android:orientation="vertical">
+ android:textStyle="italic"/>
+ android:textStyle="italic"/>
+ android:layout_height="wrap_content"/>
+ android:textStyle="bold"/>
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_welcome.xml b/app/src/main/res/layout/list_item_welcome.xml
deleted file mode 100644
index c8399b90d..000000000
--- a/app/src/main/res/layout/list_item_welcome.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_sticky_header_download.xml b/app/src/main/res/layout/list_sticky_header_download.xml
index cadcbfde5..f5e3451e3 100644
--- a/app/src/main/res/layout/list_sticky_header_download.xml
+++ b/app/src/main/res/layout/list_sticky_header_download.xml
@@ -1,18 +1,18 @@
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?sticky_header_background"
+ android:elevation="2dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:paddingTop="8dp">
+ android:textAllCaps="true"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/single_installer_view.xml b/app/src/main/res/layout/single_installer_view.xml
new file mode 100644
index 000000000..29f4941ed
--- /dev/null
+++ b/app/src/main/res/layout/single_installer_view.xml
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/status_installer.xml b/app/src/main/res/layout/status_installer.xml
new file mode 100644
index 000000000..b2f65a0c2
--- /dev/null
+++ b/app/src/main/res/layout/status_installer.xml
@@ -0,0 +1,362 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_about.xml b/app/src/main/res/layout/tab_about.xml
index d5f1e8086..8cb3c94ec 100644
--- a/app/src/main/res/layout/tab_about.xml
+++ b/app/src/main/res/layout/tab_about.xml
@@ -1,85 +1,235 @@
-
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ tools:ignore="UseCompoundDrawables,ContentDescription">
+ android:orientation="vertical">
-
+ android:layout_gravity="center"
+ android:layout_marginBottom="8dp"
+ app:cardBackgroundColor="?attr/list_download_item_color"
+ app:cardUseCompatPadding="true">
-
+
-
+
-
+
-
+
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_advanced_installer.xml b/app/src/main/res/layout/tab_advanced_installer.xml
new file mode 100644
index 000000000..84d7f41b1
--- /dev/null
+++ b/app/src/main/res/layout/tab_advanced_installer.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_downloader.xml b/app/src/main/res/layout/tab_downloader.xml
index be2a2a7ad..b32b8dadf 100644
--- a/app/src/main/res/layout/tab_downloader.xml
+++ b/app/src/main/res/layout/tab_downloader.xml
@@ -1,19 +1,66 @@
-
-
+
+
+ android:layout_height="match_parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_installer.xml b/app/src/main/res/layout/tab_installer.xml
deleted file mode 100644
index f90d9f1f7..000000000
--- a/app/src/main/res/layout/tab_installer.xml
+++ /dev/null
@@ -1,108 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_logs.xml b/app/src/main/res/layout/tab_logs.xml
index 87d6ab629..6700e86c7 100644
--- a/app/src/main/res/layout/tab_logs.xml
+++ b/app/src/main/res/layout/tab_logs.xml
@@ -1,19 +1,50 @@
-
+
-
-
-
+ android:layout_height="wrap_content">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_support.xml b/app/src/main/res/layout/tab_support.xml
index d23a7b51d..741118de3 100644
--- a/app/src/main/res/layout/tab_support.xml
+++ b/app/src/main/res/layout/tab_support.xml
@@ -1,77 +1,191 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="8dp"
+ tools:ignore="UseCompoundDrawables,ContentDescription">
+ android:orientation="vertical">
-
+ android:layout_gravity="center"
+ android:layout_marginBottom="8dp"
+ app:cardBackgroundColor="?attr/list_download_item_color"
+ app:cardUseCompatPadding="true">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/toolbar.xml b/app/src/main/res/layout/toolbar.xml
new file mode 100644
index 000000000..da1ca85d6
--- /dev/null
+++ b/app/src/main/res/layout/toolbar.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/xposed_not_active_note.xml b/app/src/main/res/layout/xposed_not_active_note.xml
index 3d939838f..70b4f3bc9 100644
--- a/app/src/main/res/layout/xposed_not_active_note.xml
+++ b/app/src/main/res/layout/xposed_not_active_note.xml
@@ -1,46 +1,38 @@
-
+ android:layout_marginBottom="8dp"
+ app:cardBackgroundColor="@color/amber_500"
+ app:cardCornerRadius="2dp"
+ app:cardElevation="4dp">
-
+ android:background="?android:attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:minHeight="48dp"
+ android:orientation="horizontal"
+ android:padding="8dp">
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp"
+ android:src="@drawable/ic_warning_grey"/>
-
-
\ No newline at end of file
+ android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
+ android:textColor="?android:textColorSecondary"/>
+
+
+
+
diff --git a/app/src/main/res/menu/context_menu_modules.xml b/app/src/main/res/menu/context_menu_modules.xml
index b49b2c80e..154bf13e0 100644
--- a/app/src/main/res/menu/context_menu_modules.xml
+++ b/app/src/main/res/menu/context_menu_modules.xml
@@ -1,5 +1,5 @@
-