diff --git a/.gitmodules b/.gitmodules index 46dfaab88..1a9e0b49c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ path = extern/nanohttpd url = https://github.com/eighthave/nanohttpd ignore = dirty +[submodule "extern/libsuperuser"] + path = extern/libsuperuser + url = https://github.com/dschuermann/libsuperuser.git + ignore = dirty \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ff5bf54dc..2b6253537 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + + + + + + + /dev/null diff --git a/build.gradle b/build.gradle index e15c643a0..d70eda598 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ dependencies { compile project(':extern:AndroidPinning') compile project(':extern:UniversalImageLoader:library') compile project(':extern:MemorizingTrustManager') + compile project(':extern:libsuperuser:libsuperuser') } project(':extern:UniversalImageLoader:library') { diff --git a/extern/libsuperuser b/extern/libsuperuser new file mode 160000 index 000000000..faffc4112 --- /dev/null +++ b/extern/libsuperuser @@ -0,0 +1 @@ +Subproject commit faffc41121b509b2b1b01d4ecac3f395e4adbee2 diff --git a/project.properties b/project.properties index 252c5c7d2..af62c9046 100644 --- a/project.properties +++ b/project.properties @@ -6,3 +6,4 @@ android.library.reference.1=extern/UniversalImageLoader/library android.library.reference.2=extern/MemorizingTrustManager android.library.reference.3=extern/AndroidPinning android.library.reference.4=extern/nanohttpd +android.library.reference.5=extern/libsuperuser/libsuperuser diff --git a/res/values/strings.xml b/res/values/strings.xml index 86fae6b4a..de6073c47 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -28,7 +28,13 @@ Do not notify of any updates Update history Days to consider apps new or recent: %s - + Install using root access + Request root access to install, update, and remove packages + Do not request root access to install, update, and remove packages + Install using system-permissions + Use system permissions to install, update, and remove packages + Do not use system permissions to install, update, and remove packages + Search Results App Details No such app found @@ -261,5 +267,15 @@ Security System Wallpaper - + + Root access + Requesting root access… + Root access denied + Either your Android device is not rooted or you have denied root access for F-Droid. + Update all + (De-)Installation Error + 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. + diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 5ae1f7d10..fa534900f 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -3,7 +3,8 @@ + + diff --git a/settings.gradle b/settings.gradle index 66e6ee88a..ed61ad01d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':extern:AndroidPinning', ':extern:UniversalImageLoader:library', ':extern:MemorizingTrustManager' +include ':extern:AndroidPinning', ':extern:UniversalImageLoader:library', ':extern:MemorizingTrustManager', ':extern:libsuperuser:libsuperuser' diff --git a/src/android/content/pm/IPackageDeleteObserver.java b/src/android/content/pm/IPackageDeleteObserver.java new file mode 100644 index 000000000..88b83a553 --- /dev/null +++ b/src/android/content/pm/IPackageDeleteObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 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 android.content.pm; + +/** + * Just a non-working implementation of this Stub to satisfy compiler! + */ +public interface IPackageDeleteObserver extends android.os.IInterface { + + public abstract static class Stub extends android.os.Binder implements + android.content.pm.IPackageDeleteObserver { + public Stub() { + throw new RuntimeException("Stub!"); + } + + public static android.content.pm.IPackageDeleteObserver asInterface(android.os.IBinder obj) { + throw new RuntimeException("Stub!"); + } + + public android.os.IBinder asBinder() { + throw new RuntimeException("Stub!"); + } + + public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, + int flags) throws android.os.RemoteException { + throw new RuntimeException("Stub!"); + } + } + + public abstract void packageDeleted(java.lang.String packageName, int returnCode) + throws android.os.RemoteException; +} \ No newline at end of file diff --git a/src/android/content/pm/IPackageInstallObserver.java b/src/android/content/pm/IPackageInstallObserver.java new file mode 100644 index 000000000..f81211404 --- /dev/null +++ b/src/android/content/pm/IPackageInstallObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 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 android.content.pm; + +/** + * Just a non-working implementation of this Stub to satisfy compiler! + */ +public interface IPackageInstallObserver extends android.os.IInterface { + + public abstract static class Stub extends android.os.Binder implements + android.content.pm.IPackageInstallObserver { + public Stub() { + throw new RuntimeException("Stub!"); + } + + public static android.content.pm.IPackageInstallObserver asInterface(android.os.IBinder obj) { + throw new RuntimeException("Stub!"); + } + + public android.os.IBinder asBinder() { + throw new RuntimeException("Stub!"); + } + + public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, + int flags) throws android.os.RemoteException { + throw new RuntimeException("Stub!"); + } + } + + public abstract void packageInstalled(java.lang.String packageName, int returnCode) + throws android.os.RemoteException; +} \ No newline at end of file diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 1e40c9ba1..96e82b6bf 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -21,28 +21,29 @@ package org.fdroid.fdroid; import android.content.*; import android.widget.*; + import org.fdroid.fdroid.data.*; +import org.fdroid.fdroid.installer.Installer; +import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException; +import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.xml.sax.XMLReader; -import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; import android.net.Uri; -import android.nfc.NfcAdapter; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.support.v4.app.NavUtils; import android.support.v4.view.MenuItemCompat; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageInfo; import android.content.pm.Signature; import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; import android.text.Editable; import android.text.Html; import android.text.Html.TagHandler; @@ -56,6 +57,7 @@ import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.graphics.Bitmap; import com.nostra13.universalimageloader.core.DisplayImageOptions; @@ -75,8 +77,6 @@ import java.util.List; public class AppDetails extends ListActivity { private static final String TAG = "AppDetails"; - private static final int REQUEST_INSTALL = 0; - private static final int REQUEST_UNINSTALL = 1; public static final int REQUEST_ENABLE_BLUETOOTH = 2; public static final String EXTRA_APPID = "appid"; @@ -95,6 +95,31 @@ public class AppDetails extends ListActivity { TextView added; TextView nativecode; } + + // observer to update view when package has been installed/deleted + AppObserver myAppObserver; + class AppObserver extends ContentObserver { + public AppObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + this.onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (!reset()) { + AppDetails.this.finish(); + return; + } + updateViews(); + + MenuManager.create(AppDetails.this).invalidateOptionsMenu(); + } + } + private class ApkListAdapter extends ArrayAdapter { @@ -255,10 +280,12 @@ public class AppDetails extends ListActivity { private final Context mctx = this; private DisplayImageOptions displayImageOptions; + private Installer installer; @Override protected void onCreate(Bundle savedInstanceState) { - + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + fdroidApp = ((FDroidApp) getApplication()); fdroidApp.applyTheme(this); @@ -308,6 +335,9 @@ public class AppDetails extends ListActivity { } mPm = getPackageManager(); + installer = Installer.getActivityInstaller(this, mPm, + myInstallerCallback); + // Get the preferences we're going to use in this Activity... AppDetails old = (AppDetails) getLastNonConfigurationInstance(); if (old != null) { @@ -317,7 +347,6 @@ public class AppDetails extends ListActivity { finish(); return; } - resetRequired = false; } SharedPreferences prefs = PreferenceManager @@ -341,7 +370,6 @@ public class AppDetails extends ListActivity { private boolean pref_expert; private boolean pref_permissions; private boolean pref_incompatibleVersions; - private boolean resetRequired; // The signature of the installed version. private Signature mInstalledSignature; @@ -349,13 +377,19 @@ public class AppDetails extends ListActivity { @Override protected void onResume() { + Log.d(TAG, "onresume"); super.onResume(); - if (resetRequired) { - if (!reset()) { - finish(); - return; - } - resetRequired = false; + + // register observer to know when install status changes + myAppObserver = new AppObserver(new Handler()); + getContentResolver().registerContentObserver( + AppProvider.getContentUri(app.id), + true, + myAppObserver); + + if (!reset()) { + finish(); + return; } updateViews(); @@ -368,6 +402,9 @@ public class AppDetails extends ListActivity { @Override protected void onPause() { + if (myAppObserver != null) { + getContentResolver().unregisterContentObserver(myAppObserver); + } if (downloadHandler != null) { downloadHandler.stopUpdates(); } @@ -924,46 +961,73 @@ public class AppDetails extends ListActivity { downloadHandler = new DownloadHandler(apk, repoaddress, Utils.getApkCacheDir(getBaseContext())); } + private void installApk(File file, String packageName) { + setProgressBarIndeterminateVisibility(true); - private void removeApk(String id) { - PackageInfo pkginfo; try { - pkginfo = mPm.getPackageInfo(id, 0); - } catch (NameNotFoundException e) { - Log.d("FDroid", "Couldn't find package " + id + " to uninstall."); - return; + installer.installPackage(file); + } catch (AndroidNotCompatibleException e) { + Log.e(TAG, "Android not compatible with this Installer!", e); } - Uri uri = Uri.fromParts("package", pkginfo.packageName, null); - Intent intent = new Intent(Intent.ACTION_DELETE, uri); - startActivityForResult(intent, REQUEST_UNINSTALL); - notifyAppChanged(id); - } - @TargetApi(14) - private void extraNotUnknownSource(Intent intent) { - if (Build.VERSION.SDK_INT < 14) { - return; + private void removeApk(String packageName) { + setProgressBarIndeterminateVisibility(true); + + try { + installer.deletePackage(packageName); + } catch (AndroidNotCompatibleException e) { + Log.e(TAG, "Android not compatible with this Installer!", e); } - intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); } - private void installApk(File file, String id) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse("file://" + file.getPath()), - "application/vnd.android.package-archive"); - extraNotUnknownSource(intent); - startActivityForResult(intent, REQUEST_INSTALL); - notifyAppChanged(id); - } + Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() { - /** - * We could probably drop this, and let the PackageReceiver take care of notifications - * for us, but I don't think the package receiver notifications are very instantaneous. - */ - private void notifyAppChanged(String id) { - getContentResolver().notifyChange(AppProvider.getContentUri(id), null); - } + @Override + public void onSuccess(final int operation) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (operation == Installer.InstallerCallback.OPERATION_INSTALL) { + if (downloadHandler != null) { + downloadHandler = null; + } + + PackageManagerCompat.setInstaller(mPm, app.id); + } + + setProgressBarIndeterminateVisibility(false); + } + }); + } + + @Override + public void onError(int operation, final int errorCode) { + if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) { + runOnUiThread(new Runnable() { + @Override + public void run() { + setProgressBarIndeterminateVisibility(false); + } + }); + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + setProgressBarIndeterminateVisibility(false); + + Log.e(TAG, "Installer aborted with errorCode: " + errorCode); + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this); + alertBuilder.setTitle(R.string.installer_error_title); + alertBuilder.setMessage(R.string.installer_error_title); + alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.create().show(); + } + }); + } + } + }; private void launchApk(String id) { Intent intent = mPm.getLaunchIntentForPackage(id); @@ -1111,20 +1175,15 @@ public class AppDetails extends ListActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // handle cases for install manager first + if (installer.handleOnActivityResult(requestCode, resultCode, data)) { + return; + } + switch (requestCode) { - case REQUEST_INSTALL: - if (downloadHandler != null) { - downloadHandler = null; - } - - PackageManagerCompat.setInstaller(mPm, app.id); - resetRequired = true; - break; - case REQUEST_UNINSTALL: - resetRequired = true; - break; case REQUEST_ENABLE_BLUETOOTH: fdroidApp.sendViaBluetooth(this, resultCode, app.id); + break; } } } diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java index 4b984deeb..44d1c113c 100644 --- a/src/org/fdroid/fdroid/Preferences.java +++ b/src/org/fdroid/fdroid/Preferences.java @@ -36,10 +36,14 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi public static final String PREF_CACHE_APK = "cacheDownloaded"; public static final String PREF_EXPERT = "expert"; public static final String PREF_UPD_LAST = "lastUpdateCheck"; + public static final String PREF_ROOT_INSTALLER = "rootInstaller"; + public static final String PREF_SYSTEM_INSTALLER = "systemInstaller"; private static final boolean DEFAULT_COMPACT_LAYOUT = false; private static final boolean DEFAULT_ROOTED = true; private static final int DEFAULT_UPD_HISTORY = 14; + private static final boolean DEFAULT_ROOT_INSTALLER = false; + private static final boolean DEFAULT_SYSTEM_INSTALLER = false; private boolean compactLayout = DEFAULT_COMPACT_LAYOUT; private boolean filterAppsRequiringRoot = DEFAULT_ROOTED; @@ -61,6 +65,14 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi private void uninitialize(String key) { initialized.put(key, false); } + + public boolean isRootInstallerEnabled() { + return preferences.getBoolean(PREF_ROOT_INSTALLER, DEFAULT_ROOT_INSTALLER); + } + + public boolean isSystemInstallerEnabled() { + return preferences.getBoolean(PREF_SYSTEM_INSTALLER, DEFAULT_SYSTEM_INSTALLER); + } public boolean hasCompactLayout() { if (!isInitialized(PREF_COMPACT_LAYOUT)) { @@ -97,7 +109,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi /** * This is cached as it is called several times inside the AppListAdapter. - * Providing it here means sthe shared preferences file only needs to be + * Providing it here means the shared preferences file only needs to be * read once, and we will keep our copy up to date by listening to changes * in PREF_ROOTED. */ diff --git a/src/org/fdroid/fdroid/PreferencesActivity.java b/src/org/fdroid/fdroid/PreferencesActivity.java index 924cc1f2b..55c36ec3e 100644 --- a/src/org/fdroid/fdroid/PreferencesActivity.java +++ b/src/org/fdroid/fdroid/PreferencesActivity.java @@ -20,22 +20,26 @@ package org.fdroid.fdroid; import android.os.Bundle; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.ListPreference; +import android.app.AlertDialog; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.view.MenuItem; - import android.support.v4.app.NavUtils; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.compat.ActionBarCompat; +import org.fdroid.fdroid.installer.CheckRootAsyncTask; +import org.fdroid.fdroid.installer.CheckRootAsyncTask.CheckRootCallback; +import org.fdroid.fdroid.installer.Installer; public class PreferencesActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { - + public static final int RESULT_RESTART = 4; private int result = 0; @@ -51,7 +55,9 @@ public class PreferencesActivity extends PreferenceActivity implements Preferences.PREF_COMPACT_LAYOUT, Preferences.PREF_IGN_TOUCH, Preferences.PREF_CACHE_APK, - Preferences.PREF_EXPERT + Preferences.PREF_EXPERT, + Preferences.PREF_ROOT_INSTALLER, + Preferences.PREF_SYSTEM_INSTALLER }; @Override @@ -148,24 +154,138 @@ public class PreferencesActivity extends PreferenceActivity implements onoffSummary(key, R.string.expert_on, R.string.expert_off); + } else if (key.equals(Preferences.PREF_ROOT_INSTALLER)) { + onoffSummary(key, R.string.root_installer_on, + R.string.root_installer_off); + + } else if (key.equals(Preferences.PREF_SYSTEM_INSTALLER)) { + onoffSummary(key, R.string.system_installer_on, + R.string.system_installer_off); + } } + + /** + * 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 OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + final CheckBoxPreference pref = (CheckBoxPreference) preference; + + if (pref.isChecked()) { + CheckRootAsyncTask checkTask = new CheckRootAsyncTask(PreferencesActivity.this, new 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(PreferencesActivity.this); + alertBuilder.setTitle(R.string.root_access_denied_title); + alertBuilder.setMessage(PreferencesActivity.this.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 + */ + protected void initSystemInstallerPreference() { + CheckBoxPreference pref = (CheckBoxPreference) findPreference(Preferences.PREF_SYSTEM_INSTALLER); + + // we are handling persistence ourself! + pref.setPersistent(false); + + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + final CheckBoxPreference pref = (CheckBoxPreference) preference; + + if (pref.isChecked()) { + if (Installer.hasSystemPermissions(PreferencesActivity.this, PreferencesActivity.this.getPackageManager())) { + // system-permission are granted, i.e. F-Droid is a system-app + SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); + editor.putBoolean(Preferences.PREF_SYSTEM_INSTALLER, true); + editor.commit(); + pref.setChecked(true); + } else { + // system-permission not available + SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); + editor.putBoolean(Preferences.PREF_SYSTEM_INSTALLER, false); + editor.commit(); + pref.setChecked(false); + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(PreferencesActivity.this); + alertBuilder.setTitle(R.string.system_permission_denied_title); + alertBuilder.setMessage(PreferencesActivity.this.getString(R.string.system_permission_denied_body)); + alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.create().show(); + } + } else { + SharedPreferences.Editor editor = pref.getSharedPreferences().edit(); + editor.putBoolean(Preferences.PREF_SYSTEM_INSTALLER, false); + editor.commit(); + pref.setChecked(false); + } + + return true; + } + }); + } @Override protected void onResume() { - super.onResume(); + getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); for (String key : summariesToUpdate) { updateSummary(key, false); } + + initRootInstallerPreference(); + initSystemInstallerPreference(); } @Override protected void onPause() { super.onPause(); + getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); } diff --git a/src/org/fdroid/fdroid/compat/PackageManagerCompat.java b/src/org/fdroid/fdroid/compat/PackageManagerCompat.java index 714c5b144..5aa752c1f 100644 --- a/src/org/fdroid/fdroid/compat/PackageManagerCompat.java +++ b/src/org/fdroid/fdroid/compat/PackageManagerCompat.java @@ -9,12 +9,12 @@ import android.util.Log; public class PackageManagerCompat extends Compatibility { @TargetApi(11) - public static void setInstaller(PackageManager mPm, String app_id) { + public static void setInstaller(PackageManager mPm, String packageName) { if (!hasApi(11)) return; try { - mPm.setInstallerPackageName(app_id, "org.fdroid.fdroid"); + mPm.setInstallerPackageName(packageName, "org.fdroid.fdroid"); Log.d("FDroid", "Installer package name for " + - app_id + " set successfully"); + packageName + " set successfully"); } catch (Exception e) { // Many problems can occur: // * App wasn't installed due to incompatibility @@ -22,8 +22,8 @@ public class PackageManagerCompat extends Compatibility { // * Another app interfered in the process // * Another app already set the target's installer package // * ... - Log.d("FDroid", "Could not set installer package name for " + - app_id + ": " + e.getMessage()); + Log.e("FDroid", "Could not set installer package name for " + + packageName, e); } } diff --git a/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java b/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java new file mode 100644 index 000000000..3146e636f --- /dev/null +++ b/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java @@ -0,0 +1,70 @@ +/* + * 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 org.fdroid.fdroid.R; + +import eu.chainfire.libsuperuser.Shell; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; + +public class CheckRootAsyncTask extends AsyncTask { + ProgressDialog mDialog; + Context mContext; + CheckRootCallback mCallback; + + public interface CheckRootCallback { + public void onRootCheck(boolean rootGranted); + } + + public CheckRootAsyncTask(Context context, CheckRootCallback callback) { + super(); + this.mContext = context; + this.mCallback = callback; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + mDialog = new ProgressDialog(mContext); + mDialog.setTitle(R.string.requesting_root_access_title); + 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/src/org/fdroid/fdroid/installer/DefaultInstaller.java b/src/org/fdroid/fdroid/installer/DefaultInstaller.java new file mode 100644 index 000000000..48619f877 --- /dev/null +++ b/src/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -0,0 +1,115 @@ +/* + * 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 java.io.File; +import java.util.List; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; + +/** + * For Android < 4: Default Installer using the public PackageManager API of + * Android to install/delete packages. This starts a Activity from the Android + * OS showing all permissions/changed permissions. The the user needs to + * manually press an install button, this Installer cannot be used for + * unattended installations. + */ +public class DefaultInstaller extends Installer { + private Activity mActivity; + + public DefaultInstaller(Activity activity, PackageManager pm, InstallerCallback callback) + throws AndroidNotCompatibleException { + super(activity, pm, callback); + this.mActivity = activity; + } + + private static final int REQUEST_CODE_INSTALL = 0; + private static final int REQUEST_CODE_DELETE = 1; + + @Override + protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(apkFile), + "application/vnd.android.package-archive"); + try { + mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL); + } catch (ActivityNotFoundException e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + protected void installPackageInternal(List apkFiles) throws AndroidNotCompatibleException { + // TODO Auto-generated method stub + + } + + @Override + protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException { + PackageInfo pkgInfo = null; + try { + pkgInfo = mPm.getPackageInfo(packageName, 0); + } catch (NameNotFoundException e) { + // already checked in super class + } + + Uri uri = Uri.fromParts("package", pkgInfo.packageName, null); + Intent intent = new Intent(Intent.ACTION_DELETE, uri); + try { + mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE); + } catch (ActivityNotFoundException e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { + /** + * resultCode is always 0 on Android < 4.0. See + * com.android.packageinstaller.PackageInstallerActivity: setResult is + * never executed on Androids before 4.0 + */ + switch (requestCode) { + case REQUEST_CODE_INSTALL: + mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); + + return true; + case REQUEST_CODE_DELETE: + mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); + + return true; + default: + return false; + } + } + + @Override + public boolean supportsUnattendedOperations() { + return false; + } + +} diff --git a/src/org/fdroid/fdroid/installer/DefaultInstallerSdk14.java b/src/org/fdroid/fdroid/installer/DefaultInstallerSdk14.java new file mode 100644 index 000000000..b45241b80 --- /dev/null +++ b/src/org/fdroid/fdroid/installer/DefaultInstallerSdk14.java @@ -0,0 +1,141 @@ +/* + * 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 java.io.File; +import java.util.List; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Build; + +/** + * For Android >= 4.0: Default Installer using the public PackageManager API of + * Android to install/delete packages. This starts a Activity from the Android + * OS showing all permissions/changed permissions. The the user needs to + * manually press an install button, this Installer cannot be used for + * unattended installations. + */ +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class DefaultInstallerSdk14 extends Installer { + private Activity mActivity; + + public DefaultInstallerSdk14(Activity activity, PackageManager pm, InstallerCallback callback) + throws AndroidNotCompatibleException { + super(activity, pm, callback); + this.mActivity = activity; + } + + private static final int REQUEST_CODE_INSTALL = 0; + private static final int REQUEST_CODE_DELETE = 1; + + @SuppressWarnings("deprecation") + @Override + protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_INSTALL_PACKAGE); + intent.setData(Uri.fromFile(apkFile)); + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + + // following extras only work when being installed as system-app + // https://code.google.com/p/android/issues/detail?id=42253 + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { + // deprecated in Android 4.1 + intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true); + } + try { + mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL); + } catch (ActivityNotFoundException e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + protected void installPackageInternal(List apkFiles) throws AndroidNotCompatibleException { + // TODO Auto-generated method stub + + } + + @Override + protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException { + PackageInfo pkgInfo = null; + try { + pkgInfo = mPm.getPackageInfo(packageName, 0); + } catch (NameNotFoundException e) { + // already checked in super class + } + + Uri uri = Uri.fromParts("package", pkgInfo.packageName, null); + Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri); + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + try { + mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE); + } catch (ActivityNotFoundException e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_INSTALL: + if (resultCode == Activity.RESULT_OK) { + mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); + } else if (resultCode == Activity.RESULT_CANCELED) { + mCallback.onError(InstallerCallback.OPERATION_INSTALL, + InstallerCallback.ERROR_CODE_CANCELED); + } else { + mCallback.onError(InstallerCallback.OPERATION_INSTALL, + InstallerCallback.ERROR_CODE_OTHER); + } + + return true; + case REQUEST_CODE_DELETE: + if (resultCode == Activity.RESULT_OK) { + mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); + } else if (resultCode == Activity.RESULT_CANCELED) { + mCallback.onError(InstallerCallback.OPERATION_DELETE, + InstallerCallback.ERROR_CODE_CANCELED); + } else { + // UninstallAppProgress actually returns + // Activity.RESULT_FIRST_USER if something breaks + mCallback.onError(InstallerCallback.OPERATION_DELETE, + InstallerCallback.ERROR_CODE_OTHER); + } + + return true; + default: + return false; + } + } + + @Override + public boolean supportsUnattendedOperations() { + return false; + } + +} diff --git a/src/org/fdroid/fdroid/installer/Installer.java b/src/org/fdroid/fdroid/installer/Installer.java new file mode 100644 index 000000000..dfcae1b82 --- /dev/null +++ b/src/org/fdroid/fdroid/installer/Installer.java @@ -0,0 +1,246 @@ +/* + * 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 java.io.File; +import java.util.List; + +import org.fdroid.fdroid.Preferences; + +import android.Manifest.permission; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +/** + * Abstract Installer class. Also provides static methods to automatically + * instantiate a working Installer based on F-Droids granted permissions. + */ +abstract public class Installer { + protected Context mContext; + protected PackageManager mPm; + protected InstallerCallback mCallback; + + public static final String TAG = "FDroid"; + + /** + * This is thrown when an Installer is not compatible with the Android OS it + * is running on. This could be due to a broken superuser in case of + * RootInstaller or due to an incompatible Android version in case of + * SystemPermissionInstaller + */ + public static class AndroidNotCompatibleException extends Exception { + + private static final long serialVersionUID = -8343133906463328027L; + + public AndroidNotCompatibleException() { + } + + public AndroidNotCompatibleException(String message) { + super(message); + } + + public AndroidNotCompatibleException(Throwable cause) { + super(cause); + } + + public AndroidNotCompatibleException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Callback from Installer. NOTE: This callback can be in a different thread + * than the UI thread + */ + public interface InstallerCallback { + + public static final int OPERATION_INSTALL = 1; + public static final int OPERATION_DELETE = 2; + + public static final int ERROR_CODE_CANCELED = 1; + public static final int ERROR_CODE_OTHER = 2; + + public void onSuccess(int operation); + + public void onError(int operation, int errorCode); + } + + public Installer(Context context, PackageManager pm, InstallerCallback callback) + throws AndroidNotCompatibleException { + this.mContext = context; + this.mPm = pm; + this.mCallback = callback; + } + + /** + * 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) { + if (hasSystemPermissions(activity, pm)) { + Log.d(TAG, "system permissions -> SystemInstaller"); + + try { + return new SystemInstaller(activity, pm, callback); + } catch (AndroidNotCompatibleException e) { + Log.e(TAG, "Android not compatible with SystemInstaller!", e); + } + } + } else { + Log.e(TAG, "SystemInstaller is enabled in prefs, but system-perms are not granted!"); + } + + // Fallback -> DefaultInstaller + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // Default installer on Android >= 4.0 + try { + Log.d(TAG, "try default installer for Android >= 4"); + + return new DefaultInstallerSdk14(activity, pm, callback); + } catch (AndroidNotCompatibleException e) { + Log.e(TAG, "Android not compatible with DefaultInstallerSdk14!", e); + } + } else { + // Default installer on Android < 4.0 + try { + Log.d(TAG, "try default installer for Android < 4"); + + return new DefaultInstaller(activity, pm, callback); + } catch (AndroidNotCompatibleException e) { + Log.e(TAG, "Android not compatible with DefaultInstaller!", e); + } + } + + // this should not happen! + return null; + } + + 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); + } else { + // nope! + throw new AndroidNotCompatibleException(); + } + } + + 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); + + if (permissionsGranted) { + return true; + } else { + return false; + } + } + + public void installPackage(File apkFile) throws AndroidNotCompatibleException { + // check if file exists... + if (!apkFile.exists()) { + Log.e(TAG, "Couldn't find file " + apkFile + " to install."); + return; + } + + installPackageInternal(apkFile); + } + + public void installPackage(List apkFiles) throws AndroidNotCompatibleException { + // check if files exist... + for (File apkFile : apkFiles) { + if (!apkFile.exists()) { + Log.e(TAG, "Couldn't find file " + apkFile + " to install."); + return; + } + } + + installPackageInternal(apkFiles); + } + + public void deletePackage(String packageName) throws AndroidNotCompatibleException { + // check if package exists before proceeding... + try { + mPm.getPackageInfo(packageName, 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Couldn't find package " + packageName + " to delete."); + return; + } + + deletePackageInternal(packageName); + } + + protected abstract void installPackageInternal(File apkFile) + throws AndroidNotCompatibleException; + + protected abstract void installPackageInternal(List apkFiles) + throws AndroidNotCompatibleException; + + protected abstract void deletePackageInternal(String packageName) + throws AndroidNotCompatibleException; + + public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data); + + public abstract boolean supportsUnattendedOperations(); +} diff --git a/src/org/fdroid/fdroid/installer/RootInstaller.java b/src/org/fdroid/fdroid/installer/RootInstaller.java new file mode 100644 index 000000000..ca6fb683b --- /dev/null +++ b/src/org/fdroid/fdroid/installer/RootInstaller.java @@ -0,0 +1,223 @@ +/* + * 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 java.io.File; +import java.util.ArrayList; +import java.util.List; + +import eu.chainfire.libsuperuser.Shell; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.util.Log; + +/** + * Installer using a root shell and "pm install", "pm uninstall" commands + */ +public class RootInstaller extends Installer { + + Shell.Interactive rootSession; + + public RootInstaller(Context context, PackageManager pm, InstallerCallback callback) + throws AndroidNotCompatibleException { + super(context, pm, callback); + } + + private Shell.Builder createShellBuilder() { + Shell.Builder shellBuilder = new Shell.Builder() + .useSU() + .setWantSTDERR(true) + .setWatchdogTimeout(5) + .setMinimalLogging(true); + + return shellBuilder; + } + + @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) { + rootSession.addCommand("pm install -r " + 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) { + ArrayList commands = new ArrayList(); + String pm = "pm install -r "; + for (File apkFile : apkFiles) { + 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) { + 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; + } + + /** + * 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/src/org/fdroid/fdroid/installer/SystemInstaller.java b/src/org/fdroid/fdroid/installer/SystemInstaller.java new file mode 100644 index 000000000..c3af08249 --- /dev/null +++ b/src/org/fdroid/fdroid/installer/SystemInstaller.java @@ -0,0 +1,473 @@ +/* + * 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 java.io.File; +import java.lang.reflect.Method; +import java.util.List; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageInstallObserver; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; + +/** + * Installer based on using internal hidden APIs of the Android OS, which are + * protected by the permissions + *

    + *
  • android.permission.INSTALL_PACKAGES
  • + *
  • android.permission.DELETE_PACKAGES
  • + *
+ *

+ * Both permissions are protected by systemOrSignature (in newer versions: + * system|signature) and only granted on F-Droid's install in the following + * cases: + *

    + *
  • On all Android versions if F-Droid is pre-deployed as a + * system-application with the Rom
  • + *
  • On Android < 4.4 also when moved into /system/app/
  • + *
  • On Android >= 4.4 also when moved into /system/priv-app/
  • + *
+ *

+ * Sources for Android 4.4 change: + * https://groups.google.com/forum/#!msg/android- + * security-discuss/r7uL_OEMU5c/LijNHvxeV80J + * https://android.googlesource.com/platform + * /frameworks/base/+/ccbf84f44c9e6a5ed3c08673614826bb237afc54 + */ +public class SystemInstaller extends Installer { + + private PackageInstallObserver mInstallObserver; + private PackageDeleteObserver mDeleteObserver; + private Method mInstallMethod; + private Method mDeleteMethod; + + public SystemInstaller(Context context, PackageManager pm, + InstallerCallback callback) throws AndroidNotCompatibleException { + super(context, pm, callback); + + // create internal callbacks + mInstallObserver = new PackageInstallObserver(); + mDeleteObserver = new PackageDeleteObserver(); + + try { + Class[] installTypes = new Class[] { + Uri.class, IPackageInstallObserver.class, int.class, + String.class + }; + Class[] deleteTypes = new Class[] { + String.class, IPackageDeleteObserver.class, + int.class + }; + + mInstallMethod = mPm.getClass().getMethod("installPackage", installTypes); + mDeleteMethod = mPm.getClass().getMethod("deletePackage", deleteTypes); + } catch (NoSuchMethodException e) { + throw new AndroidNotCompatibleException(e); + } + } + + /** + * Internal install callback from the system + */ + class PackageInstallObserver extends IPackageInstallObserver.Stub { + public void packageInstalled(String packageName, int returnCode) throws RemoteException { + // TODO: propagate other return codes? + if (returnCode == INSTALL_SUCCEEDED) { + Log.d(TAG, "Install succeeded"); + + mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); + } else { + Log.e(TAG, "Install failed with returnCode " + returnCode); + mCallback.onError(InstallerCallback.OPERATION_INSTALL, + InstallerCallback.ERROR_CODE_OTHER); + } + } + } + + /** + * Internal delete callback from the system + */ + class PackageDeleteObserver extends IPackageDeleteObserver.Stub { + public void packageDeleted(String packageName, int returnCode) throws RemoteException { + // TODO: propagate other return codes? + if (returnCode == DELETE_SUCCEEDED) { + Log.d(TAG, "Delete succeeded"); + + mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); + } else { + Log.e(TAG, "Delete failed with returnCode " + returnCode); + mCallback.onError(InstallerCallback.OPERATION_DELETE, + InstallerCallback.ERROR_CODE_OTHER); + } + } + } + + @Override + protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException { + Uri packageURI = Uri.fromFile(apkFile); + try { + mInstallMethod.invoke(mPm, new Object[] { + packageURI, mInstallObserver, INSTALL_REPLACE_EXISTING, null + }); + } catch (Exception e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + protected void installPackageInternal(List apkFiles) throws AndroidNotCompatibleException { + // TODO Auto-generated method stub + + } + + @Override + protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException { + try { + mDeleteMethod.invoke(mPm, new Object[] { + packageName, mDeleteObserver, 0 + }); + } catch (Exception e) { + throw new AndroidNotCompatibleException(e); + } + } + + @Override + public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { + // no need to handle onActivityResult + return false; + } + + @Override + public boolean supportsUnattendedOperations() { + return false; + } + + public final int INSTALL_REPLACE_EXISTING = 2; + + /** + * Following return codes are copied from Android 4.3 source code + */ + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on + * success. + */ + public static final int INSTALL_SUCCEEDED = 1; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package is already installed. + */ + public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package archive file is invalid. + */ + public static final int INSTALL_FAILED_INVALID_APK = -2; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the URI passed in is invalid. + */ + public static final int INSTALL_FAILED_INVALID_URI = -3; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package manager service found that the device didn't have enough + * storage space to install the app. + */ + public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * a package is already installed with the same name. + */ + public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the requested shared user does not exist. + */ + public static final int INSTALL_FAILED_NO_SHARED_USER = -6; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * a previously installed package of the same name has a different signature + * than the new package (and the old package's data was not removed). + */ + public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package is requested a shared user which is already installed on + * the device and does not have matching signature. + */ + public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a shared library that is not available. + */ + public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a shared library that is not available. + */ + public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed while optimizing and validating its dex files, + * either because there was not enough storage or the validation failed. + */ + public static final int INSTALL_FAILED_DEXOPT = -11; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed because the current SDK version is older than that + * required by the package. + */ + public static final int INSTALL_FAILED_OLDER_SDK = -12; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed because it contains a content provider with the + * same authority as a provider already installed in the system. + */ + public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed because the current SDK version is newer than that + * required by the package. + */ + public static final int INSTALL_FAILED_NEWER_SDK = -14; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed because it has specified that it is a test-only + * package and the caller has not supplied the {@link #INSTALL_ALLOW_TEST} + * flag. + */ + public static final int INSTALL_FAILED_TEST_ONLY = -15; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package being installed contains native code, but none that is + * compatible with the the device's CPU_ABI. + */ + public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a feature that is not available. + */ + public static final int INSTALL_FAILED_MISSING_FEATURE = -17; + + // ------ Errors related to sdcard + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * a secure container mount point couldn't be accessed on external media. + */ + public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package couldn't be installed in the specified install location. + */ + public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; + + /** + * Installation return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package couldn't be installed in the specified install location + * because the media is not available. + */ + public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser was given a path that is not a file, or does not end with the + * expected '.apk' extension. + */ + public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser was unable to retrieve the AndroidManifest.xml file. + */ + public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser encountered an unexpected exception. + */ + public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser did not find any certificates in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser found inconsistent certificates on the files in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser encountered a CertificateEncodingException in one of the files + * in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser encountered a bad or missing package name in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser encountered a bad shared user id name in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser encountered some structural problem in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; + + /** + * Installation parse return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the parser did not find any actionable tags (instrumentation or + * application) in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; + + /** + * Installation failed return code: this is passed to the + * {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the system failed to install the package because of system issues. + */ + public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; + + /** + * Return code for when package deletion succeeds. This is passed to the + * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system + * succeeded in deleting the package. + */ + public static final int DELETE_SUCCEEDED = 1; + + /** + * Deletion failed return code: this is passed to the + * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system + * failed to delete the package for an unspecified reason. + */ + public static final int DELETE_FAILED_INTERNAL_ERROR = -1; + + /** + * Deletion failed return code: this is passed to the + * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system + * failed to delete the package because it is the active DevicePolicy + * manager. + */ + public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; + + /** + * Deletion failed return code: this is passed to the + * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system + * failed to delete the package since the user is restricted. + */ + public static final int DELETE_FAILED_USER_RESTRICTED = -3; + +} diff --git a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java index d42cbd6c2..aeb59e683 100644 --- a/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/CanUpdateAppsFragment.java @@ -1,13 +1,39 @@ + package org.fdroid.fdroid.views.fragments; +import android.content.Context; import android.net.Uri; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.views.AppListAdapter; import org.fdroid.fdroid.views.CanUpdateAppListAdapter; public class CanUpdateAppsFragment extends AppListFragment { + // copied from ListFragment + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + // added for update button + static final int UPDATE_ALL_BUTTON_ID = 0x00ff0004; + + private Button mUpdateAllButton; + private Installer mInstaller; + @Override protected AppListAdapter getAppListAdapter() { return new CanUpdateAppListAdapter(getActivity(), null); @@ -23,4 +49,115 @@ public class CanUpdateAppsFragment extends AppListFragment { return AppProvider.getCanUpdateUri(); } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mInstaller = Installer.getActivityInstaller(getActivity(), getActivity() + .getPackageManager(), null); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mUpdateAllButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + // TODO + + } + }); + } + + // TODO: not really called again after coming back from preference + @Override + public void onResume() { + super.onResume(); + + if (mInstaller.supportsUnattendedOperations()) { +// mUpdateAllButton.setVisibility(View.VISIBLE); + mUpdateAllButton.setVisibility(View.GONE); + } else { + mUpdateAllButton.setVisibility(View.GONE); + } + } + + /** + * Copied from ListFragment and added Button on top of list. We do not use a + * custom layout here, because this breaks the progress bar functionality of + * ListFragment. + * + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Context context = getActivity(); + + FrameLayout root = new FrameLayout(context); + + // ------------------------------------------------------------------ + + LinearLayout pframe = new LinearLayout(context); + pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); + pframe.setOrientation(LinearLayout.VERTICAL); + pframe.setVisibility(View.GONE); + pframe.setGravity(Gravity.CENTER); + + ProgressBar progress = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + pframe.addView(progress, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(pframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + FrameLayout lframe = new FrameLayout(context); + lframe.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView tv = new TextView(getActivity()); + tv.setId(INTERNAL_EMPTY_ID); + tv.setGravity(Gravity.CENTER); + lframe.addView(tv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // Added update all button + LinearLayout linearLayout = new LinearLayout(context); + linearLayout.setOrientation(LinearLayout.VERTICAL); + + mUpdateAllButton = new Button(context); + mUpdateAllButton.setId(UPDATE_ALL_BUTTON_ID); + mUpdateAllButton.setText(R.string.update_all); + mUpdateAllButton.setCompoundDrawablesWithIntrinsicBounds( + getResources().getDrawable(R.drawable.ic_menu_refresh), null, null, null); + + linearLayout.addView(mUpdateAllButton, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + ListView lv = new ListView(getActivity()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + linearLayout.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + lframe.addView(linearLayout, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + return root; + } + }