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;
+ }
+
}