diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index cd48da400..679b6d7af 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -292,6 +292,14 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
+
+
+
+
+
+
+
+
+
+
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/label.xml b/F-Droid/res/layout/label.xml
new file mode 100644
index 000000000..b9b80e7b3
--- /dev/null
+++ b/F-Droid/res/layout/label.xml
@@ -0,0 +1,20 @@
+
+
+
+
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 22c997fdd..48095b879 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -342,6 +342,33 @@
Promising
Best 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?
+ Do you want to uninstall this app?
+
+ This update requires no new permissions.
+
+ NEW:
+ Provided by %1$s.
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..c11192dc4
--- /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/InstallConfirmActivity.java b/F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java
new file mode 100644
index 000000000..87f3adff6
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/installer/InstallConfirmActivity.java
@@ -0,0 +1,251 @@
+/*
+**
+** 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.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+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.R;
+
+public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener {
+
+ private Intent intent;
+
+ PackageManager mPm;
+ PackageInfo mPkgInfo;
+
+ private ApplicationInfo mAppInfo = null;
+
+ // 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 = mPkgInfo.applicationInfo.loadIcon(mPm);
+ final String appLabel = (String) 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 (mPkgInfo != null) {
+ AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);
+ final int NP = perms.getPermissionCount(AppSecurityPermissions.WHICH_PERSONAL);
+ final int ND = perms.getPermissionCount(AppSecurityPermissions.WHICH_DEVICE);
+ if (mAppInfo != null) {
+ msg = (mAppInfo.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 {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ TextView label = (TextView) inflater.inflate(R.layout.label, null);
+ label.setText(R.string.no_new_perms);
+ mScrollView.addView(label);
+ }
+ 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 (mAppInfo != null) {
+ // This is an update to an application, but there are no
+ // permissions at all.
+ msg = (mAppInfo.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;
+ }
+ });
+ }
+ }
+
+ private void initiateInstall() {
+ 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. display confirmation dialog if replacing pkg
+ 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".
+ mAppInfo = mPm.getApplicationInfo(pkgName,
+ PackageManager.GET_UNINSTALLED_PACKAGES);
+ if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
+ mAppInfo = null;
+ }
+ } catch (NameNotFoundException e) {
+ mAppInfo = null;
+ }
+
+ startInstallConfirm();
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPm = getPackageManager();
+
+ intent = getIntent();
+ Uri mPackageURI = intent.getData();
+ final String pkgPath = mPackageURI.getPath();
+
+ mPkgInfo = mPm.getPackageArchiveInfo(pkgPath, PackageManager.GET_PERMISSIONS);
+ mPkgInfo.applicationInfo.sourceDir = pkgPath;
+ mPkgInfo.applicationInfo.publicSourceDir = pkgPath;
+
+ setContentView(R.layout.install_start);
+ mInstallConfirm = findViewById(R.id.install_confirm_panel);
+ mInstallConfirm.setVisibility(View.INVISIBLE);
+
+ initiateInstall();
+ }
+
+ @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/Installer.java b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
index 0d42f1a19..b1610cef3 100644
--- a/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
+++ b/F-Droid/src/org/fdroid/fdroid/installer/Installer.java
@@ -140,18 +140,6 @@ abstract public class Installer {
return null;
}
- public static Installer getUnattendedInstaller(Context context, PackageManager pm,
- InstallerCallback callback) throws AndroidNotCompatibleException {
-
- 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) {
boolean hasInstallPermission =
(pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName())
diff --git a/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java b/F-Droid/src/org/fdroid/fdroid/installer/SystemInstaller.java
index 719756d29..925f2e8ee 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,8 @@
package org.fdroid.fdroid.installer;
+import android.app.Activity;
import android.app.AlertDialog;
-import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -31,12 +32,12 @@ 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;
-import org.fdroid.fdroid.R;
-
/**
* Installer based on using internal hidden APIs of the Android OS, which are
* protected by the permissions
@@ -65,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();
@@ -133,7 +138,12 @@ public class SystemInstaller extends Installer {
@Override
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
- Uri packageURI = Uri.fromFile(apkFile);
+ Intent intent = new Intent(mContext, InstallConfirmActivity.class);
+ intent.setData(Uri.fromFile(apkFile));
+ mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS);
+ }
+
+ private void doInstallPackageInternal(Uri packageURI) throws AndroidNotCompatibleException {
try {
mInstallMethod.invoke(mPm, packageURI, mInstallObserver,
INSTALL_REPLACE_EXISTING, null);
@@ -165,7 +175,7 @@ public class SystemInstaller extends Installer {
if (isUpdate) {
messageId = R.string.uninstall_update_confirm;
} else {
- messageId = R.string.uninstall_application_confirm;
+ messageId = R.string.uninstall_confirm;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
@@ -207,8 +217,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/test/src/org/fdroid/fdroid/RepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
index bb802743a..96b2ae6f5 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
@@ -6,8 +6,8 @@ import android.content.Context;
import android.test.InstrumentationTestCase;
import org.apache.commons.io.FileUtils;
-import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.RepoUpdater.UpdateException;
+import org.fdroid.fdroid.data.Repo;
import java.io.File;
import java.io.IOException;