diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index b769ecbfd..eba0b224b 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -55,9 +55,6 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/app_permission_item.xml b/F-Droid/res/layout/app_permission_item.xml
new file mode 100644
index 000000000..b5aab370d
--- /dev/null
+++ b/F-Droid/res/layout/app_permission_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/app_permission_item_money.xml b/F-Droid/res/layout/app_permission_item_money.xml
new file mode 100644
index 000000000..6b29bcd77
--- /dev/null
+++ b/F-Droid/res/layout/app_permission_item_money.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/app_permission_item_old.xml b/F-Droid/res/layout/app_permission_item_old.xml
new file mode 100644
index 000000000..a5b395ede
--- /dev/null
+++ b/F-Droid/res/layout/app_permission_item_old.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/app_perms_summary.xml b/F-Droid/res/layout/app_perms_summary.xml
new file mode 100644
index 000000000..065dab090
--- /dev/null
+++ b/F-Droid/res/layout/app_perms_summary.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/install_app_details.xml b/F-Droid/res/layout/install_app_details.xml
new file mode 100644
index 000000000..bed2ecfc2
--- /dev/null
+++ b/F-Droid/res/layout/install_app_details.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/install_confirm.xml b/F-Droid/res/layout/install_confirm.xml
new file mode 100644
index 000000000..911e8d944
--- /dev/null
+++ b/F-Droid/res/layout/install_confirm.xml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/install_start.xml b/F-Droid/res/layout/install_start.xml
new file mode 100644
index 000000000..0fb199f8a
--- /dev/null
+++ b/F-Droid/res/layout/install_start.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/permissions_list.xml b/F-Droid/res/layout/permissions_list.xml
new file mode 100644
index 000000000..e660b1608
--- /dev/null
+++ b/F-Droid/res/layout/permissions_list.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/values/colors.xml b/F-Droid/res/values/colors.xml
index 5157887e3..919c8c261 100644
--- a/F-Droid/res/values/colors.xml
+++ b/F-Droid/res/values/colors.xml
@@ -15,4 +15,9 @@
#fbb040#00a14b
-
\ No newline at end of file
+ #cc222222
+ #00000000
+
+ #fff4511e
+
+
diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index f511b8ed1..8667a83a1 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -25,10 +25,10 @@
Notify when updates are availableUpdate historyDays to consider apps new or recent: %s
- Install using root access
- Request root access to install, update, and remove packages
- Install using system-permissions
- Use system permissions to install, update, and remove packages
+ Enable privileged F-Droid
+ Use privileged permissions to install, update, and remove packages
+ Uninstall privileged F-Droid
+ Uninstall F-Droid when installed as a privileged appBroadcast Local RepoAdvertise your local repo using Bonjour (mDNS)Name of your Local Repo
@@ -290,15 +290,30 @@
No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).
- Root accessRequesting root access…Root access deniedEither 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!
+ The (de-)installation failed. If you are using F-Droid as a privileged app, try disabling this setting!System permissions denied
- This option is only available when F-Droid is installed as a system-app.
+ This option is only available when F-Droid is installed as a privileged app.
+ Install as a privileged app
+ Install privileged F-Droid?
+ Touch for more information.
+ Touch to install F-Droid as a privileged app, tightly coupled with the Android operating system. This enables extended features, such as automatic app updates.\nYou can also do this later from F-Droid\'s preferences.
+ Successful installation as a privileged app
+ Installation as a privileged app failed
+ F-Droid has been successfully installed as a privileged app. This enables extended features, such as automatic app updates.
+ The installation of F-Droid as a privileged app failed. The installation method is not supported by all Android distributions, please consult the F-Droid bug tracker for more information.
+ installing…
+ uninstalling…
+ Do you want to install F-Droid as a privileged app?\nThis takes up to 10 seconds where <b>no user interface</b> is shown.
+ Do you want to install F-Droid as a privileged app?\nThis takes up to 10 seconds where <b>no user interface</b> is shown and the device will be <b>rebooted</b> afterwards.
+ Looks like you have root access on your device. You can now install F-Droid as a privileged app, tightly coupled with the Android operating system. This enables extended features, such as automatic app updates.
+ Do you want to uninstall F-Droid?
+ This will uninstall F-Droid completely.
+ UninstallF-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device.If your friend has F-Droid and NFC turned on touch your phones together.
@@ -332,4 +347,31 @@
May workPromisingBest bet
+
+ Do you want to install this application?
+ It will get access to:
+ Do you want to install this application?
+ It does not require any special access.
+ Do you want to install an update
+ to this existing application? Your existing data will not
+ be lost. The updated application will get access to:
+ Do you want to install an update
+ to this built-in application? Your existing data will not
+ be lost. The updated application will get access to:
+ Do you want to install an update
+ to this existing application? Your existing data will not
+ be lost. It does not require any special access.
+ Do you want to install an update
+ to this built-in application? Your existing data will not
+ be lost. It does not require any special access.
+ New
+ All
+ Privacy
+ Device Access
+ this may cost you money
+ Do you want to replace this app with the factory version?
+ Do you want to uninstall this app?
+
+ NEW:
+ Provided by %1$s.
diff --git a/F-Droid/res/xml/preferences.xml b/F-Droid/res/xml/preferences.xml
index e502f448a..0de38fb22 100644
--- a/F-Droid/res/xml/preferences.xml
+++ b/F-Droid/res/xml/preferences.xml
@@ -81,13 +81,13 @@
-
+
diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java
index a532dbbc2..e811e00ef 100644
--- a/F-Droid/src/org/fdroid/fdroid/FDroid.java
+++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java
@@ -47,6 +47,8 @@ import android.widget.Toast;
import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.NewRepoConfig;
+import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity;
+import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.swap.ConnectSwapActivity;
@@ -101,6 +103,8 @@ public class FDroid extends ActionBarActivity {
Uri uri = AppProvider.getContentUri();
getContentResolver().registerContentObserver(uri, true, new AppObserver());
+
+ InstallIntoSystemDialogActivity.firstTime(this);
}
@Override
diff --git a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
index 677bfe175..6ea8a224e 100644
--- a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
+++ b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
@@ -98,23 +98,26 @@ public class FDroidApp extends Application {
}
public void applyTheme(Activity activity) {
- switch (curTheme) {
- case dark:
- activity.setTheme(R.style.AppThemeDark);
- break;
- case light:
- activity.setTheme(R.style.AppThemeLight);
- break;
- case lightWithDarkActionBar:
- activity.setTheme(R.style.AppThemeLightWithDarkActionBar);
- break;
- }
+ activity.setTheme(getCurThemeResId());
}
public static Theme getCurTheme() {
return curTheme;
}
+ public static int getCurThemeResId() {
+ switch (curTheme) {
+ case dark:
+ return R.style.AppThemeDark;
+ case light:
+ return R.style.AppThemeLight;
+ case lightWithDarkActionBar:
+ return R.style.AppThemeLightWithDarkActionBar;
+ default:
+ return R.style.AppThemeDark;
+ }
+ }
+
public static void enableSpongyCastle() {
Security.addProvider(spongyCastleProvider);
}
diff --git a/F-Droid/src/org/fdroid/fdroid/Preferences.java b/F-Droid/src/org/fdroid/fdroid/Preferences.java
index ce8d6c55e..01b8bfb9a 100644
--- a/F-Droid/src/org/fdroid/fdroid/Preferences.java
+++ b/F-Droid/src/org/fdroid/fdroid/Preferences.java
@@ -49,8 +49,8 @@ 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";
+ public static final String PREF_UNINSTALL_SYSTEM_APP = "uninstallSystemApp";
public static final String PREF_LOCAL_REPO_BONJOUR = "localRepoBonjour";
public static final String PREF_LOCAL_REPO_NAME = "localRepoName";
public static final String PREF_LOCAL_REPO_HTTPS = "localRepoHttps";
@@ -59,11 +59,12 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String PREF_PROXY_HOST = "proxyHost";
public static final String PREF_PROXY_PORT = "proxyPort";
public static final String PREF_SHOW_NFC_DURING_SWAP = "showNfcDuringSwap";
+ public static final String PREF_FIRST_TIME = "firstTime";
+ public static final String PREF_POST_SYSTEM_INSTALL = "postSystemInstall";
private static final boolean DEFAULT_COMPACT_LAYOUT = false;
private static final boolean DEFAULT_ROOTED = true;
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 static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true;
private static final boolean DEFAULT_CACHE_APK = false;
@@ -76,6 +77,8 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String DEFAULT_PROXY_HOST = "127.0.0.1";
public static final int DEFAULT_PROXY_PORT = 8118;
public static final boolean DEFAULT_SHOW_NFC_DURING_SWAP = true;
+ private static final boolean DEFAULT_FIRST_TIME = true;
+ private static final boolean DEFAULT_POST_SYSTEM_INSTALL = false;
private boolean compactLayout = DEFAULT_COMPACT_LAYOUT;
private boolean filterAppsRequiringRoot = DEFAULT_ROOTED;
@@ -101,14 +104,30 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
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 void setSystemInstallerEnabled(boolean enable) {
+ preferences.edit().putBoolean(PREF_SYSTEM_INSTALLER, enable).commit();
+ }
+
+ public boolean isFirstTime() {
+ return preferences.getBoolean(PREF_FIRST_TIME, DEFAULT_FIRST_TIME);
+ }
+
+ public void setFirstTime(boolean firstTime) {
+ preferences.edit().putBoolean(PREF_FIRST_TIME, firstTime).commit();
+ }
+
+ public boolean isPostSystemInstall() {
+ return preferences.getBoolean(PREF_POST_SYSTEM_INSTALL, DEFAULT_POST_SYSTEM_INSTALL);
+ }
+
+ public void setPostSystemInstall(boolean postInstall) {
+ preferences.edit().putBoolean(PREF_POST_SYSTEM_INSTALL, postInstall).commit();
+ }
+
public boolean isLocalRepoBonjourEnabled() {
return preferences.getBoolean(PREF_LOCAL_REPO_BONJOUR, DEFAULT_LOCAL_REPO_BONJOUR);
}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/AppDiff.java b/F-Droid/src/org/fdroid/fdroid/installer/AppDiff.java
new file mode 100644
index 000000000..e9d9ede17
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/AppDiff.java
@@ -0,0 +1,69 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+** Copyright 2015 Dominik Schürmann
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package org.fdroid.fdroid.installer;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+
+public class AppDiff {
+
+ PackageManager mPm;
+ PackageInfo mPkgInfo;
+
+ ApplicationInfo mInstalledAppInfo = null;
+
+ public AppDiff(PackageManager mPm, Uri mPackageURI) {
+ this.mPm = mPm;
+
+ final String pkgPath = mPackageURI.getPath();
+
+ mPkgInfo = mPm.getPackageArchiveInfo(pkgPath, PackageManager.GET_PERMISSIONS);
+ mPkgInfo.applicationInfo.sourceDir = pkgPath;
+ mPkgInfo.applicationInfo.publicSourceDir = pkgPath;
+
+ init();
+ }
+
+ private void init() {
+ String pkgName = mPkgInfo.packageName;
+ // Check if there is already a package on the device with this name
+ // but it has been renamed to something else.
+ final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
+ if (oldName != null && oldName.length > 0 && oldName[0] != null) {
+ pkgName = oldName[0];
+ mPkgInfo.packageName = pkgName;
+ mPkgInfo.applicationInfo.packageName = pkgName;
+ }
+ // Check if package is already installed
+ try {
+ // This is a little convoluted because we want to get all uninstalled
+ // apps, but this may include apps with just data, and if it is just
+ // data we still want to count it as "installed".
+ mInstalledAppInfo = mPm.getApplicationInfo(pkgName,
+ PackageManager.GET_UNINSTALLED_PACKAGES);
+ if ((mInstalledAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
+ mInstalledAppInfo = null;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ mInstalledAppInfo = null;
+ }
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/AppSecurityPermissions.java b/F-Droid/src/org/fdroid/fdroid/installer/AppSecurityPermissions.java
new file mode 100644
index 000000000..18e9e3f27
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/AppSecurityPermissions.java
@@ -0,0 +1,525 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+** Copyright 2015 Daniel Martí
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package org.fdroid.fdroid.installer;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.fdroid.fdroid.R;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class contains the SecurityPermissions view implementation.
+ * Initially the package's advanced or dangerous security permissions
+ * are displayed under categorized
+ * groups. Clicking on the additional permissions presents
+ * extended information consisting of all groups and permissions.
+ * To use this view define a LinearLayout or any ViewGroup and add this
+ * view by instantiating AppSecurityPermissions and invoking getPermissionsView.
+ */
+public class AppSecurityPermissions {
+
+ public static final int WHICH_PERSONAL = 1<<0;
+ public static final int WHICH_DEVICE = 1<<1;
+ public static final int WHICH_NEW = 1<<2;
+
+ private final static String TAG = "AppSecurityPermissions";
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+ private final PackageManager mPm;
+ private final Map mPermGroups = new HashMap<>();
+ private final List mPermGroupsList = new ArrayList<>();
+ private final PermissionGroupInfoComparator mPermGroupComparator = new PermissionGroupInfoComparator();
+ private final PermissionInfoComparator mPermComparator = new PermissionInfoComparator();
+ private final CharSequence mNewPermPrefix;
+ private String mPackageName;
+
+ static class MyPermissionGroupInfo extends PermissionGroupInfo {
+ CharSequence mLabel;
+
+ final List mNewPermissions = new ArrayList<>();
+ final List mPersonalPermissions = new ArrayList<>();
+ final List mDevicePermissions = new ArrayList<>();
+ final List mAllPermissions = new ArrayList<>();
+
+ MyPermissionGroupInfo(PermissionInfo perm) {
+ name = perm.packageName;
+ packageName = perm.packageName;
+ }
+
+ MyPermissionGroupInfo(PermissionGroupInfo info) {
+ super(info);
+ }
+
+ public Drawable loadGroupIcon(PackageManager pm) {
+ if (icon != 0) {
+ //return loadUnbadgedIcon(pm);
+ return loadIcon(pm);
+ } else {
+ ApplicationInfo appInfo;
+ try {
+ appInfo = pm.getApplicationInfo(packageName, 0);
+ //return appInfo.loadUnbadgedIcon(pm);
+ return appInfo.loadIcon(pm);
+ } catch (NameNotFoundException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+ }
+
+ private static class MyPermissionInfo extends PermissionInfo {
+ CharSequence mLabel;
+
+ /**
+ * PackageInfo.requestedPermissionsFlags for the new package being installed.
+ */
+ int mNewReqFlags;
+
+ /**
+ * PackageInfo.requestedPermissionsFlags for the currently installed
+ * package, if it is installed.
+ */
+ int mExistingReqFlags;
+
+ /**
+ * True if this should be considered a new permission.
+ */
+ boolean mNew;
+
+ MyPermissionInfo(PermissionInfo info) {
+ super(info);
+ }
+ }
+
+ public static class PermissionItemView extends LinearLayout implements View.OnClickListener {
+ MyPermissionGroupInfo mGroup;
+ MyPermissionInfo mPerm;
+ AlertDialog mDialog;
+
+ public PermissionItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setClickable(true);
+ }
+
+ public void setPermission(MyPermissionGroupInfo grp, MyPermissionInfo perm,
+ boolean first, CharSequence newPermPrefix, String packageName) {
+ mGroup = grp;
+ mPerm = perm;
+
+ ImageView permGrpIcon = (ImageView) findViewById(R.id.perm_icon);
+ TextView permNameView = (TextView) findViewById(R.id.perm_name);
+
+ PackageManager pm = getContext().getPackageManager();
+ Drawable icon = null;
+ if (first) {
+ icon = grp.loadGroupIcon(pm);
+ }
+ CharSequence label = perm.mLabel;
+ if (perm.mNew && newPermPrefix != null) {
+ // If this is a new permission, format it appropriately.
+ SpannableStringBuilder builder = new SpannableStringBuilder();
+ Parcel parcel = Parcel.obtain();
+ TextUtils.writeToParcel(newPermPrefix, parcel, 0);
+ parcel.setDataPosition(0);
+ CharSequence newStr = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ builder.append(newStr);
+ builder.append(label);
+ label = builder;
+ }
+
+ permGrpIcon.setImageDrawable(icon);
+ permNameView.setText(label);
+ setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mGroup != null && mPerm != null) {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ PackageManager pm = getContext().getPackageManager();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(mGroup.mLabel);
+ if (mPerm.descriptionRes != 0) {
+ builder.setMessage(mPerm.loadDescription(pm));
+ } else {
+ CharSequence appName;
+ try {
+ ApplicationInfo app = pm.getApplicationInfo(mPerm.packageName, 0);
+ appName = app.loadLabel(pm);
+ } catch (NameNotFoundException e) {
+ appName = mPerm.packageName;
+ }
+ builder.setMessage(getContext().getString(
+ R.string.perms_description_app, appName) + "\n\n" + mPerm.name);
+ }
+ builder.setCancelable(true);
+ builder.setIcon(mGroup.loadGroupIcon(pm));
+ //addRevokeUIIfNecessary(builder);
+ mDialog = builder.show();
+ mDialog.setCanceledOnTouchOutside(true);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ }
+
+ }
+
+ private AppSecurityPermissions(Context context) {
+ mContext = context;
+ mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPm = mContext.getPackageManager();
+ // Pick up from framework resources instead.
+ mNewPermPrefix = mContext.getText(R.string.perms_new_perm_prefix);
+ }
+
+ public AppSecurityPermissions(Context context, PackageInfo info) {
+ this(context);
+ if (info == null) {
+ return;
+ }
+ mPackageName = info.packageName;
+
+ final Set permSet = new HashSet<>();
+ PackageInfo installedPkgInfo = null;
+ if (info.requestedPermissions != null) {
+ try {
+ installedPkgInfo = mPm.getPackageInfo(info.packageName,
+ PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException e) {
+ // ignore
+ }
+ extractPerms(info, permSet, installedPkgInfo);
+ }
+ setPermissions(new ArrayList<>(permSet));
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private int[] getRequestedPermissionFlags(PackageInfo info) {
+ if (Build.VERSION.SDK_INT < 16) {
+ return new int[info.requestedPermissions.length];
+ }
+ return info.requestedPermissionsFlags;
+ }
+
+ private void extractPerms(PackageInfo info, Set permSet,
+ PackageInfo installedPkgInfo) {
+
+ final String[] strList = info.requestedPermissions;
+ if (strList == null || strList.length == 0) {
+ return;
+ }
+ final int[] flagsList = getRequestedPermissionFlags(info);
+
+ for (int i=0; i= 0) {
+ final int[] instFlagsList = getRequestedPermissionFlags(installedPkgInfo);
+ existingFlags = instFlagsList[existingIndex];
+ }
+ if (!isDisplayablePermission(tmpPermInfo, flagsList[i], existingFlags)) {
+ // This is not a permission that is interesting for the user
+ // to see, so skip it.
+ continue;
+ }
+ final String origGroupName = tmpPermInfo.group;
+ String groupName = origGroupName;
+ if (groupName == null) {
+ groupName = tmpPermInfo.packageName;
+ tmpPermInfo.group = groupName;
+ }
+ MyPermissionGroupInfo group = mPermGroups.get(groupName);
+ if (group == null) {
+ PermissionGroupInfo grp = null;
+ if (origGroupName != null) {
+ grp = mPm.getPermissionGroupInfo(origGroupName, 0);
+ }
+ if (grp != null) {
+ group = new MyPermissionGroupInfo(grp);
+ } else {
+ // We could be here either because the permission
+ // didn't originally specify a group or the group it
+ // gave couldn't be found. In either case, we consider
+ // its group to be the permission's package name.
+ tmpPermInfo.group = tmpPermInfo.packageName;
+ group = mPermGroups.get(tmpPermInfo.group);
+ if (group == null) {
+ group = new MyPermissionGroupInfo(tmpPermInfo);
+ }
+ group = new MyPermissionGroupInfo(tmpPermInfo);
+ }
+ mPermGroups.put(tmpPermInfo.group, group);
+ }
+ final boolean newPerm = installedPkgInfo != null
+ && (existingFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0;
+ MyPermissionInfo myPerm = new MyPermissionInfo(tmpPermInfo);
+ myPerm.mNewReqFlags = flagsList[i];
+ myPerm.mExistingReqFlags = existingFlags;
+ // This is a new permission if the app is already installed and
+ // doesn't currently hold this permission.
+ myPerm.mNew = newPerm;
+ permSet.add(myPerm);
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "Ignoring unknown permission:"+permName);
+ }
+ }
+ }
+
+ private List getPermissionList(MyPermissionGroupInfo grp, int which) {
+ switch (which) {
+ case WHICH_NEW:
+ return grp.mNewPermissions;
+ case WHICH_PERSONAL:
+ return grp.mPersonalPermissions;
+ case WHICH_DEVICE:
+ return grp.mDevicePermissions;
+ default:
+ return grp.mAllPermissions;
+ }
+ }
+
+ public int getPermissionCount(int which) {
+ int N = 0;
+ for (int i=0; i groups,
+ LinearLayout permListView, int which) {
+ permListView.removeAllViews();
+
+ int spacing = (int) (8*mContext.getResources().getDisplayMetrics().density);
+
+ for (int i=0; i perms = getPermissionList(grp, which);
+ for (int j=0; j {
+ private final Collator sCollator = Collator.getInstance();
+ PermissionGroupInfoComparator() {
+ }
+ public final int compare(MyPermissionGroupInfo a, MyPermissionGroupInfo b) {
+ if (((a.flags^b.flags)&PermissionGroupInfo.FLAG_PERSONAL_INFO) != 0) {
+ return ((a.flags&PermissionGroupInfo.FLAG_PERSONAL_INFO) != 0) ? -1 : 1;
+ }
+ if (a.priority != b.priority) {
+ return a.priority > b.priority ? -1 : 1;
+ }
+ return sCollator.compare(a.mLabel, b.mLabel);
+ }
+ }
+
+ private static class PermissionInfoComparator implements Comparator {
+ private final Collator sCollator = Collator.getInstance();
+ PermissionInfoComparator() {
+ }
+ public final int compare(MyPermissionInfo a, MyPermissionInfo b) {
+ return sCollator.compare(a.mLabel, b.mLabel);
+ }
+ }
+
+ private void addPermToList(List permList,
+ MyPermissionInfo pInfo) {
+ if (pInfo.mLabel == null) {
+ pInfo.mLabel = pInfo.loadLabel(mPm);
+ }
+ int idx = Collections.binarySearch(permList, pInfo, mPermComparator);
+ if (idx < 0) {
+ idx = -idx-1;
+ permList.add(idx, pInfo);
+ }
+ }
+
+ private void setPermissions(List permList) {
+ if (permList != null) {
+ // First pass to group permissions
+ for (MyPermissionInfo pInfo : permList) {
+ if (!isDisplayablePermission(pInfo, pInfo.mNewReqFlags, pInfo.mExistingReqFlags)) {
+ continue;
+ }
+ MyPermissionGroupInfo group = mPermGroups.get(pInfo.group);
+ if (group != null) {
+ pInfo.mLabel = pInfo.loadLabel(mPm);
+ addPermToList(group.mAllPermissions, pInfo);
+ if (pInfo.mNew) {
+ addPermToList(group.mNewPermissions, pInfo);
+ }
+ if ((group.flags&PermissionGroupInfo.FLAG_PERSONAL_INFO) != 0) {
+ addPermToList(group.mPersonalPermissions, pInfo);
+ } else {
+ addPermToList(group.mDevicePermissions, pInfo);
+ }
+ }
+ }
+ }
+
+ for (MyPermissionGroupInfo pgrp : mPermGroups.values()) {
+ if (pgrp.labelRes != 0 || pgrp.nonLocalizedLabel != null) {
+ pgrp.mLabel = pgrp.loadLabel(mPm);
+ } else {
+ try {
+ ApplicationInfo app = mPm.getApplicationInfo(pgrp.packageName, 0);
+ pgrp.mLabel = app.loadLabel(mPm);
+ } catch (NameNotFoundException e) {
+ pgrp.mLabel = pgrp.loadLabel(mPm);
+ }
+ }
+ mPermGroupsList.add(pgrp);
+ }
+ Collections.sort(mPermGroupsList, mPermGroupComparator);
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/CaffeinatedScrollView.java b/F-Droid/src/org/fdroid/fdroid/installer/CaffeinatedScrollView.java
new file mode 100644
index 000000000..37b96b419
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/CaffeinatedScrollView.java
@@ -0,0 +1,75 @@
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package org.fdroid.fdroid.installer;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+/**
+ * It's a ScrollView that knows how to stay awake.
+ */
+public class CaffeinatedScrollView extends ScrollView {
+ private Runnable mFullScrollAction;
+ private int mBottomSlop;
+
+ public CaffeinatedScrollView(Context context) {
+ super(context);
+ }
+
+ public CaffeinatedScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Make this visible so we can call it
+ */
+ @Override
+ public boolean awakenScrollBars() {
+ return super.awakenScrollBars();
+ }
+
+ public void setFullScrollAction(Runnable action) {
+ mFullScrollAction = action;
+ mBottomSlop = (int) (4 * getResources().getDisplayMetrics().density);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ checkFullScrollAction();
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ checkFullScrollAction();
+ }
+
+ private void checkFullScrollAction() {
+ if (mFullScrollAction != null) {
+ int daBottom = getChildAt(0).getBottom();
+ int screenBottom = getScrollY() + getHeight() - getPaddingBottom();
+ if ((daBottom - screenBottom) < mBottomSlop) {
+ mFullScrollAction.run();
+ mFullScrollAction = null;
+ }
+ }
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java b/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java
deleted file mode 100644
index 6fe5e2134..000000000
--- a/F-Droid/src/org/fdroid/fdroid/installer/CheckRootAsyncTask.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2014 Dominik Schürmann
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.installer;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.AsyncTask;
-
-import org.fdroid.fdroid.R;
-
-import eu.chainfire.libsuperuser.Shell;
-
-public class CheckRootAsyncTask extends AsyncTask {
- ProgressDialog mDialog;
- final Context mContext;
- final CheckRootCallback mCallback;
-
- public interface CheckRootCallback {
- void onRootCheck(boolean rootGranted);
- }
-
- public CheckRootAsyncTask(Context context, CheckRootCallback callback) {
- super();
- this.mContext = context;
- this.mCallback = callback;
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
-
- 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/F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java
new file mode 100644
index 000000000..298913284
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java
@@ -0,0 +1,218 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+** Copyright 2015 Daniel Martí
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package org.fdroid.fdroid.installer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TabHost;
+import android.widget.TextView;
+
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+
+public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener {
+
+ private Intent intent;
+
+ PackageManager mPm;
+
+ AppDiff mAppDiff;
+
+ // View for install progress
+ View mInstallConfirm;
+ // Buttons to indicate user acceptance
+ private Button mOk;
+ private Button mCancel;
+ CaffeinatedScrollView mScrollView = null;
+ private boolean mOkCanInstall = false;
+
+ private static final String TAB_ID_ALL = "all";
+ private static final String TAB_ID_NEW = "new";
+
+ private void startInstallConfirm() {
+
+ final Drawable appIcon = mAppDiff.mPkgInfo.applicationInfo.loadIcon(mPm);
+ final String appLabel = (String) mAppDiff.mPkgInfo.applicationInfo.loadLabel(mPm);
+
+ View appSnippet = findViewById(R.id.app_snippet);
+ ((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon);
+ ((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel);
+
+ TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost);
+ tabHost.setup();
+ ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
+ TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
+ adapter.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
+ @Override
+ public void onTabChanged(String tabId) {
+ }
+ });
+
+ boolean permVisible = false;
+ mScrollView = null;
+ mOkCanInstall = false;
+ int msg = 0;
+ if (mAppDiff.mPkgInfo != null) {
+ AppSecurityPermissions perms = new AppSecurityPermissions(this, mAppDiff.mPkgInfo);
+ final int NP = perms.getPermissionCount(AppSecurityPermissions.WHICH_PERSONAL);
+ final int ND = perms.getPermissionCount(AppSecurityPermissions.WHICH_DEVICE);
+ if (mAppDiff.mInstalledAppInfo != null) {
+ msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ ? R.string.install_confirm_update_system
+ : R.string.install_confirm_update;
+ mScrollView = new CaffeinatedScrollView(this);
+ mScrollView.setFillViewport(true);
+ final boolean newPermissionsFound =
+ (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
+ if (newPermissionsFound) {
+ permVisible = true;
+ mScrollView.addView(perms.getPermissionsView(
+ AppSecurityPermissions.WHICH_NEW));
+ } else {
+ throw new RuntimeException("This should not happen. No new permissions were found but InstallConfirmActivity has been started!");
+ }
+ adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(
+ getText(R.string.newPerms)), mScrollView);
+ } else {
+ findViewById(R.id.tabscontainer).setVisibility(View.GONE);
+ findViewById(R.id.divider).setVisibility(View.VISIBLE);
+ }
+ if (NP > 0 || ND > 0) {
+ permVisible = true;
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.permissions_list, null);
+ if (mScrollView == null) {
+ mScrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
+ }
+ final ViewGroup privacyList = (ViewGroup) root.findViewById(R.id.privacylist);
+ if (NP > 0) {
+ privacyList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_PERSONAL));
+ } else {
+ privacyList.setVisibility(View.GONE);
+ }
+ final ViewGroup deviceList = (ViewGroup) root.findViewById(R.id.devicelist);
+ if (ND > 0) {
+ deviceList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_DEVICE));
+ } else {
+ root.findViewById(R.id.devicelist).setVisibility(View.GONE);
+ }
+ adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator(
+ getText(R.string.allPerms)), root);
+ }
+ }
+ if (!permVisible) {
+ if (mAppDiff.mInstalledAppInfo != null) {
+ // This is an update to an application, but there are no
+ // permissions at all.
+ msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ ? R.string.install_confirm_update_system_no_perms
+ : R.string.install_confirm_update_no_perms;
+ } else {
+ // This is a new application with no permissions.
+ msg = R.string.install_confirm_no_perms;
+ }
+ tabHost.setVisibility(View.GONE);
+ findViewById(R.id.filler).setVisibility(View.VISIBLE);
+ findViewById(R.id.divider).setVisibility(View.GONE);
+ mScrollView = null;
+ }
+ if (msg != 0) {
+ ((TextView) findViewById(R.id.install_confirm)).setText(msg);
+ }
+ mInstallConfirm.setVisibility(View.VISIBLE);
+ mOk = (Button) findViewById(R.id.ok_button);
+ mCancel = (Button) findViewById(R.id.cancel_button);
+ mOk.setOnClickListener(this);
+ mCancel.setOnClickListener(this);
+ if (mScrollView == null) {
+ // There is nothing to scroll view, so the ok button is immediately
+ // set to install.
+ mOk.setText(R.string.menu_install);
+ mOkCanInstall = true;
+ } else {
+ mScrollView.setFullScrollAction(new Runnable() {
+ @Override
+ public void run() {
+ mOk.setText(R.string.menu_install);
+ mOkCanInstall = true;
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ ((FDroidApp) getApplication()).applyTheme(this);
+
+ mPm = getPackageManager();
+
+ intent = getIntent();
+ Uri packageURI = intent.getData();
+
+ mAppDiff = new AppDiff(mPm, packageURI);
+
+ setContentView(R.layout.install_start);
+ mInstallConfirm = findViewById(R.id.install_confirm_panel);
+ mInstallConfirm.setVisibility(View.INVISIBLE);
+
+ startInstallConfirm();
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ }
+
+ // Generic handling when pressing back key
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+
+ public void onClick(View v) {
+ if (v == mOk) {
+ if (mOkCanInstall || mScrollView == null) {
+ setResult(RESULT_OK, intent);
+ finish();
+ } else {
+ mScrollView.pageScroll(View.FOCUS_DOWN);
+ }
+ } else if (v == mCancel) {
+ setResult(RESULT_CANCELED, intent);
+ finish();
+ }
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java
new file mode 100644
index 000000000..ded31e1ee
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystem.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.chainfire.libsuperuser.Shell;
+
+/**
+ * Partly based on
+ * http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/
+ * https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java
+ */
+@TargetApi(Build.VERSION_CODES.FROYO)
+abstract class InstallIntoSystem {
+
+ protected final Context context;
+
+ public InstallIntoSystem(final Context context) {
+ this.context = context;
+ }
+
+ public static InstallIntoSystem create(final Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return new LollipopImpl(context);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return new KitKatToLollipopImpl(context);
+ } else {
+ return new PreKitKatImpl(context);
+ }
+ }
+
+ protected abstract String getSystemFolder();
+
+ protected void onPreInstall() {
+ // To be overridden by relevant base class[es]
+ }
+
+ public String getWarningInfo() {
+ return context.getString(R.string.system_install_question);
+ }
+
+ final void runUninstall() {
+ final String[] commands = {
+ "am force-stop org.fdroid.fdroid",
+ "pm clear org.fdroid.fdroid",
+ "mount -o rw,remount /system",
+ "pm uninstall " + context.getPackageName(),
+ "rm -f " + getInstallPath(),
+ "sleep 5",
+ "mount -o ro,remount /system"
+ };
+ Shell.SU.run(commands);
+ }
+
+ final void runInstall() {
+ onPreInstall();
+ Shell.SU.run(getInstallCommands());
+ }
+
+ protected String getInstallPath() {
+ return getSystemFolder() + "FDroid.apk";
+ }
+
+ private List getInstallCommands() {
+ final List commands = new ArrayList<>();
+ commands.add("mount -o rw,remount /system");
+ commands.addAll(getCopyToSystemCommands());
+ commands.add("pm uninstall -k " + context.getPackageName()); // -k to retain data
+ commands.add("mv " + getInstallPath() + ".tmp " + getInstallPath());
+ commands.add("pm install -r " + getInstallPath());
+ commands.add("sleep 5"); // wait until the app is really installed
+ commands.add("mount -o ro,remount /system");
+ commands.add("am force-stop org.fdroid.fdroid");
+ commands.addAll(getPostInstallCommands());
+ return commands;
+ }
+
+ protected List getCopyToSystemCommands() {
+ final List commands = new ArrayList<>(2);
+ commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp");
+ commands.add("chmod 655 " + getInstallPath() + ".tmp");
+ return commands;
+ }
+
+ protected List getPostInstallCommands() {
+ final List commands = new ArrayList<>(1);
+ commands.add("am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true");
+ return commands;
+ }
+
+ private static class PreKitKatImpl extends InstallIntoSystem {
+
+ public PreKitKatImpl(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected String getSystemFolder() {
+ return "/system/app";
+ }
+
+ }
+
+ private static class KitKatToLollipopImpl extends InstallIntoSystem {
+
+ public KitKatToLollipopImpl(Context context) {
+ super(context);
+ }
+
+ /**
+ * On KitKat, "Some system apps are more system than others"
+ * https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54
+ */
+ @Override
+ protected String getSystemFolder() {
+ return "/system/priv-app/";
+ }
+
+ }
+
+ /**
+ * History of PackageManagerService in Lollipop:
+ * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java
+ */
+ private static class LollipopImpl extends InstallIntoSystem {
+
+ public LollipopImpl(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onPreInstall() {
+ // Setup preference to execute postInstall after reboot
+ Preferences.get().setPostSystemInstall(true);
+ }
+
+ public String getWarningInfo() {
+ return context.getString(R.string.system_install_question_lollipop);
+ }
+
+ /**
+ * Cluster-style layout where each app is placed in a unique directory
+ */
+ @Override
+ protected String getSystemFolder() {
+ return "/system/priv-app/FDroid/";
+ }
+
+ /**
+ * Create app directory
+ */
+ @Override
+ protected List getCopyToSystemCommands() {
+ List commands = new ArrayList<>(3);
+ commands.add("mkdir " + getSystemFolder()); // create app directory if not existing
+ commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp");
+ commands.add("chmod 655 " + getInstallPath() + ".tmp");
+ return commands;
+ }
+
+ /**
+ * TODO: Currently only works with reboot
+ *
+ * File observers on /system/priv-app/ have been removed because they don't work with the new
+ * cluser-style layout. See
+ * https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103
+ *
+ * Related stack overflow post: http://stackoverflow.com/q/26487750
+ */
+ @Override
+ protected List getPostInstallCommands() {
+ List commands = new ArrayList<>(3);
+ commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN");
+ commands.add("sleep 1");
+ commands.add("reboot");
+ return commands;
+ }
+
+ }
+
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java
new file mode 100644
index 000000000..42f7e57cc
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemBootReceiver.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.fdroid.fdroid.Preferences;
+
+public class InstallIntoSystemBootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+ if (Preferences.get().isPostSystemInstall()) {
+ Preferences.get().setPostSystemInstall(false);
+
+ Intent postInstall = new Intent(context.getApplicationContext(), InstallIntoSystemDialogActivity.class);
+ postInstall.setAction(InstallIntoSystemDialogActivity.ACTION_POST_INSTALL);
+ postInstall.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(postInstall);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java
new file mode 100644
index 000000000..9eb1ee740
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallIntoSystemDialogActivity.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.NotificationCompat;
+import android.text.Html;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+
+import org.fdroid.fdroid.FDroid;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.R;
+
+import eu.chainfire.libsuperuser.Shell;
+
+/**
+ * Note: This activity has no view on its own, it displays consecutive dialogs.
+ */
+public class InstallIntoSystemDialogActivity extends FragmentActivity {
+
+ private static final String TAG = "InstallIntoSystem";
+
+ public static final String ACTION_INSTALL = "install";
+ public static final String ACTION_UNINSTALL = "uninstall";
+ public static final String ACTION_POST_INSTALL = "post_install";
+ public static final String ACTION_FIRST_TIME = "first_time";
+
+ String action;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // this activity itself has no content view (see manifest)
+
+ if (getIntent().getAction() == null) {
+ Log.e(TAG, "Please define an action!");
+ finish();
+ return;
+ }
+
+ action = getIntent().getAction();
+ if (ACTION_UNINSTALL.equals(action)) {
+ uninstall();
+ } else if (ACTION_INSTALL.equals(action)) {
+ checkRootTask.execute();
+ } else if (ACTION_FIRST_TIME.equals(action)) {
+ checkRootTask.execute();
+ } else if (ACTION_POST_INSTALL.equals(action)) {
+ postInstall();
+ }
+ }
+
+ public static void firstTime(final Context context) {
+ if (Preferences.get().isFirstTime()) {
+ Preferences.get().setFirstTime(false);
+
+ if (Installer.hasSystemPermissions(context, context.getPackageManager())) {
+ Preferences.get().setSystemInstallerEnabled(true);
+ } else {
+ runFirstTime(context);
+ }
+ }
+ }
+
+ public static void runFirstTime(final Context context) {
+ // don't do a "real" root access, just look up if "su" is present and has a version!
+ // a real check would annoy the user
+ AsyncTask checkRoot = new AsyncTask() {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return (Shell.SU.version(true) != null);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean probablyRoot) {
+ super.onPostExecute(probablyRoot);
+
+ if (probablyRoot) {
+ // looks like we have root, at least su has a version number and is present
+
+ Intent installIntent = new Intent(context, InstallIntoSystemDialogActivity.class);
+ installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_FIRST_TIME);
+ installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ PendingIntent resultPendingIntent =
+ PendingIntent.getActivity(
+ context,
+ 0,
+ installIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(context)
+ .setContentIntent(resultPendingIntent)
+ .setSmallIcon(R.drawable.ic_stat_notify)
+ .setContentTitle(context.getString(R.string.system_install_first_time_notification))
+ .setContentText(context.getString(R.string.system_install_first_time_notification_message_short))
+ .setDefaults(Notification.DEFAULT_ALL)
+ .setAutoCancel(true)
+ /*
+ * Sets the big view "big text" style and supplies the
+ * text (the user's reminder message) that will be displayed
+ * in the detail area of the expanded notification.
+ * These calls are ignored by the support library for
+ * pre-4.1 devices.
+ */
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(context.getString(R.string.system_install_first_time_notification_message)));
+
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(42, builder.build());
+ }
+ }
+ };
+ checkRoot.execute();
+ }
+
+ /**
+ * first time
+ */
+ private void firstTime() {
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
+
+ String message = getString(R.string.system_install_first_time_message) + "
" + InstallIntoSystem.create(getApplicationContext()).getWarningInfo();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(theme)
+ .setMessage(Html.fromHtml(message))
+ .setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ installTask.execute();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
+ InstallIntoSystemDialogActivity.this.finish();
+ }
+ });
+ builder.create().show();
+ }
+
+ /**
+ * 1. Check for root access
+ */
+ public AsyncTask checkRootTask = new AsyncTask() {
+ ProgressDialog mProgressDialog;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
+ FDroidApp.getCurThemeResId());
+
+ mProgressDialog = new ProgressDialog(theme);
+ mProgressDialog.setMessage(getString(R.string.requesting_root_access_body));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return Shell.SU.available();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean rootGranted) {
+ super.onPostExecute(rootGranted);
+
+ mProgressDialog.dismiss();
+
+ if (rootGranted) {
+ // root access granted
+
+ if (ACTION_UNINSTALL.equals(action)) {
+ uninstallTask.execute();
+ } else if (ACTION_INSTALL.equals(action)) {
+ installTask.execute();
+ } else if (ACTION_FIRST_TIME.equals(action)) {
+ firstTime();
+ }
+ } else {
+ // root access denied
+
+ if (!ACTION_FIRST_TIME.equals(action)) {
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
+ FDroidApp.getCurThemeResId());
+
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(theme)
+ .setTitle(R.string.root_access_denied_title)
+ .setMessage(getString(R.string.root_access_denied_body))
+ .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
+ InstallIntoSystemDialogActivity.this.finish();
+ }
+ });
+ alertBuilder.create().show();
+ }
+ }
+ }
+ };
+
+ /**
+ * 2. Install into system
+ */
+ AsyncTask installTask = new AsyncTask() {
+ ProgressDialog mProgressDialog;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
+ FDroidApp.getCurThemeResId());
+
+ mProgressDialog = new ProgressDialog(theme);
+ mProgressDialog.setMessage(getString(R.string.system_install_installing));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ InstallIntoSystem.create(getApplicationContext()).runInstall();
+ return null;
+ }
+ };
+
+ /**
+ * 3. Verify that install worked
+ */
+ private void postInstall() {
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
+
+ final boolean success = Installer.hasSystemPermissions(this, this.getPackageManager());
+
+ // enable system installer on installation success
+ Preferences.get().setSystemInstallerEnabled(success);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(theme)
+ .setTitle(success ? R.string.system_install_post_success : R.string.system_install_post_fail)
+ .setMessage(success ? R.string.system_install_post_success_message : R.string.system_install_post_fail_message)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ InstallIntoSystemDialogActivity.this.setResult(success ? Activity.RESULT_OK : Activity.RESULT_CANCELED);
+ InstallIntoSystemDialogActivity.this.finish();
+ startActivity(new Intent(InstallIntoSystemDialogActivity.this, FDroid.class));
+ }
+ })
+ .setCancelable(false);
+ builder.create().show();
+ }
+
+ private void uninstall() {
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
+
+ final boolean systemApp = Installer.hasSystemPermissions(this, this.getPackageManager());
+
+ if (systemApp) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(theme)
+ .setTitle(R.string.system_uninstall)
+ .setMessage(R.string.system_uninstall_message)
+ .setPositiveButton(R.string.system_uninstall_button, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ checkRootTask.execute();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
+ InstallIntoSystemDialogActivity.this.finish();
+ }
+ });
+ builder.create().show();
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(theme)
+ .setTitle(R.string.system_permission_denied_title)
+ .setMessage(getString(R.string.system_permission_denied_body))
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
+ InstallIntoSystemDialogActivity.this.finish();
+ }
+ });
+ builder.create().show();
+ }
+ }
+
+ AsyncTask uninstallTask = new AsyncTask() {
+ ProgressDialog mProgressDialog;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
+ ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
+ FDroidApp.getCurThemeResId());
+
+ mProgressDialog = new ProgressDialog(theme);
+ mProgressDialog.setMessage(getString(R.string.system_install_uninstalling));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ InstallIntoSystem.create(getApplicationContext()).runUninstall();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void unused) {
+ super.onPostExecute(unused);
+
+ mProgressDialog.dismiss();
+
+ // app is uninstalled but still display, kill it!
+ System.exit(0);
+ }
+ };
+
+}
+
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
index 1079f8afa..f20c07279 100644
--- a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
+++ b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
@@ -95,28 +95,10 @@ abstract public class Installer {
/**
* Creates a new Installer for installing/deleting processes starting from
* an Activity
- *
- * @param activity
- * @param pm
- * @param callback
- * @return
- * @throws AndroidNotCompatibleException
*/
public static Installer getActivityInstaller(Activity activity, PackageManager pm,
InstallerCallback callback) {
- // if root installer has been activated in preferences -> RootInstaller
- boolean isRootInstallerEnabled = Preferences.get().isRootInstallerEnabled();
- if (isRootInstallerEnabled) {
- Log.d(TAG, "root installer preference enabled -> RootInstaller");
-
- try {
- return new RootInstaller(activity, pm, callback);
- } catch (AndroidNotCompatibleException e) {
- Log.e(TAG, "Android not compatible with RootInstaller!", e);
- }
- }
-
// system permissions and pref enabled -> SystemInstaller
boolean isSystemInstallerEnabled = Preferences.get().isSystemInstallerEnabled();
if (isSystemInstallerEnabled) {
@@ -133,7 +115,7 @@ abstract public class Installer {
}
}
- // Fallback -> DefaultInstaller
+ // else -> DefaultInstaller
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// Default installer on Android >= 4.0
try {
@@ -158,38 +140,15 @@ abstract public class Installer {
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);
+ boolean hasInstallPermission =
+ (pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName())
+ == PackageManager.PERMISSION_GRANTED);
+ boolean hasDeletePermission =
+ (pm.checkPermission(permission.DELETE_PACKAGES, context.getPackageName())
+ == PackageManager.PERMISSION_GRANTED);
- return permissionsGranted;
+ return (hasInstallPermission && hasDeletePermission);
}
public void installPackage(File apkFile) throws AndroidNotCompatibleException {
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java
deleted file mode 100644
index 6cec7f80f..000000000
--- a/F-Droid/src/org/fdroid/fdroid/installer/RootInstaller.java
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Copyright (C) 2014 Dominik Schürmann
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid.installer;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.util.Log;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import eu.chainfire.libsuperuser.Shell;
-
-/**
- * Installer using a root shell and "pm install", "pm uninstall" commands
- */
-public class RootInstaller extends Installer {
-
- private static final String TAG = "RootInstaller";
-
- Shell.Interactive rootSession;
-
- public RootInstaller(Context context, PackageManager pm, InstallerCallback callback)
- throws AndroidNotCompatibleException {
- super(context, pm, callback);
- }
-
- private Shell.Builder createShellBuilder() {
- return new Shell.Builder()
- .useSU()
- .setWantSTDERR(true)
- .setWatchdogTimeout(30)
- .setMinimalLogging(false);
- }
-
- @Override
- protected void installPackageInternal(final File apkFile) throws AndroidNotCompatibleException {
- rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
-
- // Callback to report whether the shell was successfully
- // started up
- @Override
- public void onCommandResult(int commandCode, int exitCode, List output) {
- if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
- // NOTE: Additional exit codes:
- // Shell.OnCommandResultListener.SHELL_WRONG_UID
- // Shell.OnCommandResultListener.SHELL_EXEC_FAILED
-
- Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
- mCallback.onError(InstallerCallback.OPERATION_INSTALL,
- InstallerCallback.ERROR_CODE_OTHER);
- } else {
- addInstallCommand(apkFile);
- }
- }
- });
- }
-
- @Override
- protected void installPackageInternal(final List apkFiles)
- throws AndroidNotCompatibleException {
- rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
-
- // Callback to report whether the shell was successfully
- // started up
- @Override
- public void onCommandResult(int commandCode, int exitCode, List output) {
- if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
- // NOTE: Additional exit codes:
- // Shell.OnCommandResultListener.SHELL_WRONG_UID
- // Shell.OnCommandResultListener.SHELL_EXEC_FAILED
-
- Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
- mCallback.onError(InstallerCallback.OPERATION_INSTALL,
- InstallerCallback.ERROR_CODE_OTHER);
- } else {
- addInstallCommand(apkFiles);
- }
- }
- });
- }
-
- @Override
- protected void deletePackageInternal(final String packageName)
- throws AndroidNotCompatibleException {
- rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
-
- // Callback to report whether the shell was successfully
- // started up
- @Override
- public void onCommandResult(int commandCode, int exitCode, List output) {
- if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
- // NOTE: Additional exit codes:
- // Shell.OnCommandResultListener.SHELL_WRONG_UID
- // Shell.OnCommandResultListener.SHELL_EXEC_FAILED
-
- Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- } else {
- addDeleteCommand(packageName);
- }
- }
- });
-
- }
-
- @Override
- public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
- // no need to handle onActivityResult
- return false;
- }
-
- private void addInstallCommand(File apkFile) {
- // Like package names, apk files should also only contain letters, numbers, dots, or underscore,
- // e.g., org.fdroid.fdroid_9.apk
- if (!isValidPackageName(apkFile.getName())) {
- Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): "
- + apkFile.getName());
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- return;
- }
-
- rootSession.addCommand("pm install -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) {
- List commands = new ArrayList<>();
- String pm = "pm install -r ";
- for (File apkFile : apkFiles) {
- // see addInstallCommand()
- if (!isValidPackageName(apkFile.getName())) {
- Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): "
- + apkFile.getName());
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- return;
- }
- commands.add(pm + "\"" + apkFile.getAbsolutePath() + "\"");
- }
-
- rootSession.addCommand(commands, 0,
- new Shell.OnCommandResultListener() {
- public void onCommandResult(int commandCode, int exitCode,
- List output) {
- // close su shell
- rootSession.close();
-
- if (exitCode < 0) {
- Log.e(TAG, "Install failed with exit code " + exitCode);
- mCallback.onError(InstallerCallback.OPERATION_INSTALL,
- InstallerCallback.ERROR_CODE_OTHER);
- } else {
- mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
- }
- }
- });
-
- }
-
- private void addDeleteCommand(String packageName) {
- if (!isValidPackageName(packageName)) {
- Log.e(TAG, "Package name is not valid (contains characters other than letters, numbers, dots, or underscore): "
- + packageName);
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- return;
- }
-
- rootSession.addCommand("pm uninstall \"" + packageName + "\"", 0,
- new Shell.OnCommandResultListener() {
- public void onCommandResult(int commandCode, int exitCode, List output) {
- // close su shell
- rootSession.close();
-
- if (exitCode < 0) {
- Log.e(TAG, "Delete failed with exit code " + exitCode);
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- } else {
- mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
- }
- }
- });
- }
-
- @Override
- public boolean supportsUnattendedOperations() {
- return true;
- }
-
- private static final Pattern PACKAGE_NAME_BLACKLIST = Pattern.compile("[^a-zA-Z0-9\\.\\_]");
-
- /**
- * Package names should only contain letters, numbers, dots, and underscores!
- * Prevent injection attacks with app names like ";touch $'\057data\057injected'"
- *
- * @param packageName
- * @return
- */
- private boolean isValidPackageName(String packageName) {
- Matcher matcher = PACKAGE_NAME_BLACKLIST.matcher(packageName);
- return !matcher.find();
- }
-
- /**
- * pm install [-l] [-r] [-t] [-i INSTALLER_PACKAGE_NAME] [-s] [-f] [--algo
- * --key --iv ] [--originating-uri
- * ] [--referrer ] PATH
- *
- * pm install: installs a package to the system.
- *
- * Options:
- * -l: install the package with FORWARD_LOCK.
- * -r: reinstall an existing app, keeping its data.
- * -t: allow test .apks to be installed.
- * -i: specify the installer package name.
- * -s: install package on sdcard.
- * -f: install package on internal flash.
- * -d: allow version code downgrade.
- *
- * pm uninstall [-k] PACKAGE
- *
- * pm uninstall: removes a package from the system.
- *
- * Options:
- * -k: keep the data and cache directories around after package removal.
- */
-
-}
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java
index fa155ac74..8794409ec 100644
--- a/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java
+++ b/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2014 Dominik Schürmann
+ * Copyright (C) 2015 Daniel Martí
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -19,8 +20,11 @@
package org.fdroid.fdroid.installer;
-import android.content.Context;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.IPackageInstallObserver;
import android.content.pm.PackageManager;
@@ -28,6 +32,8 @@ import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
+import org.fdroid.fdroid.R;
+
import java.io.File;
import java.lang.reflect.Method;
import java.util.List;
@@ -60,14 +66,18 @@ public class SystemInstaller extends Installer {
private static final String TAG = "SystemInstaller";
+ private Activity mActivity;
private final PackageInstallObserver mInstallObserver;
private final PackageDeleteObserver mDeleteObserver;
private Method mInstallMethod;
private Method mDeleteMethod;
- public SystemInstaller(Context context, PackageManager pm,
+ public static final int REQUEST_CONFIRM_PERMS = 0;
+
+ public SystemInstaller(Activity activity, PackageManager pm,
InstallerCallback callback) throws AndroidNotCompatibleException {
- super(context, pm, callback);
+ super(activity, pm, callback);
+ this.mActivity = activity;
// create internal callbacks
mInstallObserver = new PackageInstallObserver();
@@ -128,7 +138,35 @@ public class SystemInstaller extends Installer {
@Override
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
- Uri packageURI = Uri.fromFile(apkFile);
+ Uri packageUri = Uri.fromFile(apkFile);
+ if (hasNewPermissions(packageUri)) {
+ Intent intent = new Intent(mContext, InstallConfirmActivity.class);
+ intent.setData(packageUri);
+ mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS);
+ } else {
+ try {
+ doInstallPackageInternal(packageUri);
+ } catch (AndroidNotCompatibleException e) {
+ mCallback.onError(InstallerCallback.OPERATION_INSTALL,
+ InstallerCallback.ERROR_CODE_OTHER);
+ }
+ }
+ }
+
+ private boolean hasNewPermissions(Uri packageUri) {
+ AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri);
+ if (appDiff.mPkgInfo != null) {
+ AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo);
+ if (appDiff.mInstalledAppInfo != null) { // it is an update to an existing app
+ // return false if there are no new permissions
+ return (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
+ }
+ }
+ // default: show install confirm activity
+ return true;
+ }
+
+ private void doInstallPackageInternal(Uri packageURI) throws AndroidNotCompatibleException {
try {
mInstallMethod.invoke(mPm, packageURI, mInstallObserver,
INSTALL_REPLACE_EXISTING, null);
@@ -137,13 +175,62 @@ public class SystemInstaller extends Installer {
}
}
+
@Override
- protected void installPackageInternal(List apkFiles) throws AndroidNotCompatibleException {
+ protected void installPackageInternal(List apkFiles)
+ throws AndroidNotCompatibleException {
// not used
}
@Override
- protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException {
+ protected void deletePackageInternal(final String packageName)
+ throws AndroidNotCompatibleException {
+ ApplicationInfo appInfo;
+ try {
+ appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Failed to get ApplicationInfo for uninstalling");
+ return;
+ }
+
+ final boolean isUpdate = ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
+ int messageId;
+ if (isUpdate) {
+ messageId = R.string.uninstall_update_confirm;
+ } else {
+ messageId = R.string.uninstall_confirm;
+ }
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(appInfo.loadLabel(mPm));
+ builder.setIcon(appInfo.loadIcon(mPm));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ doDeletePackageInternal(packageName);
+ } catch (AndroidNotCompatibleException e) {
+ mCallback.onError(InstallerCallback.OPERATION_DELETE,
+ InstallerCallback.ERROR_CODE_OTHER);
+ }
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ mCallback.onError(InstallerCallback.OPERATION_DELETE,
+ InstallerCallback.ERROR_CODE_CANCELED);
+ }
+ });
+ builder.setMessage(messageId);
+ builder.create().show();
+ }
+
+ private void doDeletePackageInternal(final String packageName)
+ throws AndroidNotCompatibleException {
try {
mDeleteMethod.invoke(mPm, packageName, mDeleteObserver, 0);
} catch (Exception e) {
@@ -153,8 +240,24 @@ public class SystemInstaller extends Installer {
@Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
- // no need to handle onActivityResult
- return false;
+ switch (requestCode) {
+ case REQUEST_CONFIRM_PERMS:
+ if (resultCode == Activity.RESULT_OK) {
+ final Uri packageUri = data.getData();
+ try {
+ doInstallPackageInternal(packageUri);
+ } catch (AndroidNotCompatibleException e) {
+ mCallback.onError(InstallerCallback.OPERATION_INSTALL,
+ InstallerCallback.ERROR_CODE_OTHER);
+ }
+ } else {
+ mCallback.onError(InstallerCallback.OPERATION_INSTALL,
+ InstallerCallback.ERROR_CODE_CANCELED);
+ }
+ return true;
+ default:
+ return false;
+ }
}
@Override
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/TabsAdapter.java b/F-Droid/src/org/fdroid/fdroid/installer/TabsAdapter.java
new file mode 100644
index 000000000..8e211f8c5
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/TabsAdapter.java
@@ -0,0 +1,163 @@
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package org.fdroid.fdroid.installer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+
+import java.util.ArrayList;
+
+/**
+ * This is a helper class that implements the management of tabs and all
+ * details of connecting a ViewPager with associated TabHost. It relies on a
+ * trick. Normally a tab host has a simple API for supplying a View or
+ * Intent that each tab will show. This is not sufficient for switching
+ * between pages. So instead we make the content part of the tab host
+ * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
+ * view to show as the tab content. It listens to changes in tabs, and takes
+ * care of switch to the correct paged in the ViewPager whenever the selected
+ * tab changes.
+ */
+public class TabsAdapter extends PagerAdapter
+ implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
+ private final Context mContext;
+ private final TabHost mTabHost;
+ private final ViewPager mViewPager;
+ private final ArrayList mTabs = new ArrayList<>();
+ private final Rect mTempRect = new Rect();
+ private TabHost.OnTabChangeListener mOnTabChangeListener;
+
+ static final class TabInfo {
+ private final String tag;
+ private final View view;
+
+ TabInfo(String _tag, View _view) {
+ tag = _tag;
+ view = _view;
+ }
+ }
+
+ static class DummyTabFactory implements TabHost.TabContentFactory {
+ private final Context mContext;
+
+ public DummyTabFactory(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public View createTabContent(String tag) {
+ View v = new View(mContext);
+ v.setMinimumWidth(0);
+ v.setMinimumHeight(0);
+ return v;
+ }
+ }
+
+ public TabsAdapter(Activity activity, TabHost tabHost, ViewPager pager) {
+ mContext = activity;
+ mTabHost = tabHost;
+ mViewPager = pager;
+ mTabHost.setOnTabChangedListener(this);
+ mViewPager.setAdapter(this);
+ mViewPager.setOnPageChangeListener(this);
+ }
+
+ public void addTab(TabHost.TabSpec tabSpec, View view) {
+ tabSpec.setContent(new DummyTabFactory(mContext));
+ String tag = tabSpec.getTag();
+
+ TabInfo info = new TabInfo(tag, view);
+ mTabs.add(info);
+ mTabHost.addTab(tabSpec);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ View view = mTabs.get(position).view;
+ container.addView(view);
+ return view;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ container.removeView((View)object);
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ public void setOnTabChangedListener(TabHost.OnTabChangeListener listener) {
+ mOnTabChangeListener = listener;
+ }
+
+ @Override
+ public void onTabChanged(String tabId) {
+ int position = mTabHost.getCurrentTab();
+ mViewPager.setCurrentItem(position);
+ if (mOnTabChangeListener != null) {
+ mOnTabChangeListener.onTabChanged(tabId);
+ }
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ // Unfortunately when TabHost changes the current tab, it kindly
+ // also takes care of putting focus on it when not in touch mode.
+ // The jerk.
+ // This hack tries to prevent this from pulling focus out of our
+ // ViewPager.
+ TabWidget widget = mTabHost.getTabWidget();
+ int oldFocusability = widget.getDescendantFocusability();
+ widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ mTabHost.setCurrentTab(position);
+ widget.setDescendantFocusability(oldFocusability);
+
+ // Scroll the current tab into visibility if needed.
+ View tab = widget.getChildTabViewAt(position);
+ mTempRect.set(tab.getLeft(), tab.getTop(), tab.getRight(), tab.getBottom());
+ widget.requestRectangleOnScreen(mTempRect, false);
+
+ // Make sure the scrollbars are visible for a moment after selection
+ final View contentView = mTabs.get(position).view;
+ if (contentView instanceof CaffeinatedScrollView) {
+ ((CaffeinatedScrollView) contentView).awakenScrollBars();
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
index 8e835e2b6..308cc951e 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -44,7 +44,6 @@ public class LocalRepoService extends Service {
public static final String STOPPED = "org.fdroid.fdroid.category.LOCAL_REPO_STOPPED";
private NotificationManager notificationManager;
- private Notification notification;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
private final int NOTIFICATION = R.string.local_repo_running;
@@ -139,7 +138,7 @@ public class LocalRepoService extends Service {
Intent intent = new Intent(this, SwapActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- notification = new NotificationCompat.Builder(this)
+ Notification notification = new NotificationCompat.Builder(this)
.setContentTitle(getText(R.string.local_repo_running))
.setContentText(getText(R.string.touch_to_configure_local_repo))
.setSmallIcon(R.drawable.ic_swap)
diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
index 47e482524..55752e9b9 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
@@ -1,13 +1,17 @@
package org.fdroid.fdroid.views.fragments;
import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
import android.content.SharedPreferences;
+import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.support.v4.preference.PreferenceFragment;
+import android.text.Html;
import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
@@ -15,7 +19,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.PreferencesActivity;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.installer.CheckRootAsyncTask;
+import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity;
import org.fdroid.fdroid.installer.Installer;
import java.util.Locale;
@@ -39,7 +43,6 @@ public class PreferencesFragment extends PreferenceFragment
Preferences.PREF_LANGUAGE,
Preferences.PREF_CACHE_APK,
Preferences.PREF_EXPERT,
- Preferences.PREF_ROOT_INSTALLER,
Preferences.PREF_SYSTEM_INSTALLER,
Preferences.PREF_ENABLE_PROXY,
Preferences.PREF_PROXY_HOST,
@@ -152,10 +155,6 @@ public class PreferencesFragment extends PreferenceFragment
checkSummary(key, R.string.expert_on);
break;
- case Preferences.PREF_ROOT_INSTALLER:
- checkSummary(key, R.string.root_installer_on);
- break;
-
case Preferences.PREF_SYSTEM_INSTALLER:
checkSummary(key, R.string.system_installer_on);
break;
@@ -186,61 +185,6 @@ public class PreferencesFragment extends PreferenceFragment
}
}
- /**
- * Initializes RootInstaller preference. This method ensures that the preference can only be checked and persisted
- * when the user grants root access for F-Droid.
- */
- protected void initRootInstallerPreference() {
- CheckBoxPreference pref = (CheckBoxPreference) findPreference(Preferences.PREF_ROOT_INSTALLER);
-
- // we are handling persistence ourself!
- pref.setPersistent(false);
-
- pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-
- @Override
- public boolean onPreferenceClick(Preference preference) {
- final CheckBoxPreference pref = (CheckBoxPreference) preference;
-
- if (pref.isChecked()) {
- CheckRootAsyncTask checkTask = new CheckRootAsyncTask(getActivity(), new CheckRootAsyncTask.CheckRootCallback() {
-
- @Override
- public void onRootCheck(boolean rootGranted) {
- if (rootGranted) {
- // root access granted
- SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
- editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, true);
- editor.commit();
- pref.setChecked(true);
- } else {
- // root access denied
- SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
- editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false);
- editor.commit();
- pref.setChecked(false);
-
- AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
- alertBuilder.setTitle(R.string.root_access_denied_title);
- alertBuilder.setMessage(getActivity().getString(R.string.root_access_denied_body));
- alertBuilder.setNeutralButton(android.R.string.ok, null);
- alertBuilder.create().show();
- }
- }
- });
- checkTask.execute();
- } else {
- SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
- editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false);
- editor.commit();
- pref.setChecked(false);
- }
-
- return true;
- }
- });
- }
-
/**
* Initializes SystemInstaller preference, which can only be enabled when F-Droid is installed as a system-app
*/
@@ -272,8 +216,23 @@ public class PreferencesFragment extends PreferenceFragment
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
alertBuilder.setTitle(R.string.system_permission_denied_title);
- alertBuilder.setMessage(getActivity().getString(R.string.system_permission_denied_body));
- alertBuilder.setNeutralButton(android.R.string.ok, null);
+ String message = getActivity().getString(R.string.system_permission_denied_body) +
+ "