From 198c9fbaff5f30bb18f531ec9ececf96d0cb950f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 10 May 2015 16:21:48 +0200 Subject: [PATCH 01/20] Remove deprecated superuser permission, see https://plus.google.com/+Chainfire/posts/Ka3ujLb4bJu --- F-Droid/AndroidManifest.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index d798d09f7..9ad429def 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -55,9 +55,6 @@ - - - Date: Sun, 10 May 2015 21:09:38 +0200 Subject: [PATCH 02/20] Allow downgrade via root installer --- F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java index 6cec7f80f..53d3627cd 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java @@ -143,7 +143,7 @@ public class RootInstaller extends Installer { return; } - rootSession.addCommand("pm install -r \"" + apkFile.getAbsolutePath() + "\"", 0, + rootSession.addCommand("pm install -dr \"" + apkFile.getAbsolutePath() + "\"", 0, new Shell.OnCommandResultListener() { public void onCommandResult(int commandCode, int exitCode, List output) { // close su shell @@ -162,7 +162,7 @@ public class RootInstaller extends Installer { private void addInstallCommand(List apkFiles) { List commands = new ArrayList<>(); - String pm = "pm install -r "; + String pm = "pm install -dr "; for (File apkFile : apkFiles) { // see addInstallCommand() if (!isValidPackageName(apkFile.getName())) { From f328a9de5e3c2fcfa8a65bdafb948de94be03e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 10 May 2015 23:47:14 +0200 Subject: [PATCH 03/20] Install F-Droid as system-app via root access --- F-Droid/AndroidManifest.xml | 10 + F-Droid/res/values/strings.xml | 16 +- F-Droid/res/xml/preferences.xml | 4 + F-Droid/src/org/fdroid/fdroid/FDroid.java | 7 + F-Droid/src/org/fdroid/fdroid/FDroidApp.java | 25 +- .../src/org/fdroid/fdroid/Preferences.java | 25 ++ .../fdroid/installer/CheckRootAsyncTask.java | 9 +- .../InstallIntoSystemBootReceiver.java | 43 ++ .../InstallIntoSystemDialogActivity.java | 367 ++++++++++++++++++ .../views/fragments/PreferencesFragment.java | 42 +- 10 files changed, 532 insertions(+), 16 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 9ad429def..cd48da400 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -403,6 +403,16 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".FDroid" /> + + + + + + + Request root access to install, update, and remove packages Install using system-permissions Use system permissions to install, update, and remove packages + Uninstall F-Droid + Uninstall F-Droid when installed as a system-app. Broadcast Local Repo Advertise your local repo using Bonjour (mDNS) Name of your Local Repo @@ -287,7 +289,6 @@ No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps. All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date). - Root access Requesting root access… Root access denied Either your Android device is not rooted or you have denied root access for F-Droid. @@ -296,6 +297,19 @@ The (de-)installation failed. If you are using root access, try disabling this setting! System permissions denied This option is only available when F-Droid is installed as a system-app. + Install as system-app + Successful installation as system-app + Installation as system-app failed + F-Droid has been successfully installed into the system partition. This enables extended features, such as automatic app updates. + The installation of F-Droid into the system partition failed. The installation method is not supported by all Android distributions, please consult the F-Droid bug tracker for more information. + installing… + uninstalling… + Do you want to install F-Droid as a system-app?\nThis takes up to 10 seconds where <b>no user interface</b> is shown. + Do you want to install F-Droid as a system-app?\nThis takes up to 10 seconds where <b>no user interface</b> is shown and the device will be <b>rebooted</b> afterwards. + Looks like you have root access on your device. You could install F-Droid as a system-app to enable extended features, such as automatic app updates. + Do you want to uninstall F-Droid? + This will uninstall F-Droid completely. + Uninstall F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device. If your friend has F-Droid and NFC turned on touch your phones together. diff --git a/F-Droid/res/xml/preferences.xml b/F-Droid/res/xml/preferences.xml index 9d94bcbc0..f89fb09bd 100644 --- a/F-Droid/res/xml/preferences.xml +++ b/F-Droid/res/xml/preferences.xml @@ -92,5 +92,9 @@ android:defaultValue="false" android:key="systemInstaller" android:dependency="expert" /> + diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java index a532dbbc2..87581b7f3 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroid.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java @@ -47,6 +47,7 @@ import android.widget.Toast; import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.NewRepoConfig; +import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity; import org.fdroid.fdroid.views.AppListFragmentPagerAdapter; import org.fdroid.fdroid.views.ManageReposActivity; import org.fdroid.fdroid.views.swap.ConnectSwapActivity; @@ -101,6 +102,12 @@ public class FDroid extends ActionBarActivity { Uri uri = AppProvider.getContentUri(); getContentResolver().registerContentObserver(uri, true, new AppObserver()); + + if (Preferences.get().isFirstTime()) { + Intent installIntent = new Intent(this, InstallIntoSystemDialogActivity.class); + installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_FIRST_TIME); + startActivity(installIntent); + } } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java index 6f99b3106..3410e3df5 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java @@ -98,23 +98,26 @@ public class FDroidApp extends Application { } public void applyTheme(Activity activity) { - switch (curTheme) { - case dark: - activity.setTheme(R.style.AppThemeDark); - break; - case light: - activity.setTheme(R.style.AppThemeLight); - break; - case lightWithDarkActionBar: - activity.setTheme(R.style.AppThemeLightWithDarkActionBar); - break; - } + activity.setTheme(getCurThemeResId()); } public static Theme getCurTheme() { return curTheme; } + public static int getCurThemeResId() { + switch (curTheme) { + case dark: + return R.style.AppThemeDark; + case light: + return R.style.AppThemeLight; + case lightWithDarkActionBar: + return R.style.AppThemeLightWithDarkActionBar; + default: + return R.style.AppThemeDark; + } + } + public static void enableSpongyCastle() { Security.addProvider(spongyCastleProvider); } diff --git a/F-Droid/src/org/fdroid/fdroid/Preferences.java b/F-Droid/src/org/fdroid/fdroid/Preferences.java index 2502d8eac..fae56de5a 100644 --- a/F-Droid/src/org/fdroid/fdroid/Preferences.java +++ b/F-Droid/src/org/fdroid/fdroid/Preferences.java @@ -52,6 +52,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_UPD_LAST = "lastUpdateCheck"; public static final String PREF_ROOT_INSTALLER = "rootInstaller"; public static final String PREF_SYSTEM_INSTALLER = "systemInstaller"; + public static final String PREF_UNINSTALL_SYSTEM_APP = "uninstallSystemApp"; public static final String PREF_LOCAL_REPO_BONJOUR = "localRepoBonjour"; public static final String PREF_LOCAL_REPO_NAME = "localRepoName"; public static final String PREF_LOCAL_REPO_HTTPS = "localRepoHttps"; @@ -60,6 +61,8 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_PROXY_HOST = "proxyHost"; public static final String PREF_PROXY_PORT = "proxyPort"; public static final String PREF_SHOW_NFC_DURING_SWAP = "showNfcDuringSwap"; + public static final String PREF_FIRST_TIME = "firstTime"; + public static final String PREF_POST_SYSTEM_INSTALL = "postSystemInstall"; private static final boolean DEFAULT_COMPACT_LAYOUT = false; private static final boolean DEFAULT_ROOTED = true; @@ -77,6 +80,8 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String DEFAULT_PROXY_HOST = "127.0.0.1"; public static final int DEFAULT_PROXY_PORT = 8118; public static final boolean DEFAULT_SHOW_NFC_DURING_SWAP = true; + private static final boolean DEFAULT_FIRST_TIME = true; + private static final boolean DEFAULT_POST_SYSTEM_INSTALL = false; private boolean compactLayout = DEFAULT_COMPACT_LAYOUT; private boolean filterAppsRequiringRoot = DEFAULT_ROOTED; @@ -110,6 +115,26 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi return preferences.getBoolean(PREF_SYSTEM_INSTALLER, DEFAULT_SYSTEM_INSTALLER); } + public void setSystemInstallerEnabled(boolean enable) { + preferences.edit().putBoolean(PREF_SYSTEM_INSTALLER, enable).commit(); + } + + public boolean isFirstTime() { + return preferences.getBoolean(PREF_FIRST_TIME, DEFAULT_FIRST_TIME); + } + + public void setFirstTime(boolean firstTime) { + preferences.edit().putBoolean(PREF_FIRST_TIME, firstTime).commit(); + } + + public boolean isPostSystemInstall() { + return preferences.getBoolean(PREF_POST_SYSTEM_INSTALL, DEFAULT_POST_SYSTEM_INSTALL); + } + + public void setPostSystemInstall(boolean postInstall) { + preferences.edit().putBoolean(PREF_POST_SYSTEM_INSTALL, postInstall).commit(); + } + public boolean isLocalRepoBonjourEnabled() { return preferences.getBoolean(PREF_LOCAL_REPO_BONJOUR, DEFAULT_LOCAL_REPO_BONJOUR); } diff --git a/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java b/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java index 6fe5e2134..01d8ff6cd 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java @@ -22,7 +22,9 @@ package org.fdroid.fdroid.installer; import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; +import android.view.ContextThemeWrapper; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import eu.chainfire.libsuperuser.Shell; @@ -46,8 +48,11 @@ public class CheckRootAsyncTask extends AsyncTask { protected void onPreExecute() { super.onPreExecute(); - mDialog = new ProgressDialog(mContext); - mDialog.setTitle(R.string.requesting_root_access_title); + // if the dialog is displayed from the application class, design is missing + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(mContext, FDroidApp.getCurThemeResId()); + + mDialog = new ProgressDialog(theme); mDialog.setMessage(mContext.getString(R.string.requesting_root_access_body)); mDialog.setIndeterminate(true); mDialog.setCancelable(false); diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java new file mode 100644 index 000000000..42f7e57cc --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.installer; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.fdroid.fdroid.Preferences; + +public class InstallIntoSystemBootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + if (Preferences.get().isPostSystemInstall()) { + Preferences.get().setPostSystemInstall(false); + + Intent postInstall = new Intent(context.getApplicationContext(), InstallIntoSystemDialogActivity.class); + postInstall.setAction(InstallIntoSystemDialogActivity.ACTION_POST_INSTALL); + postInstall.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(postInstall); + } + } + } +} \ No newline at end of file diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java new file mode 100644 index 000000000..bc0a567c6 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.installer; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.text.Html; +import android.util.Log; +import android.view.ContextThemeWrapper; + +import org.fdroid.fdroid.FDroid; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; + +import eu.chainfire.libsuperuser.Shell; + +/** + * Note: This activity has no view on its own, it displays consecutive dialogs. + *

+ * Partly based on https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java + * http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/ + *

+ * Info for lollipop: + * http://stackoverflow.com/q/26487750 + *

+ * Removed apk observers in + * https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103 + *

+ * History of PackageManagerService: + * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java + */ +public class InstallIntoSystemDialogActivity extends FragmentActivity { + + private static final String TAG = "InstallIntoSystem"; + + public static final String ACTION_INSTALL = "install"; + public static final String ACTION_UNINSTALL = "uninstall"; + public static final String ACTION_POST_INSTALL = "post_install"; + public static final String ACTION_FIRST_TIME = "first_time"; + + private static String SYSTEM_FOLDER; + + static { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + SYSTEM_FOLDER = "/system/app/"; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // new folder introduced in + // https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54 + SYSTEM_FOLDER = "/system/priv-app/"; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // new cluster based installation and app dirs + SYSTEM_FOLDER = "/system/priv-app/FDroid/"; + } + } + + private static final String APK_NAME = "FDroid.apk"; + + private static final String CMD_SCRIPT_KITKAT = "mount -o rw,remount /system\n" + + "cat %s > " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + + "chmod 655 " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + + "pm uninstall -k %s\n" + // -k to retain data + "mv " + SYSTEM_FOLDER + APK_NAME + ".tmp " + SYSTEM_FOLDER + APK_NAME + "\n" + + "pm install -r " + SYSTEM_FOLDER + APK_NAME + "\n" + + "sleep 5\n" + + "mount -o ro,remount /system\n" + + "am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true"; + + // TODO: Currently only works with reboot. Find a way how this could work without. + // See http://stackoverflow.com/q/26487750 + private static final String CMD_SCRIPT_LOLLIPOP_REBOOT = "am broadcast -a android.intent.action.ACTION_SHUTDOWN\n" + + "sleep 1\n" + + "reboot"; + + private static final String CMD_SCRIPT_LOLLIPOP = "mount -o rw,remount /system\n" + + "mkdir " + SYSTEM_FOLDER + "\n" + // cluster based app directories + "cat %s > " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + + "chmod 655 " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + + "pm uninstall -k %s\n" + // -k to retain data + "mv " + SYSTEM_FOLDER + APK_NAME + ".tmp " + SYSTEM_FOLDER + APK_NAME + "\n" + + "pm install -r " + SYSTEM_FOLDER + APK_NAME + "\n" + + "sleep 5\n" + CMD_SCRIPT_LOLLIPOP_REBOOT; + + private static final String CMD_UNINSTALL = "mount -o rw,remount /system\n" + + "pm uninstall %s\n" + + "rm -f " + SYSTEM_FOLDER + APK_NAME + "\n" + + "sleep 5\n" + + "mount -o ro,remount /system"; + + String action; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // this activity itself has no content view (see manifest) + + if (getIntent().getAction() == null) { + Log.e(TAG, "Please define an action!"); + finish(); + return; + } + + action = getIntent().getAction(); + if (ACTION_UNINSTALL.equals(action)) { + uninstall(); + } else if (ACTION_INSTALL.equals(action)) { + checkRootTask.execute(); + } else if (ACTION_FIRST_TIME.equals(action)) { + Preferences.get().setFirstTime(false); + checkRootTask.execute(); + } else if (ACTION_POST_INSTALL.equals(action)) { + postInstall(); + } + } + + /** + * first time + */ + private void firstTime() { + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); + + AlertDialog.Builder builder = new AlertDialog.Builder(theme); + String message = getString(R.string.system_install_first_time_message) + + "

"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + message += getString(R.string.system_install_question_lollipop); + } else { + message += getString(R.string.system_install_question); + } + builder.setMessage(Html.fromHtml(message)); + builder.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + installTask.execute(); + } + }); + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }); + builder.create().show(); + } + + /** + * 1. Check for root access + */ + public AsyncTask checkRootTask = new AsyncTask() { + ProgressDialog mProgressDialog; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this, + FDroidApp.getCurThemeResId()); + + mProgressDialog = new ProgressDialog(theme); + mProgressDialog.setMessage(getString(R.string.requesting_root_access_body)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return Shell.SU.available(); + } + + @Override + protected void onPostExecute(Boolean rootGranted) { + super.onPostExecute(rootGranted); + + mProgressDialog.dismiss(); + + if (rootGranted) { + // root access granted + + if (ACTION_UNINSTALL.equals(action)) { + uninstallTask.execute(); + } else if (ACTION_INSTALL.equals(action)) { + installTask.execute(); + } else if (ACTION_FIRST_TIME.equals(action)) { + firstTime(); + } + } else { + // root access denied + + if (!ACTION_FIRST_TIME.equals(action)) { + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this, + FDroidApp.getCurThemeResId()); + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(theme); + alertBuilder.setTitle(R.string.root_access_denied_title); + alertBuilder.setMessage(getString(R.string.root_access_denied_body)); + alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.create().show(); + } + } + } + }; + + /** + * 2. Install into system + */ + AsyncTask installTask = new AsyncTask() { + ProgressDialog mProgressDialog; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this, + FDroidApp.getCurThemeResId()); + + mProgressDialog = new ProgressDialog(theme); + mProgressDialog.setMessage(getString(R.string.system_install_installing)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + + @Override + protected Void doInBackground(Void... voids) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // execute postInstall after reboot + Preferences.get().setPostSystemInstall(true); + + Shell.SU.run(String.format(CMD_SCRIPT_LOLLIPOP, + new String[]{ + InstallIntoSystemDialogActivity.this.getPackageCodePath(), + InstallIntoSystemDialogActivity.this.getPackageName() + })); + } else { + Shell.SU.run(String.format(CMD_SCRIPT_KITKAT, + new String[]{ + InstallIntoSystemDialogActivity.this.getPackageCodePath(), + InstallIntoSystemDialogActivity.this.getPackageName() + })); + } + return null; + } + }; + + /** + * 3. Verify that install worked + */ + private void postInstall() { + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); + + final boolean success = Installer.hasSystemPermissions(this, this.getPackageManager()); + + // enable system installer on installation success + Preferences.get().setSystemInstallerEnabled(success); + + AlertDialog.Builder builder = new AlertDialog.Builder(theme); + builder.setTitle(success ? R.string.system_install_post_success : R.string.system_install_post_fail); + builder.setMessage(success ? R.string.system_install_post_success_message : R.string.system_install_post_fail_message); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + InstallIntoSystemDialogActivity.this.setResult(success ? Activity.RESULT_OK : Activity.RESULT_CANCELED); + InstallIntoSystemDialogActivity.this.finish(); + startActivity(new Intent(InstallIntoSystemDialogActivity.this, FDroid.class)); + } + }); + builder.create().show(); + } + + private void uninstall() { + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); + + final boolean systemApp = Installer.hasSystemPermissions(this, this.getPackageManager()); + + if (systemApp) { + AlertDialog.Builder builder = new AlertDialog.Builder(theme); + builder.setTitle(R.string.system_uninstall); + builder.setMessage(R.string.system_uninstall_message); + builder.setPositiveButton(R.string.system_uninstall_button, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + checkRootTask.execute(); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(theme); + builder.setTitle(R.string.system_permission_denied_title); + builder.setMessage(getString(R.string.system_permission_denied_body)); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } + } + + AsyncTask uninstallTask = new AsyncTask() { + ProgressDialog mProgressDialog; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this, + FDroidApp.getCurThemeResId()); + + mProgressDialog = new ProgressDialog(theme); + mProgressDialog.setMessage(getString(R.string.system_install_uninstalling)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + + @Override + protected Void doInBackground(Void... voids) { + Shell.SU.run(String.format(CMD_UNINSTALL, + new String[]{ + InstallIntoSystemDialogActivity.this.getPackageName() + })); + return null; + } + + @Override + protected void onPostExecute(Void unused) { + super.onPostExecute(unused); + + mProgressDialog.dismiss(); + + // app is uninstalled but still display, kill it! + System.exit(0); + } + }; + +} + diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index 294733b0d..e0fd1098d 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -1,13 +1,17 @@ package org.fdroid.fdroid.views.fragments; import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.support.v4.preference.PreferenceFragment; +import android.text.Html; import android.text.TextUtils; import org.fdroid.fdroid.FDroidApp; @@ -16,6 +20,7 @@ import org.fdroid.fdroid.PreferencesActivity; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.installer.CheckRootAsyncTask; +import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity; import org.fdroid.fdroid.installer.Installer; import java.util.Locale; @@ -277,8 +282,23 @@ public class PreferencesFragment extends PreferenceFragment AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); alertBuilder.setTitle(R.string.system_permission_denied_title); - alertBuilder.setMessage(getActivity().getString(R.string.system_permission_denied_body)); - alertBuilder.setNeutralButton(android.R.string.ok, null); + String message = getActivity().getString(R.string.system_permission_denied_body) + + "

"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + message += getActivity().getString(R.string.system_install_question_lollipop); + } else { + message += getActivity().getString(R.string.system_install_question); + } + alertBuilder.setMessage(Html.fromHtml(message)); + alertBuilder.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent installIntent = new Intent(getActivity(), InstallIntoSystemDialogActivity.class); + installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_INSTALL); + startActivity(installIntent); + } + }); + alertBuilder.setNegativeButton(R.string.cancel, null); alertBuilder.create().show(); } } else { @@ -293,6 +313,23 @@ public class PreferencesFragment extends PreferenceFragment }); } + protected void initUninstallSystemAppPreference() { + Preference pref = findPreference(Preferences.PREF_UNINSTALL_SYSTEM_APP); + pref.setPersistent(false); + + pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + Intent uninstallIntent = new Intent(getActivity(), InstallIntoSystemDialogActivity.class); + uninstallIntent.setAction(InstallIntoSystemDialogActivity.ACTION_UNINSTALL); + startActivity(uninstallIntent); + + return true; + } + }); + } + private void langSpinner(String key) { final ListPreference pref = (ListPreference)findPreference(key); final String[] langValues = getResources().getStringArray(R.array.languageValues); @@ -317,6 +354,7 @@ public class PreferencesFragment extends PreferenceFragment initRootInstallerPreference(); initSystemInstallerPreference(); + initUninstallSystemAppPreference(); } @Override From dbbad1c95ccea99cf44ffadb887a3daa8b57feee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 May 2015 20:37:47 +0200 Subject: [PATCH 04/20] Finish transparent activity on dialog cancels --- .../InstallIntoSystemDialogActivity.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java index bc0a567c6..dee1a24dc 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java @@ -164,6 +164,8 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { + InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED); + InstallIntoSystemDialogActivity.this.finish(); } }); builder.create().show(); @@ -222,7 +224,13 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { AlertDialog.Builder alertBuilder = new AlertDialog.Builder(theme); alertBuilder.setTitle(R.string.root_access_denied_title); alertBuilder.setMessage(getString(R.string.root_access_denied_body)); - alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED); + InstallIntoSystemDialogActivity.this.finish(); + } + }); alertBuilder.create().show(); } } @@ -314,13 +322,25 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { checkRootTask.execute(); } }); - builder.setNegativeButton(R.string.cancel, null); + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED); + InstallIntoSystemDialogActivity.this.finish(); + } + }); builder.create().show(); } else { AlertDialog.Builder builder = new AlertDialog.Builder(theme); builder.setTitle(R.string.system_permission_denied_title); builder.setMessage(getString(R.string.system_permission_denied_body)); - builder.setPositiveButton(R.string.ok, null); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED); + InstallIntoSystemDialogActivity.this.finish(); + } + }); builder.create().show(); } } From c27d8f6287c167b1c73c76b666a48cdd242e7ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 17 May 2015 14:39:56 +0200 Subject: [PATCH 05/20] Enable system installer directly on first start if FDroid is a system-app --- F-Droid/src/org/fdroid/fdroid/FDroid.java | 12 +++++++++--- .../org/fdroid/fdroid/installer/Installer.java | 15 +++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java index 87581b7f3..bf4f0ffb7 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroid.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java @@ -48,6 +48,7 @@ import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity; +import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.views.AppListFragmentPagerAdapter; import org.fdroid.fdroid.views.ManageReposActivity; import org.fdroid.fdroid.views.swap.ConnectSwapActivity; @@ -104,9 +105,14 @@ public class FDroid extends ActionBarActivity { getContentResolver().registerContentObserver(uri, true, new AppObserver()); if (Preferences.get().isFirstTime()) { - Intent installIntent = new Intent(this, InstallIntoSystemDialogActivity.class); - installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_FIRST_TIME); - startActivity(installIntent); + if (Installer.hasSystemPermissions(this, this.getPackageManager())) { + Preferences.get().setSystemInstallerEnabled(true); + Preferences.get().setFirstTime(false); + } else { + Intent installIntent = new Intent(this, InstallIntoSystemDialogActivity.class); + installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_FIRST_TIME); + startActivity(installIntent); + } } } diff --git a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java index 1079f8afa..30e110ba1 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java @@ -181,15 +181,14 @@ abstract public class Installer { } public static boolean hasSystemPermissions(Context context, PackageManager pm) { - int checkInstallPermission = - pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName()); - int checkDeletePermission = - pm.checkPermission(permission.DELETE_PACKAGES, context.getPackageName()); - boolean permissionsGranted = - (checkInstallPermission == PackageManager.PERMISSION_GRANTED - && checkDeletePermission == PackageManager.PERMISSION_GRANTED); + boolean hasInstallPermission = + (pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName()) + == PackageManager.PERMISSION_GRANTED); + boolean hasDeletePermission = + (pm.checkPermission(permission.DELETE_PACKAGES, context.getPackageName()) + == PackageManager.PERMISSION_GRANTED); - return permissionsGranted; + return (hasInstallPermission && hasDeletePermission); } public void installPackage(File apkFile) throws AndroidNotCompatibleException { From 4775c176531bf52fe4edf4660c0416cdfcbdce40 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 13 May 2015 12:09:20 +1000 Subject: [PATCH 06/20] Refactored system app converter into classes. The multiple occurances of "if (Build.SDK_INT < ... )" statements hint at the prospect that there are a couple of different implementations of this class which behave differently. The new classes start with InstallFDroidAsSystem, and then there are SDK specific subclasses which provide the customization relevant for those subclasses. --- .../installer/InstallFDroidAsSystem.java | 167 ++++++++++++++++++ .../InstallIntoSystemDialogActivity.java | 83 +-------- 2 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java new file mode 100644 index 000000000..fec08f6a0 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java @@ -0,0 +1,167 @@ +package org.fdroid.fdroid.installer; + +import android.content.Context; +import android.os.Build; + +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; + +import java.util.ArrayList; +import java.util.List; + +import eu.chainfire.libsuperuser.Shell; + +abstract class InstallFDroidAsSystem { + + protected final Context context; + + public InstallFDroidAsSystem(final Context context) { + this.context = context; + } + + public static InstallFDroidAsSystem create(final Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return new LollipopImpl(context); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return new KitKatToLollipopImpl(context); + } else { + return new PreKitKatImpl(context); + } + } + + protected abstract String getSystemFolder(); + + protected void onPreInstall() { + // To be overridden by relevant base class[es] + } + + public String getWarningInfo() { + return context.getString(R.string.system_install_question); + } + + final void performUninstall() { + final String[] commands = { + "mount -o rw,remount /system", + "pm uninstall " + context.getPackageName(), + "rm -f " + installPath(), + "sleep 5", + "mount -o ro,remount /system" + }; + Shell.SU.run(commands); + } + + final void performInstall() { + onPreInstall(); + Shell.SU.run(getCommands()); + } + + private String installPath() { + return getSystemFolder() + "FDroid.apk"; + } + + private List getCommands() { + final List commands = new ArrayList<>(); + commands.add(makePartitionWriteable()); + commands.add(copyApkToPartition()); + commands.add(uninstallFDroid()); + commands.addAll(reinstallFDroidAsSystem()); + commands.addAll(makePartitionReadOnly()); + return commands; + } + + protected String makePartitionWriteable() { + return "mount -o rw,remount /system"; + } + + protected String copyApkToPartition() { + return "cat " + context.getPackageCodePath() + " > " + installPath() + ".tmp"; + } + + protected String uninstallFDroid() { + return "pm uninstall -k " + context.getPackageName(); + } + + protected List reinstallFDroidAsSystem() { + final List commands = new ArrayList<>(3); + commands.add("mv " + installPath() + ".tmp " + installPath()); + commands.add("pm install -r " + installPath()); + commands.add("sleep 5"); + return commands; + } + + protected List makePartitionReadOnly() { + final List commands = new ArrayList<>(2); + commands.add("mount -o ro,remount /system"); + commands.add("am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true"); + return commands; + } + + private static class PreKitKatImpl extends InstallFDroidAsSystem { + + public PreKitKatImpl(Context context) { + super(context); + } + + @Override + protected String getSystemFolder() { + return "/system/app"; + } + + } + + private static class KitKatToLollipopImpl extends InstallFDroidAsSystem { + + public KitKatToLollipopImpl(Context context) { + super(context); + } + + /** + * New folder introduced in + * https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54 + */ + @Override + protected String getSystemFolder() { + return "/system/priv-app/"; + } + + } + + private static class LollipopImpl extends InstallFDroidAsSystem { + + public LollipopImpl(Context context) { + super(context); + } + + /** + * New cluster based installation and app dirs + */ + @Override + protected String getSystemFolder() { + return "/system/priv-app/FDroid/"; + } + + /** + * TODO: Currently only works with reboot. Find a way how this could work without. + * See http://stackoverflow.com/q/26487750 + */ + @Override + protected List makePartitionReadOnly() { + List commands = new ArrayList<>(3); + commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN"); + commands.add("sleep 1"); + commands.add("reboot"); + return commands; + } + + protected void onPreInstall() { + // Setup preference to execute postInstall after reboot + Preferences.get().setPostSystemInstall(true); + } + + public String getWarningInfo() { + return context.getString(R.string.system_install_question_lollipop); + } + + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java index dee1a24dc..b23374b4e 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java @@ -22,6 +22,7 @@ package org.fdroid.fdroid.installer; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; @@ -37,6 +38,9 @@ import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; +import java.util.ArrayList; +import java.util.List; + import eu.chainfire.libsuperuser.Shell; /** @@ -63,55 +67,6 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { public static final String ACTION_POST_INSTALL = "post_install"; public static final String ACTION_FIRST_TIME = "first_time"; - private static String SYSTEM_FOLDER; - - static { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - SYSTEM_FOLDER = "/system/app/"; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // new folder introduced in - // https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54 - SYSTEM_FOLDER = "/system/priv-app/"; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // new cluster based installation and app dirs - SYSTEM_FOLDER = "/system/priv-app/FDroid/"; - } - } - - private static final String APK_NAME = "FDroid.apk"; - - private static final String CMD_SCRIPT_KITKAT = "mount -o rw,remount /system\n" + - "cat %s > " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + - "chmod 655 " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + - "pm uninstall -k %s\n" + // -k to retain data - "mv " + SYSTEM_FOLDER + APK_NAME + ".tmp " + SYSTEM_FOLDER + APK_NAME + "\n" + - "pm install -r " + SYSTEM_FOLDER + APK_NAME + "\n" + - "sleep 5\n" + - "mount -o ro,remount /system\n" + - "am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true"; - - // TODO: Currently only works with reboot. Find a way how this could work without. - // See http://stackoverflow.com/q/26487750 - private static final String CMD_SCRIPT_LOLLIPOP_REBOOT = "am broadcast -a android.intent.action.ACTION_SHUTDOWN\n" + - "sleep 1\n" + - "reboot"; - - private static final String CMD_SCRIPT_LOLLIPOP = "mount -o rw,remount /system\n" + - "mkdir " + SYSTEM_FOLDER + "\n" + // cluster based app directories - "cat %s > " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + - "chmod 655 " + SYSTEM_FOLDER + APK_NAME + ".tmp\n" + - "pm uninstall -k %s\n" + // -k to retain data - "mv " + SYSTEM_FOLDER + APK_NAME + ".tmp " + SYSTEM_FOLDER + APK_NAME + "\n" + - "pm install -r " + SYSTEM_FOLDER + APK_NAME + "\n" + - "sleep 5\n" + CMD_SCRIPT_LOLLIPOP_REBOOT; - - private static final String CMD_UNINSTALL = "mount -o rw,remount /system\n" + - "pm uninstall %s\n" + - "rm -f " + SYSTEM_FOLDER + APK_NAME + "\n" + - "sleep 5\n" + - "mount -o ro,remount /system"; - String action; @Override @@ -147,13 +102,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); AlertDialog.Builder builder = new AlertDialog.Builder(theme); - String message = getString(R.string.system_install_first_time_message) + - "

"; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - message += getString(R.string.system_install_question_lollipop); - } else { - message += getString(R.string.system_install_question); - } + String message = getString(R.string.system_install_first_time_message) + "

" + InstallFDroidAsSystem.create(getApplicationContext()).getWarningInfo(); builder.setMessage(Html.fromHtml(message)); builder.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() { @Override @@ -260,22 +209,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // execute postInstall after reboot - Preferences.get().setPostSystemInstall(true); - - Shell.SU.run(String.format(CMD_SCRIPT_LOLLIPOP, - new String[]{ - InstallIntoSystemDialogActivity.this.getPackageCodePath(), - InstallIntoSystemDialogActivity.this.getPackageName() - })); - } else { - Shell.SU.run(String.format(CMD_SCRIPT_KITKAT, - new String[]{ - InstallIntoSystemDialogActivity.this.getPackageCodePath(), - InstallIntoSystemDialogActivity.this.getPackageName() - })); - } + InstallFDroidAsSystem.create(getApplicationContext()).performInstall(); return null; } }; @@ -365,10 +299,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - Shell.SU.run(String.format(CMD_UNINSTALL, - new String[]{ - InstallIntoSystemDialogActivity.this.getPackageName() - })); + InstallFDroidAsSystem.create(getApplicationContext()).performUninstall(); return null; } From 709bd9d699a927f2753688ba6c159573c3bced07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 17 May 2015 15:28:17 +0200 Subject: [PATCH 07/20] Simplify subclass changes, fix mkdir missing on lollipop --- .../installer/InstallFDroidAsSystem.java | 149 +++++++++++------- .../InstallIntoSystemDialogActivity.java | 21 +-- 2 files changed, 94 insertions(+), 76 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java index fec08f6a0..864911b16 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java @@ -1,5 +1,25 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + package org.fdroid.fdroid.installer; +import android.annotation.TargetApi; import android.content.Context; import android.os.Build; @@ -11,6 +31,12 @@ import java.util.List; import eu.chainfire.libsuperuser.Shell; +/** + * Partly based on + * http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/ + * https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java + */ +@TargetApi(Build.VERSION_CODES.FROYO) abstract class InstallFDroidAsSystem { protected final Context context; @@ -39,59 +65,47 @@ abstract class InstallFDroidAsSystem { return context.getString(R.string.system_install_question); } - final void performUninstall() { + final void runUninstall() { final String[] commands = { "mount -o rw,remount /system", "pm uninstall " + context.getPackageName(), - "rm -f " + installPath(), + "rm -f " + getInstallPath(), "sleep 5", "mount -o ro,remount /system" }; Shell.SU.run(commands); } - final void performInstall() { + final void runInstall() { onPreInstall(); - Shell.SU.run(getCommands()); + Shell.SU.run(getInstallCommands()); } - private String installPath() { + protected String getInstallPath() { return getSystemFolder() + "FDroid.apk"; } - private List getCommands() { + private List getInstallCommands() { final List commands = new ArrayList<>(); - commands.add(makePartitionWriteable()); - commands.add(copyApkToPartition()); - commands.add(uninstallFDroid()); - commands.addAll(reinstallFDroidAsSystem()); - commands.addAll(makePartitionReadOnly()); - return commands; - } - - protected String makePartitionWriteable() { - return "mount -o rw,remount /system"; - } - - protected String copyApkToPartition() { - return "cat " + context.getPackageCodePath() + " > " + installPath() + ".tmp"; - } - - protected String uninstallFDroid() { - return "pm uninstall -k " + context.getPackageName(); - } - - protected List reinstallFDroidAsSystem() { - final List commands = new ArrayList<>(3); - commands.add("mv " + installPath() + ".tmp " + installPath()); - commands.add("pm install -r " + installPath()); - commands.add("sleep 5"); - return commands; - } - - protected List makePartitionReadOnly() { - final List commands = new ArrayList<>(2); + commands.add("mount -o rw,remount /system"); + commands.addAll(getCopyToSystemCommands()); + commands.add("pm uninstall -k " + context.getPackageName()); // -k to retain data + commands.add("mv " + getInstallPath() + ".tmp " + getInstallPath()); + commands.add("pm install -r " + getInstallPath()); + commands.add("sleep 5"); // wait until the app is really installed commands.add("mount -o ro,remount /system"); + commands.addAll(getPostInstallCommands()); + return commands; + } + + protected List getCopyToSystemCommands() { + final List commands = new ArrayList<>(1); + commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp"); + return commands; + } + + protected List getPostInstallCommands() { + final List commands = new ArrayList<>(1); commands.add("am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true"); return commands; } @@ -116,7 +130,7 @@ abstract class InstallFDroidAsSystem { } /** - * New folder introduced in + * On KitKat, "Some system apps are more system than others" * https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54 */ @Override @@ -126,33 +140,17 @@ abstract class InstallFDroidAsSystem { } + /** + * History of PackageManagerService in Lollipop: + * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java + */ private static class LollipopImpl extends InstallFDroidAsSystem { public LollipopImpl(Context context) { super(context); } - /** - * New cluster based installation and app dirs - */ @Override - protected String getSystemFolder() { - return "/system/priv-app/FDroid/"; - } - - /** - * TODO: Currently only works with reboot. Find a way how this could work without. - * See http://stackoverflow.com/q/26487750 - */ - @Override - protected List makePartitionReadOnly() { - List commands = new ArrayList<>(3); - commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN"); - commands.add("sleep 1"); - commands.add("reboot"); - return commands; - } - protected void onPreInstall() { // Setup preference to execute postInstall after reboot Preferences.get().setPostSystemInstall(true); @@ -162,6 +160,43 @@ abstract class InstallFDroidAsSystem { return context.getString(R.string.system_install_question_lollipop); } + /** + * Cluster-style layout where each app is placed in a unique directory + */ + @Override + protected String getSystemFolder() { + return "/system/priv-app/FDroid/"; + } + + /** + * Create app directory + */ + @Override + protected List getCopyToSystemCommands() { + List commands = new ArrayList<>(2); + commands.add("mkdir " + getSystemFolder()); // create app directory if not existing + commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp"); + return commands; + } + + /** + * TODO: Currently only works with reboot + *

+ * File observers on /system/priv-app/ have been removed because they don't work with the new + * cluser-style layout. See + * https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103 + *

+ * Related stack overflow post: http://stackoverflow.com/q/26487750 + */ + @Override + protected List getPostInstallCommands() { + List commands = new ArrayList<>(3); + commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN"); + commands.add("sleep 1"); + commands.add("reboot"); + return commands; + } + } } diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java index b23374b4e..317236bfb 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java @@ -22,11 +22,9 @@ package org.fdroid.fdroid.installer; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.text.Html; @@ -38,25 +36,10 @@ import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import java.util.ArrayList; -import java.util.List; - import eu.chainfire.libsuperuser.Shell; /** * Note: This activity has no view on its own, it displays consecutive dialogs. - *

- * Partly based on https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java - * http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/ - *

- * Info for lollipop: - * http://stackoverflow.com/q/26487750 - *

- * Removed apk observers in - * https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103 - *

- * History of PackageManagerService: - * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java */ public class InstallIntoSystemDialogActivity extends FragmentActivity { @@ -209,7 +192,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - InstallFDroidAsSystem.create(getApplicationContext()).performInstall(); + InstallFDroidAsSystem.create(getApplicationContext()).runInstall(); return null; } }; @@ -299,7 +282,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - InstallFDroidAsSystem.create(getApplicationContext()).performUninstall(); + InstallFDroidAsSystem.create(getApplicationContext()).runUninstall(); return null; } From 03b7f59dce699c25c7b1577b07945634f5682a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 17 May 2015 15:39:20 +0200 Subject: [PATCH 08/20] Add missing chmod command --- .../org/fdroid/fdroid/installer/InstallFDroidAsSystem.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java index 864911b16..4befd4359 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java @@ -99,8 +99,9 @@ abstract class InstallFDroidAsSystem { } protected List getCopyToSystemCommands() { - final List commands = new ArrayList<>(1); + final List commands = new ArrayList<>(2); commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp"); + commands.add("chmod 655 " + getInstallPath() + ".tmp"); return commands; } @@ -173,9 +174,10 @@ abstract class InstallFDroidAsSystem { */ @Override protected List getCopyToSystemCommands() { - List commands = new ArrayList<>(2); + List commands = new ArrayList<>(3); commands.add("mkdir " + getSystemFolder()); // create app directory if not existing commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp"); + commands.add("chmod 655 " + getInstallPath() + ".tmp"); return commands; } From 51b2aeda1a4b96d1d4e4ce3354dcbc8a7883ee1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 18 May 2015 11:10:57 +0200 Subject: [PATCH 09/20] Rename for consistency --- ...allFDroidAsSystem.java => InstallIntoSystem.java} | 12 ++++++------ .../installer/InstallIntoSystemDialogActivity.java | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) rename F-Droid/src/org/fdroid/fdroid/installer/{InstallFDroidAsSystem.java => InstallIntoSystem.java} (94%) diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java similarity index 94% rename from F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java rename to F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java index 4befd4359..147e123d6 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallFDroidAsSystem.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java @@ -37,15 +37,15 @@ import eu.chainfire.libsuperuser.Shell; * https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java */ @TargetApi(Build.VERSION_CODES.FROYO) -abstract class InstallFDroidAsSystem { +abstract class InstallIntoSystem { protected final Context context; - public InstallFDroidAsSystem(final Context context) { + public InstallIntoSystem(final Context context) { this.context = context; } - public static InstallFDroidAsSystem create(final Context context) { + public static InstallIntoSystem create(final Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return new LollipopImpl(context); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { @@ -111,7 +111,7 @@ abstract class InstallFDroidAsSystem { return commands; } - private static class PreKitKatImpl extends InstallFDroidAsSystem { + private static class PreKitKatImpl extends InstallIntoSystem { public PreKitKatImpl(Context context) { super(context); @@ -124,7 +124,7 @@ abstract class InstallFDroidAsSystem { } - private static class KitKatToLollipopImpl extends InstallFDroidAsSystem { + private static class KitKatToLollipopImpl extends InstallIntoSystem { public KitKatToLollipopImpl(Context context) { super(context); @@ -145,7 +145,7 @@ abstract class InstallFDroidAsSystem { * History of PackageManagerService in Lollipop: * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java */ - private static class LollipopImpl extends InstallFDroidAsSystem { + private static class LollipopImpl extends InstallIntoSystem { public LollipopImpl(Context context) { super(context); diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java index 317236bfb..0634a24ba 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java @@ -85,7 +85,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); AlertDialog.Builder builder = new AlertDialog.Builder(theme); - String message = getString(R.string.system_install_first_time_message) + "

" + InstallFDroidAsSystem.create(getApplicationContext()).getWarningInfo(); + String message = getString(R.string.system_install_first_time_message) + "

" + InstallIntoSystem.create(getApplicationContext()).getWarningInfo(); builder.setMessage(Html.fromHtml(message)); builder.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() { @Override @@ -192,7 +192,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - InstallFDroidAsSystem.create(getApplicationContext()).runInstall(); + InstallIntoSystem.create(getApplicationContext()).runInstall(); return null; } }; @@ -282,7 +282,7 @@ public class InstallIntoSystemDialogActivity extends FragmentActivity { @Override protected Void doInBackground(Void... voids) { - InstallFDroidAsSystem.create(getApplicationContext()).runUninstall(); + InstallIntoSystem.create(getApplicationContext()).runUninstall(); return null; } From a7aa554f66ee57a1dac0d5af422dd8f87adc3961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 18 May 2015 11:47:33 +0200 Subject: [PATCH 10/20] Remove root installer --- F-Droid/res/values/strings.xml | 2 - F-Droid/res/xml/preferences.xml | 4 - .../src/org/fdroid/fdroid/Preferences.java | 6 - .../fdroid/installer/CheckRootAsyncTask.java | 76 ----- .../fdroid/fdroid/installer/Installer.java | 28 -- .../fdroid/installer/RootInstaller.java | 266 ------------------ .../views/fragments/PreferencesFragment.java | 62 ---- 7 files changed, 444 deletions(-) delete mode 100644 F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java delete mode 100644 F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 31d985aad..ce6e74710 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -25,8 +25,6 @@ Notify when updates are available Update history Days to consider apps new or recent: %s - Install using root access - Request root access to install, update, and remove packages Install using system-permissions Use system permissions to install, update, and remove packages Uninstall F-Droid diff --git a/F-Droid/res/xml/preferences.xml b/F-Droid/res/xml/preferences.xml index f89fb09bd..ac9a9f29b 100644 --- a/F-Droid/res/xml/preferences.xml +++ b/F-Droid/res/xml/preferences.xml @@ -84,10 +84,6 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301, USA. - */ - -package org.fdroid.fdroid.installer; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.view.ContextThemeWrapper; - -import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.R; - -import eu.chainfire.libsuperuser.Shell; - -public class CheckRootAsyncTask extends AsyncTask { - ProgressDialog mDialog; - final Context mContext; - final CheckRootCallback mCallback; - - public interface CheckRootCallback { - void onRootCheck(boolean rootGranted); - } - - public CheckRootAsyncTask(Context context, CheckRootCallback callback) { - super(); - this.mContext = context; - this.mCallback = callback; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - - // if the dialog is displayed from the application class, design is missing - // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay - ContextThemeWrapper theme = new ContextThemeWrapper(mContext, FDroidApp.getCurThemeResId()); - - mDialog = new ProgressDialog(theme); - mDialog.setMessage(mContext.getString(R.string.requesting_root_access_body)); - mDialog.setIndeterminate(true); - mDialog.setCancelable(false); - mDialog.show(); - } - - @Override - protected Boolean doInBackground(Void... params) { - return Shell.SU.available(); - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - - mDialog.dismiss(); - - mCallback.onRootCheck(result); - } - -} diff --git a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java index 30e110ba1..0d42f1a19 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java @@ -95,28 +95,10 @@ abstract public class Installer { /** * Creates a new Installer for installing/deleting processes starting from * an Activity - * - * @param activity - * @param pm - * @param callback - * @return - * @throws AndroidNotCompatibleException */ public static Installer getActivityInstaller(Activity activity, PackageManager pm, InstallerCallback callback) { - // if root installer has been activated in preferences -> RootInstaller - boolean isRootInstallerEnabled = Preferences.get().isRootInstallerEnabled(); - if (isRootInstallerEnabled) { - Log.d(TAG, "root installer preference enabled -> RootInstaller"); - - try { - return new RootInstaller(activity, pm, callback); - } catch (AndroidNotCompatibleException e) { - Log.e(TAG, "Android not compatible with RootInstaller!", e); - } - } - // system permissions and pref enabled -> SystemInstaller boolean isSystemInstallerEnabled = Preferences.get().isSystemInstallerEnabled(); if (isSystemInstallerEnabled) { @@ -161,16 +143,6 @@ abstract public class Installer { public static Installer getUnattendedInstaller(Context context, PackageManager pm, InstallerCallback callback) throws AndroidNotCompatibleException { - // if root installer has been activated in preferences -> RootInstaller - boolean useRootInstaller = Preferences.get().isRootInstallerEnabled(); - if (useRootInstaller) { - try { - return new RootInstaller(context, pm, callback); - } catch (AndroidNotCompatibleException e) { - Log.e(TAG, "Android not compatible with RootInstaller!", e); - } - } - if (hasSystemPermissions(context, pm)) { // we have system permissions! return new SystemInstaller(context, pm, callback); diff --git a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java deleted file mode 100644 index 53d3627cd..000000000 --- a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301, USA. - */ - -package org.fdroid.fdroid.installer; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.chainfire.libsuperuser.Shell; - -/** - * Installer using a root shell and "pm install", "pm uninstall" commands - */ -public class RootInstaller extends Installer { - - private static final String TAG = "RootInstaller"; - - Shell.Interactive rootSession; - - public RootInstaller(Context context, PackageManager pm, InstallerCallback callback) - throws AndroidNotCompatibleException { - super(context, pm, callback); - } - - private Shell.Builder createShellBuilder() { - return new Shell.Builder() - .useSU() - .setWantSTDERR(true) - .setWatchdogTimeout(30) - .setMinimalLogging(false); - } - - @Override - protected void installPackageInternal(final File apkFile) throws AndroidNotCompatibleException { - rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() { - - // Callback to report whether the shell was successfully - // started up - @Override - public void onCommandResult(int commandCode, int exitCode, List output) { - if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) { - // NOTE: Additional exit codes: - // Shell.OnCommandResultListener.SHELL_WRONG_UID - // Shell.OnCommandResultListener.SHELL_EXEC_FAILED - - Log.e(TAG, "Error opening root shell with exitCode " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } else { - addInstallCommand(apkFile); - } - } - }); - } - - @Override - protected void installPackageInternal(final List apkFiles) - throws AndroidNotCompatibleException { - rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() { - - // Callback to report whether the shell was successfully - // started up - @Override - public void onCommandResult(int commandCode, int exitCode, List output) { - if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) { - // NOTE: Additional exit codes: - // Shell.OnCommandResultListener.SHELL_WRONG_UID - // Shell.OnCommandResultListener.SHELL_EXEC_FAILED - - Log.e(TAG, "Error opening root shell with exitCode " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } else { - addInstallCommand(apkFiles); - } - } - }); - } - - @Override - protected void deletePackageInternal(final String packageName) - throws AndroidNotCompatibleException { - rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() { - - // Callback to report whether the shell was successfully - // started up - @Override - public void onCommandResult(int commandCode, int exitCode, List output) { - if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) { - // NOTE: Additional exit codes: - // Shell.OnCommandResultListener.SHELL_WRONG_UID - // Shell.OnCommandResultListener.SHELL_EXEC_FAILED - - Log.e(TAG, "Error opening root shell with exitCode " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - } else { - addDeleteCommand(packageName); - } - } - }); - - } - - @Override - public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { - // no need to handle onActivityResult - return false; - } - - private void addInstallCommand(File apkFile) { - // Like package names, apk files should also only contain letters, numbers, dots, or underscore, - // e.g., org.fdroid.fdroid_9.apk - if (!isValidPackageName(apkFile.getName())) { - Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): " - + apkFile.getName()); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - return; - } - - rootSession.addCommand("pm install -dr \"" + apkFile.getAbsolutePath() + "\"", 0, - new Shell.OnCommandResultListener() { - public void onCommandResult(int commandCode, int exitCode, List output) { - // close su shell - rootSession.close(); - - if (exitCode < 0) { - Log.e(TAG, "Install failed with exit code " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } else { - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); - } - } - }); - } - - private void addInstallCommand(List apkFiles) { - List commands = new ArrayList<>(); - String pm = "pm install -dr "; - for (File apkFile : apkFiles) { - // see addInstallCommand() - if (!isValidPackageName(apkFile.getName())) { - Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): " - + apkFile.getName()); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - return; - } - commands.add(pm + "\"" + apkFile.getAbsolutePath() + "\""); - } - - rootSession.addCommand(commands, 0, - new Shell.OnCommandResultListener() { - public void onCommandResult(int commandCode, int exitCode, - List output) { - // close su shell - rootSession.close(); - - if (exitCode < 0) { - Log.e(TAG, "Install failed with exit code " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } else { - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); - } - } - }); - - } - - private void addDeleteCommand(String packageName) { - if (!isValidPackageName(packageName)) { - Log.e(TAG, "Package name is not valid (contains characters other than letters, numbers, dots, or underscore): " - + packageName); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - return; - } - - rootSession.addCommand("pm uninstall \"" + packageName + "\"", 0, - new Shell.OnCommandResultListener() { - public void onCommandResult(int commandCode, int exitCode, List output) { - // close su shell - rootSession.close(); - - if (exitCode < 0) { - Log.e(TAG, "Delete failed with exit code " + exitCode); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - } else { - mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); - } - } - }); - } - - @Override - public boolean supportsUnattendedOperations() { - return true; - } - - private static final Pattern PACKAGE_NAME_BLACKLIST = Pattern.compile("[^a-zA-Z0-9\\.\\_]"); - - /** - * Package names should only contain letters, numbers, dots, and underscores! - * Prevent injection attacks with app names like ";touch $'\057data\057injected'" - * - * @param packageName - * @return - */ - private boolean isValidPackageName(String packageName) { - Matcher matcher = PACKAGE_NAME_BLACKLIST.matcher(packageName); - return !matcher.find(); - } - - /** - * pm install [-l] [-r] [-t] [-i INSTALLER_PACKAGE_NAME] [-s] [-f] [--algo - * --key --iv ] [--originating-uri - * ] [--referrer ] PATH - *

- * pm install: installs a package to the system. - *

- * Options:
- * -l: install the package with FORWARD_LOCK.
- * -r: reinstall an existing app, keeping its data.
- * -t: allow test .apks to be installed.
- * -i: specify the installer package name.
- * -s: install package on sdcard.
- * -f: install package on internal flash.
- * -d: allow version code downgrade.
- *

- * pm uninstall [-k] PACKAGE - *

- * pm uninstall: removes a package from the system. - *

- * Options:
- * -k: keep the data and cache directories around after package removal. - */ - -} diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index e0fd1098d..b12fee0d4 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -19,7 +19,6 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.PreferencesActivity; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.installer.CheckRootAsyncTask; import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity; import org.fdroid.fdroid.installer.Installer; @@ -45,7 +44,6 @@ public class PreferencesFragment extends PreferenceFragment Preferences.PREF_LANGUAGE, Preferences.PREF_CACHE_APK, Preferences.PREF_EXPERT, - Preferences.PREF_ROOT_INSTALLER, Preferences.PREF_SYSTEM_INSTALLER, Preferences.PREF_ENABLE_PROXY, Preferences.PREF_PROXY_HOST, @@ -162,10 +160,6 @@ public class PreferencesFragment extends PreferenceFragment checkSummary(key, R.string.expert_on); break; - case Preferences.PREF_ROOT_INSTALLER: - checkSummary(key, R.string.root_installer_on); - break; - case Preferences.PREF_SYSTEM_INSTALLER: checkSummary(key, R.string.system_installer_on); break; @@ -196,61 +190,6 @@ public class PreferencesFragment extends PreferenceFragment } } - /** - * Initializes RootInstaller preference. This method ensures that the preference can only be checked and persisted - * when the user grants root access for F-Droid. - */ - protected void initRootInstallerPreference() { - CheckBoxPreference pref = (CheckBoxPreference) findPreference(Preferences.PREF_ROOT_INSTALLER); - - // we are handling persistence ourself! - pref.setPersistent(false); - - pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - final CheckBoxPreference pref = (CheckBoxPreference) preference; - - if (pref.isChecked()) { - CheckRootAsyncTask checkTask = new CheckRootAsyncTask(getActivity(), new CheckRootAsyncTask.CheckRootCallback() { - - @Override - public void onRootCheck(boolean rootGranted) { - if (rootGranted) { - // root access granted - SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); - editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, true); - editor.commit(); - pref.setChecked(true); - } else { - // root access denied - SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); - editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false); - editor.commit(); - pref.setChecked(false); - - AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - alertBuilder.setTitle(R.string.root_access_denied_title); - alertBuilder.setMessage(getActivity().getString(R.string.root_access_denied_body)); - alertBuilder.setNeutralButton(android.R.string.ok, null); - alertBuilder.create().show(); - } - } - }); - checkTask.execute(); - } else { - SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); - editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false); - editor.commit(); - pref.setChecked(false); - } - - return true; - } - }); - } - /** * Initializes SystemInstaller preference, which can only be enabled when F-Droid is installed as a system-app */ @@ -352,7 +291,6 @@ public class PreferencesFragment extends PreferenceFragment updateSummary(key, false); } - initRootInstallerPreference(); initSystemInstallerPreference(); initUninstallSystemAppPreference(); } From 6e89a558798616faf23ddc62318cd7a80c3f49fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 10 May 2015 20:27:35 +0200 Subject: [PATCH 11/20] Add confirmation dialog to uninstalling via SystemInstaller --- F-Droid/res/values/strings.xml | 3 + .../fdroid/installer/SystemInstaller.java | 58 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index ce6e74710..22c997fdd 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -341,4 +341,7 @@ May work Promising Best bet + + Do you want to replace this app with the factory version? + Do you want to uninstall this app? diff --git a/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java index fa155ac74..719756d29 100644 --- a/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java +++ b/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java @@ -19,8 +19,11 @@ package org.fdroid.fdroid.installer; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageInstallObserver; import android.content.pm.PackageManager; @@ -32,6 +35,8 @@ import java.io.File; import java.lang.reflect.Method; import java.util.List; +import org.fdroid.fdroid.R; + /** * Installer based on using internal hidden APIs of the Android OS, which are * protected by the permissions @@ -137,13 +142,62 @@ public class SystemInstaller extends Installer { } } + @Override - protected void installPackageInternal(List apkFiles) throws AndroidNotCompatibleException { + protected void installPackageInternal(List apkFiles) + throws AndroidNotCompatibleException { // not used } @Override - protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException { + protected void deletePackageInternal(final String packageName) + throws AndroidNotCompatibleException { + ApplicationInfo appInfo; + try { + appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "Failed to get ApplicationInfo for uninstalling"); + return; + } + + final boolean isUpdate = ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); + int messageId; + if (isUpdate) { + messageId = R.string.uninstall_update_confirm; + } else { + messageId = R.string.uninstall_application_confirm; + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(appInfo.loadLabel(mPm)); + builder.setIcon(appInfo.loadIcon(mPm)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + doDeletePackageInternal(packageName); + } catch (AndroidNotCompatibleException e) { + mCallback.onError(InstallerCallback.OPERATION_DELETE, + InstallerCallback.ERROR_CODE_OTHER); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + mCallback.onError(InstallerCallback.OPERATION_DELETE, + InstallerCallback.ERROR_CODE_CANCELED); + } + }); + builder.setMessage(messageId); + builder.create().show(); + } + + private void doDeletePackageInternal(final String packageName) + throws AndroidNotCompatibleException { try { mDeleteMethod.invoke(mPm, packageName, mDeleteObserver, 0); } catch (Exception e) { From 8e62d9ae7e47c3d84895761b254034604b6ea80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 10 May 2015 22:43:05 +0200 Subject: [PATCH 12/20] Add install confirm+perms screen to SystemInstaller --- F-Droid/AndroidManifest.xml | 8 + .../ic_bullet_key_permission.png | Bin 0 -> 545 bytes F-Droid/res/drawable-hdpi/ic_coins_s.png | Bin 0 -> 1100 bytes .../drawable-hdpi/tab_unselected_holo.9.png | Bin 0 -> 153 bytes .../ic_bullet_key_permission.png | Bin 0 -> 611 bytes .../ic_bullet_key_permission.png | Bin 0 -> 453 bytes F-Droid/res/drawable-mdpi/ic_coins_s.png | Bin 0 -> 741 bytes .../drawable-mdpi/tab_unselected_holo.9.png | Bin 0 -> 157 bytes .../ic_bullet_key_permission.png | Bin 0 -> 646 bytes F-Droid/res/drawable-xhdpi/ic_coins_s.png | Bin 0 -> 1483 bytes .../drawable-xhdpi/tab_unselected_holo.9.png | Bin 0 -> 166 bytes .../ic_bullet_key_permission.png | Bin 0 -> 1518 bytes .../drawable-xxhdpi/tab_unselected_holo.9.png | Bin 0 -> 1054 bytes F-Droid/res/drawable/ic_text_dot.xml | 22 + F-Droid/res/layout/app_permission_item.xml | 50 ++ .../res/layout/app_permission_item_money.xml | 76 +++ .../res/layout/app_permission_item_old.xml | 55 ++ F-Droid/res/layout/app_perms_summary.xml | 43 ++ F-Droid/res/layout/install_app_details.xml | 68 +++ F-Droid/res/layout/install_confirm.xml | 149 +++++ F-Droid/res/layout/install_start.xml | 37 ++ F-Droid/res/layout/label.xml | 20 + F-Droid/res/layout/permissions_list.xml | 53 ++ F-Droid/res/values/colors.xml | 7 +- F-Droid/res/values/strings.xml | 29 +- .../installer/AppSecurityPermissions.java | 525 ++++++++++++++++++ .../installer/CaffeinatedScrollView.java | 75 +++ .../installer/InstallConfirmActivity.java | 251 +++++++++ .../fdroid/fdroid/installer/Installer.java | 12 - .../fdroid/installer/SystemInstaller.java | 44 +- .../fdroid/fdroid/installer/TabsAdapter.java | 163 ++++++ .../fdroid/localrepo/LocalRepoService.java | 3 +- .../org/fdroid/fdroid/RepoUpdaterTest.java | 2 +- 33 files changed, 1666 insertions(+), 26 deletions(-) create mode 100644 F-Droid/res/drawable-hdpi/ic_bullet_key_permission.png create mode 100644 F-Droid/res/drawable-hdpi/ic_coins_s.png create mode 100644 F-Droid/res/drawable-hdpi/tab_unselected_holo.9.png create mode 100644 F-Droid/res/drawable-ldpi/ic_bullet_key_permission.png create mode 100644 F-Droid/res/drawable-mdpi/ic_bullet_key_permission.png create mode 100644 F-Droid/res/drawable-mdpi/ic_coins_s.png create mode 100644 F-Droid/res/drawable-mdpi/tab_unselected_holo.9.png create mode 100644 F-Droid/res/drawable-xhdpi/ic_bullet_key_permission.png create mode 100644 F-Droid/res/drawable-xhdpi/ic_coins_s.png create mode 100644 F-Droid/res/drawable-xhdpi/tab_unselected_holo.9.png create mode 100644 F-Droid/res/drawable-xxhdpi/ic_bullet_key_permission.png create mode 100644 F-Droid/res/drawable-xxhdpi/tab_unselected_holo.9.png create mode 100644 F-Droid/res/drawable/ic_text_dot.xml create mode 100644 F-Droid/res/layout/app_permission_item.xml create mode 100644 F-Droid/res/layout/app_permission_item_money.xml create mode 100644 F-Droid/res/layout/app_permission_item_old.xml create mode 100644 F-Droid/res/layout/app_perms_summary.xml create mode 100644 F-Droid/res/layout/install_app_details.xml create mode 100644 F-Droid/res/layout/install_confirm.xml create mode 100644 F-Droid/res/layout/install_start.xml create mode 100644 F-Droid/res/layout/label.xml create mode 100644 F-Droid/res/layout/permissions_list.xml create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/AppSecurityPermissions.java create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/CaffeinatedScrollView.java create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java create mode 100644 F-Droid/src/org/fdroid/fdroid/installer/TabsAdapter.java diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index cd48da400..679b6d7af 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -292,6 +292,14 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".FDroid" /> + + + ArXh)PIb&XWFX+WUv|m8B?Sy(whYpI z4M$XWu%tUL^4``mIi%=WX(GE)OVaj}Y$x+mE06s@{;fhORBNi&Q{Dnq&o9jS>YR~3 zA2rQUxqsrFpI=<;%}+UOf49$`GcC$%*DI|jkL|kp5`5$oZITXmGB`XqqITAZXA1I!rWdkM^V<_pW&RL)!75e*Zd#xPc}W&ubpz} z)1nud{*M(?JJ!SoY`@XNVk3`tH*T4`|Epd$~Nl7e8wMs5Z1yT$~ z28M>Zh6cKZCLxAqRwkxaCdRr3=2iv<=cb=iM$wR)pOTqYiLSxG%D@n!!OLtB2T+3~ u$cEtjw370~qErUQl>DSr1<%~X^wgl##FWaylc_d9MGT&!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ueoXKL{?^yL>WGgtNdSvY3H^TL^?1FWs&C!@$7w!_&nv#N+tftKJ8V@{z0k{Dx_E~`X{LjsM32TUg9RN;KbQ)iDrtwCX&n?l z#uENikhq7;uJ!ibMPHPU+O6QXt)*JcD zMNhd)95e6Q{8e$Y{=F^69X<>t6+FA03M@+Rcdyqw*RHsmyT84!O3VRh)9eqLe%#ZH zS<~0(?3P*gSF2-<(3)*>0gtx@+?&_(Dd(;L_YJedZ+AJy_Xc+{{JUSu_UyOaD6J*7 zj7I5mB6Ard-W4A^^U!pqm>lneoeVoUZ*T7Idz4et&Hgd*$Hs}97R|iX{~^%8w&VHZ zb==)mjAiO#tMV?ae^g|>?s(jvw&H@g25zPHkHtTJ^i7!0k;S;BrB`xid-?|E^N*}` z9vu&eNw)LZuk|Ol?NiOQmeS*!ZaT00|AuGbeP!i&zosiZXS1xGcm17Av(6uuYY+5u zjz#XUmw0+yTdn&13Xg_4+niT^l?gJ~ArXGWZui^7n6BH(A4&x>lIHJUR#C z_#-e)=h%P#l{2nAbjmq6y+UErTF2Rp>kc2EI7_-mI$q92d5-+f*mXH?7c$&!RPIuo zlKeq(-NERCTV@6+HI*wd%{{Nh^TBhEWm3_Xbz#pF_jE1)$mVyvH)!En%>$c`9{<=I za#6gcdfSv`PcPoozVk83pqlT#x9#$mfpmn(eL&HhUj;nCRMd`t%2m zJl>=G-Gp@7AF-}YLCpHy-Ygk=dbUGer3M>LDA&RkNj%{-kmJ`!Tx#A zKej*5?%PQ}WSI!e7@{Sv5hcz=sfi_-`FRQ9s&XYfl%)5RRG2KmPx>XJ%$TT(5HGK*O1ak257UFsy01aiHFf4G12u u^?&$m-q&fz7T&FR9W0_QnO}Q~fkExE@YIkG=lX%hFnGH9xvXE@AWHNvAv3=$)fM` zEiEh0owVHl`B3~+174Ptqbnp|w`mGD{5Z_8dC`^o%NPYR6Ksw?bZQ8`m4U}ve2T;gy)+U{Jb^UZa1s#u9tp%hRJ^z z+yk6v#_zd0XL6x&-m$a$?!G=WYmv}5jrErV3RUmeCMA1D>S%VSPJ8L;v3$kLT!|Iz zrVR^3YYvP3{lhA(?H~}7pSZp{R8)8F^BkGA8LeGmS09=j*ACg5D3Kb`())Jq?g^3% zKC{l={c}rg>XsdHdDogHA{piUy>x4H1H(_fNf)0!SNBbJXQJsmPr(JN|5{4QcDu)S z^^41<>sM~rfA?#5RL6vbJ(?n_VsAWJ8QAK-&Rp;MhbR2}uisDqzh)Mn-Wgl)-Od>p z2dX8m5hW>!C8<`)MX5lF!N|bSK-bVv*T5*m(9p`j#LC!I*TCG$z+j^Is@o_Ua`RI% z(<*Umc;F=jie*WV4Z-k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(epwmK8Xr18D^?ZvQoBE|787)5S5w$+S{q5R~8 zyX|kU7z!Arzqxd5ee+zW?#C4;Ug!&a+phCk+EYuzf+2KHn(bPZhuc2}Ox_XaT~w5~ zb<=Or4a>@`UTVEuxV$H3(S!di9L+Bz7$sgZP5y0or9-aZ-~#t6JFjcI9A?;~@S~A! z;?e{A?=$s@@?|_WozbhKW?kfbXeMWc^9SMQHJouRtkV(_Y-i6t5S}=bamL2RPM77< zFBi>TvisRvx2{#cwtZSOKUAk8e@2hWbg5-cP0k+}7g#Kv;QJ)~8qi~^C9V-ADTyVi zR>?)FK#IZ0z|c_F&_LJFB*f6n%EZ*l#8}tB+{(b<-1KwGC>nC}Q!>*k(KQ%Y85lw| zc$qEY0BVo~*$|wcR#Ki=l*-_klAn~S;F+74o*I;zm{M7IGSvpCh{4m<&t;ucLK6UC Cg`{Wz literal 0 HcmV?d00001 diff --git a/F-Droid/res/drawable-mdpi/ic_coins_s.png b/F-Droid/res/drawable-mdpi/ic_coins_s.png new file mode 100644 index 0000000000000000000000000000000000000000..3b8fd8abd5277e2ae61becdfbdee85a8ec89c3ca GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ueoXKL{?^yL>WGgtNdSvY3H^TL^?1FWs&C12lf3r;B5V#p%+^w%H+x0;~_7-`oAs zc)=o$MPgb@gl3ygh+xtEBEMu4mspc;j?N#(s9P&rd;c-#-c^cP(dxpo)nHLapuj{? zmkVz;RqK9>+2J80@$Bhei{fWz_PE}CJxwCn`rJdFzL__w*E~?HTfB3%#cnREn==l} z%DDF`F_=#K$R+-94KEW=>AVBUZ;qXK7WOb)XzxLhwx%-6{`Q?aX7U^Kdh4d8GH=+N z`1t>|Id6}a&he5=oq6m3mM;>j+pY({NahU>`XqEr_1+^hyVk&|H@=nc=sc>@Bt7Hh z1MYckN9AOE_KNO%TW4>)ZX&E6V7A_nc$yGQ8bd3CuO#<_{dDHwt@w-ykY} z=^DT0@^kKmEr8ycDYnX(M1Koz7QC9x#cDjB3ImBGls&`8(7QrE~d z#L(2rz}(8zSQkhd7?hMA0GWu;kei>9nO2Eg1FJ^GKA;8}kPX54X(i=}MX3yqDfvmM i3T~N2spa`a*~JRZ!KQ^Hr>mdKI;Vst04D7$hX4Qo literal 0 HcmV?d00001 diff --git a/F-Droid/res/drawable-xhdpi/ic_bullet_key_permission.png b/F-Droid/res/drawable-xhdpi/ic_bullet_key_permission.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0bdfcb1b43be721326d91b544e0878cdbd1027 GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;rX+877l!}s{b%+Ad7K3vk;OpT z1B~5HX4`=T%L*LRfwTh{zxwCawb0DYxw>?OZkO z|GZgk;#qa3?3=0#)1~-YUmRGvI$R|sT+epKc7a6oc`H(#owlt|znbL6c-VB#wGT(9 zu8x|qY5D)>^)q}PEu5}*{ICArF9mb5KUuC-e5s*QweN%AkCWm1l<$7{x_#%Idy{gP zJw8(R;{B{k>&%v>Rs~CegH3?t{W)d7c?*s2p0#@gj3U(%*NBpo#FA92>rd0T%!O literal 0 HcmV?d00001 diff --git a/F-Droid/res/drawable-xhdpi/ic_coins_s.png b/F-Droid/res/drawable-xhdpi/ic_coins_s.png new file mode 100644 index 0000000000000000000000000000000000000000..13d134e6b64e2760f3c9ed505fe17d4c4a9853e0 GIT binary patch literal 1483 zcmZ{kdpOe#6vuxt$>j1j%gp^QCCnycUUONqR${EEDZ`W%+nAAia$A(!DtaXqT9U#; z5k-?JrHcyXGA&WB%$;h_yLbM4|9GGCob!FoIiK%y{ySV>AG{hE4h8@~jo|G?lB>~r zZrUiXv5ARGaseH4-{%eh4F#%Fs*=3l5KF>)0QLQP-`2@6Z_+*hNVEWe^h^NwB_E|v z13;V|0DKPt0OwKw&}Npm`C;UXjiE%m7a&`Y%8r{Q@`(!5J17hPR`sQu(=f zf|q+hprCq?kk2D)D$gB_*YKiX9nHZe_V%4rO3!f$MA?rES3-$6WY$C28DhQ#9IfN4 z0mlU(&nAkV;Bpe|EOzD+uNT2^pm-G+7F0I7GWUJCJu)Iu^T-12(W&6OwPTNK{~;e$ zx}4$~O7_K#b~+mj*el_?$Bsy|4d;`L4|^)^m6yY=o!&?R}NYXG5YC zYM0>e3=jH0^9je-!X{rExs{^ei%gai!piLBBLniuVtO0dF{c$yf^mqLdH9D3a`_?Y zn1?7f8D{lPcC+Ey0ax}NK|;Q&%Pz4ZMF;VI1`AmYEzmkIU|lzG=1fpWbngEsIM&!* zx~RaVqcG*^S!>PdmXH@+*fB#1>eepGnZKAgX@yGN=^JJ(n62lmIo?%owzFh^U)urM z>*%`+D_nlkwRiWN#ZDF1)l0s@=TL;J-bufC_(`E^f@16(xJ7FtJuCSRlS%CD?*i`- z6x$)S?jpr#+i}Feq?nYKqHZX@lVG^L=)D%#Ry0*OnsKEeO}XAChH6IA4R$UZKj;l@ z*q~a~EmgQm=t%P~G8}yXpW2P-`xJ+$Na~v!<_}E^1KRb*P;0ps+So=(%bnfc2+1`!&2)hl_BsNYB`ez{+Oy=*y3NHEZ? zZcDS`?JcPdIcf9&Z*pIMNbm8hONpJ<+?7Q-M@J-a`&>5DIb1ozb1>CYOU{y*`3&W! zIKi7t(<1P7<3jQpKlrG67wp!tyPHbY#2`iUgL#^4-PEWHrK}W~A(Y=-GfBDcA}A|M z_$bogt+e-=AM|cRUic~hCbNvUj6yEvR>&?wBo4-y1N3S)7==#re?DBX-D36Z zPHy*y?Du)6|ANq|efzi5;iW*ZD3`!DvicQ!* zj<@t0zoOedmDzqyzO{6b5cH?%7om~0g+KO0Bwsv+1euAkrAi<-=Jib@JMdZTfgAXh+xN4V`zYTI3tv1v^z3} c7SD(|5o_e>k76~89pxNA!1;L9dxT{C3-1hx3;+NC literal 0 HcmV?d00001 diff --git a/F-Droid/res/drawable-xhdpi/tab_unselected_holo.9.png b/F-Droid/res/drawable-xhdpi/tab_unselected_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..9465173781eaa7fc9ab0e191904f928ad1c0b8f4 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ#0V1dK=^FznmSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wLn1+PZ!4!j+x0n{{OdUW@bKIZ*t~9!x;yr|1&pm7#RS;1ieP?Kk11H z2?+-Ger=J9{IX`d2{ZSUyAFF~k44WnGFVV~q2hHg8$(d6=$w>Y|ILABGI+ZBxvX8JZ{~IQEfqBRDZL5C%*V34;ZlLAHoZrr|VOO`BtCuX0?CDAfe%|R^BWA_bM9C?GgCq~58U*(HmAIC*MafP7~=ZYWZh+rM8 zD3L5hN9;1P-BqmZiCD}g(4@o(9*(PxbU=}hlQ_}G@u00l4?0PTVHH^^&q}m7EietV z*Di2=a5=vivyT1$j(Y{`{XaQ_WoKZeIsVlwkt4YSmF-&V%fj05(5yT$f;<`o-~2NG zM8D|3t)Ae`#ZKpLyg0ROJnP8e!#k6%e?&k~E0g?IXm7V+^4#>)rWY<8n9MBKzjr4F z?`*K)ozL}WKm;D2)Nydi^68DyEVy(bb$p1^-TSL6ywD-uo9XZjR2c8S9Y3)_l?Q-8 zR0xO#q7#9q74Cm%-7@&F{oLu%ozMQ(UpikLcArc-7XQ=8(c}L9H;Igl>YP;1NK5Wr z^-EumaQ2(Tus-P3L!X&8{S@s@#Wg@^7`qz|clF3r_3`o|b%s66V2cg}x*HY9=m;I8j1#|&4t7A2fX;5K-onW34K&(i0Kaw)!~Ei^K*;T;cW|{Z}Buw!729Yg|WbEd0&Oyi`2mB zvv*T_2anH|HZ=Ftomvbxg>tU6&v}lv-`M>7Qs1O&dcNf$N$#CKnZ4z)scmZUV)E3> za~~v41iShU+BNam!wS_R=YMyUBTAT6|Dz3#^VU#}k!d*LuqG-X0$ z`U2#E;JR^K1M849(O)+XwGLbBJ`$QJ#A|^?>(NaoV4Az5g~n9b0QI z@<>fDa`X>xJGLroRVlBFRl^XaTpq%_0;}a*wrXUxavm63!f_Z@(&UPy=ve?OIS36{ zmc&Y>pj2c<5-CSE!PdNxn4X0aU5DxpvGS2vRSS?waL~edHR*uG9ws>KVV~8OvTWV7 z94|h{<=Kx`M}cz}+0_8M>>$6YbBI2W^8i9c28sxhxC=7Omlatt^18$gvG)JuOrXvP zadZ6BEXft^z<7Jm`t)!xJjkOH6VTB}PvmClST$;TxfOlx{My?qji%FIHpjkgJxQJ4 zd7ypVoE;n4N^Sorz0AyinjU{KKa+YC%{cFWo_lV--JQO9>-VcS>52PauVd%+)Aan~VPi)O-=-ZXs_x=DA97YZR literal 0 HcmV?d00001 diff --git a/F-Droid/res/drawable/ic_text_dot.xml b/F-Droid/res/drawable/ic_text_dot.xml new file mode 100644 index 000000000..f8f396453 --- /dev/null +++ b/F-Droid/res/drawable/ic_text_dot.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/F-Droid/res/layout/app_permission_item.xml b/F-Droid/res/layout/app_permission_item.xml new file mode 100644 index 000000000..b5aab370d --- /dev/null +++ b/F-Droid/res/layout/app_permission_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/F-Droid/res/layout/app_permission_item_money.xml b/F-Droid/res/layout/app_permission_item_money.xml new file mode 100644 index 000000000..6b29bcd77 --- /dev/null +++ b/F-Droid/res/layout/app_permission_item_money.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + diff --git a/F-Droid/res/layout/app_permission_item_old.xml b/F-Droid/res/layout/app_permission_item_old.xml new file mode 100644 index 000000000..a5b395ede --- /dev/null +++ b/F-Droid/res/layout/app_permission_item_old.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/F-Droid/res/layout/app_perms_summary.xml b/F-Droid/res/layout/app_perms_summary.xml new file mode 100644 index 000000000..065dab090 --- /dev/null +++ b/F-Droid/res/layout/app_perms_summary.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/F-Droid/res/layout/install_app_details.xml b/F-Droid/res/layout/install_app_details.xml new file mode 100644 index 000000000..bed2ecfc2 --- /dev/null +++ b/F-Droid/res/layout/install_app_details.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + diff --git a/F-Droid/res/layout/install_confirm.xml b/F-Droid/res/layout/install_confirm.xml new file mode 100644 index 000000000..911e8d944 --- /dev/null +++ b/F-Droid/res/layout/install_confirm.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +