Add install confirm+perms screen to SystemInstaller

This commit is contained in:
Daniel Martí 2015-05-10 22:43:05 +02:00 committed by Dominik Schürmann
parent 6e89a55879
commit 8e62d9ae7e
33 changed files with 1666 additions and 26 deletions

View File

@ -292,6 +292,14 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:name=".installer.InstallConfirmActivity"
android:label=""
android:parentActivityName=".FDroid">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:name=".views.ManageReposActivity"
android:label="@string/app_name"

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:inset="10dp">
<shape android:shape="oval">
<solid android:color="?android:attr/textColorSecondary" />
<size android:width="4dp" android:height="4dp" />
</shape>
</inset>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<!--
Defines the layout of a single permission item.
-->
<view class="org.fdroid.fdroid.installer.AppSecurityPermissions$PermissionItemView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/perm_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:scaleType="fitCenter" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/dividerVertical" />
<TextView
android:id="@+id/perm_name"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="16sp"
android:layout_marginStart="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|left" />
</view>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 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.
-->
<!--
Defines the layout of a single permission item that costs money.
-->
<view class="org.fdroid.fdroid.installer.AppSecurityPermissions$PermissionItemView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/perm_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:scaleType="fitCenter" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/dividerVertical" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/perm_name"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="16sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<ImageView
android:id="@+id/perm_money_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignBottom="@+id/perm_money_label"
android:scaleType="fitCenter"
android:tint="@color/perms_costs_money"
android:tintMode="src_in"
android:src="@drawable/ic_coins_s" />
<TextView
android:id="@+id/perm_money_label"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="16sp"
android:textColor="@color/perms_costs_money"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/perm_money_icon"
android:layout_below="@id/perm_name"
android:layout_marginStart="8dp"
android:text="@string/perm_costs_money" />
</RelativeLayout>
</view>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<!--
Defines the layout of a single permission item.
Contains the group name and a list of permission labels under the group.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/perm_icon"
android:layout_width="24dip"
android:layout_height="24dip"
android:layout_alignParentStart="true"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/permission_group"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:paddingStart="6dip"
android:layout_toEndOf="@id/perm_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/permission_list"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_marginTop="-4dip"
android:paddingBottom="8dip"
android:paddingStart="6dip"
android:layout_below="@id/permission_group"
android:layout_toEndOf="@id/perm_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<!-- Describes permission item consisting of a group name and the list of permisisons under the group -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/no_permissions"
android:text="@string/no_permissions"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- Populated with all permissions. -->
<LinearLayout
android:id="@+id/perms_list"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<!--
Defines the layout of the application snippet that appears on top of the
installation screens
-->
<!-- The snippet about the application - title, icon, description. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/app_snippet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dip"
android:paddingEnd="16dip"
android:paddingTop="24dip"
>
<ImageView android:id="@+id/app_icon"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_marginStart="8dip"
android:background="@color/transparent"
android:layout_alignParentStart="true"
android:gravity="start"
android:scaleType="centerCrop"/>
<TextView android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
android:shadowColor="@color/shadow"
android:shadowRadius="2"
android:layout_toEndOf="@id/app_icon"
android:singleLine="true"
android:layout_centerInParent="true"
android:paddingEnd="16dip"
android:paddingTop="3dip"
android:paddingStart="16dip"
android:ellipsize="end"/>
<FrameLayout
android:id="@+id/top_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dip"
android:layout_below="@id/app_name">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<!--
Defines the layout of the splash screen that displays the security
settings required for an application and requests the confirmation of the
user before it is installed.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip" />
<ImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal"
android:visibility="gone" />
<FrameLayout
android:id="@+id/filler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone">
</FrameLayout>
<TabHost
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView android:id="@+id/tabscontainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/tab_unselected_holo"
android:fillViewport="true"
android:scrollbars="none">
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<TabWidget
android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</HorizontalScrollView>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
</TabHost>
<!-- OK confirm and cancel buttons. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="?android:attr/dividerHorizontal"
android:showDividers="beginning">
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:measureWithLargestChild="true">
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button android:id="@+id/cancel_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:text="@string/cancel"
android:maxLines="2"
style="?android:attr/buttonBarButtonStyle" />
<Button android:id="@+id/ok_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:text="@string/next"
android:maxLines="2"
android:filterTouchesWhenObscured="true"
style="?android:attr/buttonBarButtonStyle" />
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/install_app_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/app_snippet"/>
<include
layout="@layout/install_confirm"
android:id="@+id/install_confirm_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/app_snippet"
android:layout_alignParentBottom="true"/>
</RelativeLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:textAppearance="?android:attr/textAppearanceMedium"
android:gravity="center" />

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 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.
-->
<!--
This is the structure for the list of all permissions.
-->
<org.fdroid.fdroid.installer.CaffeinatedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scrollview"
android:fillViewport="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:id="@+id/privacylist"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp">
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/privacyPerms" />
</LinearLayout>
<LinearLayout android:id="@+id/devicelist"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp">
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/devicePerms" />
</LinearLayout>
</LinearLayout>
</org.fdroid.fdroid.installer.CaffeinatedScrollView>

View File

@ -15,4 +15,9 @@
<color name="swap_wifi_may_work">#fbb040</color>
<color name="swap_wifi_likely_to_work">#00a14b</color>
<color name="shadow">#cc222222</color>
<color name="transparent">#00000000</color>
<color name="perms_costs_money">#fff4511e</color>
</resources>

View File

@ -342,6 +342,33 @@
<string name="wifi_warning_private">Promising</string>
<string name="wifi_warning_personal">Best bet</string>
<string name="install_confirm">Do you want to install this application?
It will get access to:</string>
<string name="install_confirm_no_perms">Do you want to install this application?
It does not require any special access.</string>
<string name="install_confirm_update">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:</string>
<string name="install_confirm_update_system">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:</string>
<string name="install_confirm_update_no_perms">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.</string>
<string name="install_confirm_update_system_no_perms">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.</string>
<!--<string name="no_permissions_required">No permissions required</string>-->
<string name="newPerms">New</string>
<string name="allPerms">All</string>
<string name="privacyPerms">Privacy</string>
<string name="devicePerms">Device Access</string>
<string name="perm_costs_money">this may cost you money</string>
<string name="uninstall_update_confirm">Do you want to replace this app with the factory version?</string>
<string name="uninstall_application_confirm">Do you want to uninstall this app?</string>
<string name="uninstall_confirm">Do you want to uninstall this app?</string>
<string name="no_new_perms">This update requires no new permissions.</string>
<string name="perms_new_perm_prefix"><font size="12" fgcolor="#ff33b5e5">NEW: </font></string>
<string name="perms_description_app">Provided by %1$s.</string>
</resources>

View File

@ -0,0 +1,525 @@
/*
**
** Copyright 2007, The Android Open Source Project
** Copyright 2015 Daniel Martí <mvdan@mvdan.cc>
**
** 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<String, MyPermissionGroupInfo> mPermGroups = new HashMap<>();
private final List<MyPermissionGroupInfo> 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<MyPermissionInfo> mNewPermissions = new ArrayList<>();
final List<MyPermissionInfo> mPersonalPermissions = new ArrayList<>();
final List<MyPermissionInfo> mDevicePermissions = new ArrayList<>();
final List<MyPermissionInfo> 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<MyPermissionInfo> 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<MyPermissionInfo> 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<strList.length; i++) {
String permName = strList[i];
// If we are only looking at an existing app, then we only
// care about permissions that have actually been granted to it.
if (installedPkgInfo != null && info == installedPkgInfo) {
if ((flagsList[i]&PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0) {
continue;
}
}
try {
PermissionInfo tmpPermInfo = mPm.getPermissionInfo(permName, 0);
if (tmpPermInfo == null) {
continue;
}
int existingIndex = -1;
if (installedPkgInfo != null && installedPkgInfo.requestedPermissions != null) {
for (int j=0; j<installedPkgInfo.requestedPermissions.length; j++) {
if (permName.equals(installedPkgInfo.requestedPermissions[j])) {
existingIndex = j;
break;
}
}
}
int existingFlags = 0;
if (existingIndex >= 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<MyPermissionInfo> 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<mPermGroupsList.size(); i++) {
N += getPermissionList(mPermGroupsList.get(i), which).size();
}
return N;
}
public View getPermissionsView(int which) {
LinearLayout permsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null);
LinearLayout displayList = (LinearLayout) permsView.findViewById(R.id.perms_list);
View noPermsView = permsView.findViewById(R.id.no_permissions);
displayPermissions(mPermGroupsList, displayList, which);
if (displayList.getChildCount() <= 0) {
noPermsView.setVisibility(View.VISIBLE);
}
return permsView;
}
/**
* Utility method that displays permissions from a map containing group name and
* list of permission descriptions.
*/
private void displayPermissions(List<MyPermissionGroupInfo> groups,
LinearLayout permListView, int which) {
permListView.removeAllViews();
int spacing = (int) (8*mContext.getResources().getDisplayMetrics().density);
for (int i=0; i<groups.size(); i++) {
MyPermissionGroupInfo grp = groups.get(i);
final List<MyPermissionInfo> perms = getPermissionList(grp, which);
for (int j=0; j<perms.size(); j++) {
MyPermissionInfo perm = perms.get(j);
View view = getPermissionItemView(grp, perm, j == 0,
which != WHICH_NEW ? mNewPermPrefix : null);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
if (j == 0) {
lp.topMargin = spacing;
}
if (j == grp.mAllPermissions.size()-1) {
lp.bottomMargin = spacing;
}
if (permListView.getChildCount() == 0) {
lp.topMargin *= 2;
}
permListView.addView(view, lp);
}
}
}
private PermissionItemView getPermissionItemView(MyPermissionGroupInfo grp,
MyPermissionInfo perm, boolean first, CharSequence newPermPrefix) {
PermissionItemView permView = (PermissionItemView) mInflater.inflate(
(perm.flags & PermissionInfo.FLAG_COSTS_MONEY) != 0
? R.layout.app_permission_item_money : R.layout.app_permission_item,
null);
permView.setPermission(grp, perm, first, newPermPrefix, mPackageName);
return permView;
}
private boolean isDisplayablePermission(PermissionInfo pInfo, int newReqFlags,
int existingReqFlags) {
final int base = pInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
final boolean isNormal = (base == PermissionInfo.PROTECTION_NORMAL);
final boolean isDangerous = (base == PermissionInfo.PROTECTION_DANGEROUS);
final boolean isRequired =
((newReqFlags&PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0);
final boolean isDevelopment =
((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0);
final boolean wasGranted =
((existingReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
final boolean isGranted =
((newReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
// Dangerous and normal permissions are always shown to the user if the permission
// is required, or it was previously granted
if ((isNormal || isDangerous) && (isRequired || wasGranted || isGranted)) {
return true;
}
// Development permissions are only shown to the user if they are already
// granted to the app -- if we are installing an app and they are not
// already granted, they will not be granted as part of the install.
if (isDevelopment && wasGranted) {
return true;
}
return false;
}
private static class PermissionGroupInfoComparator implements Comparator<MyPermissionGroupInfo> {
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<MyPermissionInfo> {
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<MyPermissionInfo> 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<MyPermissionInfo> 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);
}
}

View File

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

View File

@ -0,0 +1,251 @@
/*
**
** Copyright 2007, The Android Open Source Project
** Copyright 2015 Daniel Martí <mvdan@mvdan.cc>
**
** 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();
}
}
}

View File

@ -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())

View File

@ -1,5 +1,6 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 Daniel Martí <mvdan@mvdan.cc>
*
* 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,9 +217,25 @@ public class SystemInstaller extends Installer {
@Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
// no need to handle onActivityResult
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
public boolean supportsUnattendedOperations() {

View File

@ -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<TabInfo> 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) {
}
}

View File

@ -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)

View File

@ -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;