Merge branch 'fdroid-system-install'

This commit is contained in:
Daniel Martí 2015-06-14 18:15:19 +02:00
commit 62f9c84252
49 changed files with 4025 additions and 493 deletions

View File

@ -55,9 +55,6 @@
<uses-permission android:name="android.permission.DELETE_PACKAGES"
tools:ignore="ProtectedPermissions"/>
<!-- Indicate that F-Droid may request root access (introduced by Koush's Superuser app) -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
<application
android:name="FDroidApp"
android:icon="@drawable/ic_launcher"
@ -295,6 +292,14 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:name=".installer.InstallConfirmActivity"
android:label="@string/menu_install"
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"
@ -406,6 +411,16 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<!-- Note: Theme.NoDisplay, this activity shows dialogs only -->
<activity
android:name=".installer.InstallIntoSystemDialogActivity"
android:theme="@android:style/Theme.NoDisplay" />
<receiver
android:name=".installer.InstallIntoSystemBootReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".SearchResults"
android:label="@string/search_results"

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: 610 B

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: 344 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: 424 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: 691 B

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.1 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,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>
</resources>
<color name="shadow">#cc222222</color>
<color name="transparent">#00000000</color>
<color name="perms_costs_money">#fff4511e</color>
</resources>

View File

@ -25,10 +25,10 @@
<string name="notify_on">Notify when updates are available</string>
<string name="update_history">Update history</string>
<string name="update_history_summ">Days to consider apps new or recent: %s</string>
<string name="root_installer">Install using root access</string>
<string name="root_installer_on">Request root access to install, update, and remove packages</string>
<string name="system_installer">Install using system-permissions</string>
<string name="system_installer_on">Use system permissions to install, update, and remove packages</string>
<string name="system_installer">Enable privileged F-Droid</string>
<string name="system_installer_on">Use privileged permissions to install, update, and remove packages</string>
<string name="uninstall_system">Uninstall privileged F-Droid</string>
<string name="uninstall_system_summary">Uninstall F-Droid when installed as a privileged app</string>
<string name="local_repo_bonjour">Broadcast Local Repo</string>
<string name="local_repo_bonjour_on">Advertise your local repo using Bonjour (mDNS)</string>
<string name="local_repo_name">Name of your Local Repo</string>
@ -290,15 +290,30 @@
<string name="empty_available_app_list">No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.</string>
<string name="empty_can_update_app_list">All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).</string>
<string name="requesting_root_access_title">Root access</string>
<string name="requesting_root_access_body">Requesting root access…</string>
<string name="root_access_denied_title">Root access denied</string>
<string name="root_access_denied_body">Either your Android device is not rooted or you have denied root access for F-Droid.</string>
<string name="update_all">Update all</string>
<string name="installer_error_title">(De-)Installation Error</string>
<string name="installer_error_body">The (de-)installation failed. If you are using root access, try disabling this setting!</string>
<string name="installer_error_body">The (de-)installation failed. If you are using F-Droid as a privileged app, try disabling this setting!</string>
<string name="system_permission_denied_title">System permissions denied</string>
<string name="system_permission_denied_body">This option is only available when F-Droid is installed as a system-app.</string>
<string name="system_permission_denied_body">This option is only available when F-Droid is installed as a privileged app.</string>
<string name="system_permission_install_via_root">Install as a privileged app</string>
<string name="system_install_first_time_notification">Install privileged F-Droid?</string>
<string name="system_install_first_time_notification_message_short">Touch for more information.</string>
<string name="system_install_first_time_notification_message">Touch to install F-Droid as a privileged app, tightly coupled with the Android operating system. This enables extended features, such as automatic app updates.\nYou can also do this later from F-Droid\'s preferences.</string>
<string name="system_install_post_success">Successful installation as a privileged app</string>
<string name="system_install_post_fail">Installation as a privileged app failed</string>
<string name="system_install_post_success_message">F-Droid has been successfully installed as a privileged app. This enables extended features, such as automatic app updates.</string>
<string name="system_install_post_fail_message">The installation of F-Droid as a privileged app failed. The installation method is not supported by all Android distributions, please consult the F-Droid bug tracker for more information.</string>
<string name="system_install_installing">installing…</string>
<string name="system_install_uninstalling">uninstalling…</string>
<string name="system_install_question">Do you want to install F-Droid as a privileged app?\nThis takes up to 10 seconds where &lt;b>no user interface&lt;/b> is shown.</string>
<string name="system_install_question_lollipop">Do you want to install F-Droid as a privileged app?\nThis takes up to 10 seconds where &lt;b>no user interface&lt;/b> is shown and the device will be &lt;b>rebooted&lt;/b> afterwards.</string>
<string name="system_install_first_time_message">Looks like you have root access on your device. You can now install F-Droid as a privileged app, tightly coupled with the Android operating system. This enables extended features, such as automatic app updates.</string>
<string name="system_uninstall">Do you want to uninstall F-Droid?</string>
<string name="system_uninstall_message">This will uninstall F-Droid completely.</string>
<string name="system_uninstall_button">Uninstall</string>
<string name="app_description">F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device.</string>
<string name="swap_nfc_description">If your friend has <b>F-Droid and NFC turned on</b> touch your phones together.</string>
@ -332,4 +347,31 @@
<string name="wifi_warning_public">May work</string>
<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="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_confirm">Do you want to uninstall this app?</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

@ -81,13 +81,13 @@
<CheckBoxPreference android:title="@string/expert"
android:defaultValue="false"
android:key="expert" />
<CheckBoxPreference android:title="@string/root_installer"
android:defaultValue="false"
android:key="rootInstaller"
android:dependency="expert" />
<CheckBoxPreference android:title="@string/system_installer"
android:defaultValue="false"
android:key="systemInstaller"
android:dependency="expert" />
<Preference android:title="@string/uninstall_system"
android:summary="@string/uninstall_system_summary"
android:key="uninstallSystemApp"
android:dependency="expert" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -47,6 +47,8 @@ import android.widget.Toast;
import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.swap.ConnectSwapActivity;
@ -101,6 +103,8 @@ public class FDroid extends ActionBarActivity {
Uri uri = AppProvider.getContentUri();
getContentResolver().registerContentObserver(uri, true, new AppObserver());
InstallIntoSystemDialogActivity.firstTime(this);
}
@Override

View File

@ -98,23 +98,26 @@ public class FDroidApp extends Application {
}
public void applyTheme(Activity activity) {
switch (curTheme) {
case dark:
activity.setTheme(R.style.AppThemeDark);
break;
case light:
activity.setTheme(R.style.AppThemeLight);
break;
case lightWithDarkActionBar:
activity.setTheme(R.style.AppThemeLightWithDarkActionBar);
break;
}
activity.setTheme(getCurThemeResId());
}
public static Theme getCurTheme() {
return curTheme;
}
public static int getCurThemeResId() {
switch (curTheme) {
case dark:
return R.style.AppThemeDark;
case light:
return R.style.AppThemeLight;
case lightWithDarkActionBar:
return R.style.AppThemeLightWithDarkActionBar;
default:
return R.style.AppThemeDark;
}
}
public static void enableSpongyCastle() {
Security.addProvider(spongyCastleProvider);
}

View File

@ -49,8 +49,8 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String PREF_CACHE_APK = "cacheDownloaded";
public static final String PREF_EXPERT = "expert";
public static final String PREF_UPD_LAST = "lastUpdateCheck";
public static final String PREF_ROOT_INSTALLER = "rootInstaller";
public static final String PREF_SYSTEM_INSTALLER = "systemInstaller";
public static final String PREF_UNINSTALL_SYSTEM_APP = "uninstallSystemApp";
public static final String PREF_LOCAL_REPO_BONJOUR = "localRepoBonjour";
public static final String PREF_LOCAL_REPO_NAME = "localRepoName";
public static final String PREF_LOCAL_REPO_HTTPS = "localRepoHttps";
@ -59,11 +59,12 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String PREF_PROXY_HOST = "proxyHost";
public static final String PREF_PROXY_PORT = "proxyPort";
public static final String PREF_SHOW_NFC_DURING_SWAP = "showNfcDuringSwap";
public static final String PREF_FIRST_TIME = "firstTime";
public static final String PREF_POST_SYSTEM_INSTALL = "postSystemInstall";
private static final boolean DEFAULT_COMPACT_LAYOUT = false;
private static final boolean DEFAULT_ROOTED = true;
private static final int DEFAULT_UPD_HISTORY = 14;
private static final boolean DEFAULT_ROOT_INSTALLER = false;
private static final boolean DEFAULT_SYSTEM_INSTALLER = false;
private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true;
private static final boolean DEFAULT_CACHE_APK = false;
@ -76,6 +77,8 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String DEFAULT_PROXY_HOST = "127.0.0.1";
public static final int DEFAULT_PROXY_PORT = 8118;
public static final boolean DEFAULT_SHOW_NFC_DURING_SWAP = true;
private static final boolean DEFAULT_FIRST_TIME = true;
private static final boolean DEFAULT_POST_SYSTEM_INSTALL = false;
private boolean compactLayout = DEFAULT_COMPACT_LAYOUT;
private boolean filterAppsRequiringRoot = DEFAULT_ROOTED;
@ -101,14 +104,30 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
initialized.put(key, false);
}
public boolean isRootInstallerEnabled() {
return preferences.getBoolean(PREF_ROOT_INSTALLER, DEFAULT_ROOT_INSTALLER);
}
public boolean isSystemInstallerEnabled() {
return preferences.getBoolean(PREF_SYSTEM_INSTALLER, DEFAULT_SYSTEM_INSTALLER);
}
public void setSystemInstallerEnabled(boolean enable) {
preferences.edit().putBoolean(PREF_SYSTEM_INSTALLER, enable).commit();
}
public boolean isFirstTime() {
return preferences.getBoolean(PREF_FIRST_TIME, DEFAULT_FIRST_TIME);
}
public void setFirstTime(boolean firstTime) {
preferences.edit().putBoolean(PREF_FIRST_TIME, firstTime).commit();
}
public boolean isPostSystemInstall() {
return preferences.getBoolean(PREF_POST_SYSTEM_INSTALL, DEFAULT_POST_SYSTEM_INSTALL);
}
public void setPostSystemInstall(boolean postInstall) {
preferences.edit().putBoolean(PREF_POST_SYSTEM_INSTALL, postInstall).commit();
}
public boolean isLocalRepoBonjourEnabled() {
return preferences.getBoolean(PREF_LOCAL_REPO_BONJOUR, DEFAULT_LOCAL_REPO_BONJOUR);
}

View File

@ -0,0 +1,69 @@
/*
**
** Copyright 2007, The Android Open Source Project
** Copyright 2015 Dominik Schürmann <dominik@dominikschuermann.de>
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package org.fdroid.fdroid.installer;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
public class AppDiff {
PackageManager mPm;
PackageInfo mPkgInfo;
ApplicationInfo mInstalledAppInfo = null;
public AppDiff(PackageManager mPm, Uri mPackageURI) {
this.mPm = mPm;
final String pkgPath = mPackageURI.getPath();
mPkgInfo = mPm.getPackageArchiveInfo(pkgPath, PackageManager.GET_PERMISSIONS);
mPkgInfo.applicationInfo.sourceDir = pkgPath;
mPkgInfo.applicationInfo.publicSourceDir = pkgPath;
init();
}
private void init() {
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
// but it has been renamed to something else.
final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
if (oldName != null && oldName.length > 0 && oldName[0] != null) {
pkgName = oldName[0];
mPkgInfo.packageName = pkgName;
mPkgInfo.applicationInfo.packageName = pkgName;
}
// Check if package is already installed
try {
// This is a little convoluted because we want to get all uninstalled
// apps, but this may include apps with just data, and if it is just
// data we still want to count it as "installed".
mInstalledAppInfo = mPm.getApplicationInfo(pkgName,
PackageManager.GET_UNINSTALLED_PACKAGES);
if ((mInstalledAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
mInstalledAppInfo = null;
}
} catch (PackageManager.NameNotFoundException e) {
mInstalledAppInfo = null;
}
}
}

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

@ -1,71 +0,0 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import org.fdroid.fdroid.R;
import eu.chainfire.libsuperuser.Shell;
public class CheckRootAsyncTask extends AsyncTask<Void, Void, Boolean> {
ProgressDialog mDialog;
final Context mContext;
final CheckRootCallback mCallback;
public interface CheckRootCallback {
void onRootCheck(boolean rootGranted);
}
public CheckRootAsyncTask(Context context, CheckRootCallback callback) {
super();
this.mContext = context;
this.mCallback = callback;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mDialog = new ProgressDialog(mContext);
mDialog.setTitle(R.string.requesting_root_access_title);
mDialog.setMessage(mContext.getString(R.string.requesting_root_access_body));
mDialog.setIndeterminate(true);
mDialog.setCancelable(false);
mDialog.show();
}
@Override
protected Boolean doInBackground(Void... params) {
return Shell.SU.available();
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
mDialog.dismiss();
mCallback.onRootCheck(result);
}
}

View File

@ -0,0 +1,218 @@
/*
**
** 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.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener {
private Intent intent;
PackageManager mPm;
AppDiff mAppDiff;
// View for install progress
View mInstallConfirm;
// Buttons to indicate user acceptance
private Button mOk;
private Button mCancel;
CaffeinatedScrollView mScrollView = null;
private boolean mOkCanInstall = false;
private static final String TAB_ID_ALL = "all";
private static final String TAB_ID_NEW = "new";
private void startInstallConfirm() {
final Drawable appIcon = mAppDiff.mPkgInfo.applicationInfo.loadIcon(mPm);
final String appLabel = (String) mAppDiff.mPkgInfo.applicationInfo.loadLabel(mPm);
View appSnippet = findViewById(R.id.app_snippet);
((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon);
((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel);
TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost);
tabHost.setup();
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
adapter.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
@Override
public void onTabChanged(String tabId) {
}
});
boolean permVisible = false;
mScrollView = null;
mOkCanInstall = false;
int msg = 0;
if (mAppDiff.mPkgInfo != null) {
AppSecurityPermissions perms = new AppSecurityPermissions(this, mAppDiff.mPkgInfo);
final int NP = perms.getPermissionCount(AppSecurityPermissions.WHICH_PERSONAL);
final int ND = perms.getPermissionCount(AppSecurityPermissions.WHICH_DEVICE);
if (mAppDiff.mInstalledAppInfo != null) {
msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
? R.string.install_confirm_update_system
: R.string.install_confirm_update;
mScrollView = new CaffeinatedScrollView(this);
mScrollView.setFillViewport(true);
final boolean newPermissionsFound =
(perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
if (newPermissionsFound) {
permVisible = true;
mScrollView.addView(perms.getPermissionsView(
AppSecurityPermissions.WHICH_NEW));
} else {
throw new RuntimeException("This should not happen. No new permissions were found but InstallConfirmActivity has been started!");
}
adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(
getText(R.string.newPerms)), mScrollView);
} else {
findViewById(R.id.tabscontainer).setVisibility(View.GONE);
findViewById(R.id.divider).setVisibility(View.VISIBLE);
}
if (NP > 0 || ND > 0) {
permVisible = true;
LayoutInflater inflater = (LayoutInflater) getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.permissions_list, null);
if (mScrollView == null) {
mScrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
}
final ViewGroup privacyList = (ViewGroup) root.findViewById(R.id.privacylist);
if (NP > 0) {
privacyList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_PERSONAL));
} else {
privacyList.setVisibility(View.GONE);
}
final ViewGroup deviceList = (ViewGroup) root.findViewById(R.id.devicelist);
if (ND > 0) {
deviceList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_DEVICE));
} else {
root.findViewById(R.id.devicelist).setVisibility(View.GONE);
}
adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator(
getText(R.string.allPerms)), root);
}
}
if (!permVisible) {
if (mAppDiff.mInstalledAppInfo != null) {
// This is an update to an application, but there are no
// permissions at all.
msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
? R.string.install_confirm_update_system_no_perms
: R.string.install_confirm_update_no_perms;
} else {
// This is a new application with no permissions.
msg = R.string.install_confirm_no_perms;
}
tabHost.setVisibility(View.GONE);
findViewById(R.id.filler).setVisibility(View.VISIBLE);
findViewById(R.id.divider).setVisibility(View.GONE);
mScrollView = null;
}
if (msg != 0) {
((TextView) findViewById(R.id.install_confirm)).setText(msg);
}
mInstallConfirm.setVisibility(View.VISIBLE);
mOk = (Button) findViewById(R.id.ok_button);
mCancel = (Button) findViewById(R.id.cancel_button);
mOk.setOnClickListener(this);
mCancel.setOnClickListener(this);
if (mScrollView == null) {
// There is nothing to scroll view, so the ok button is immediately
// set to install.
mOk.setText(R.string.menu_install);
mOkCanInstall = true;
} else {
mScrollView.setFullScrollAction(new Runnable() {
@Override
public void run() {
mOk.setText(R.string.menu_install);
mOkCanInstall = true;
}
});
}
}
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
((FDroidApp) getApplication()).applyTheme(this);
mPm = getPackageManager();
intent = getIntent();
Uri packageURI = intent.getData();
mAppDiff = new AppDiff(mPm, packageURI);
setContentView(R.layout.install_start);
mInstallConfirm = findViewById(R.id.install_confirm_panel);
mInstallConfirm.setVisibility(View.INVISIBLE);
startInstallConfirm();
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
// Generic handling when pressing back key
public void onCancel(DialogInterface dialog) {
finish();
}
public void onClick(View v) {
if (v == mOk) {
if (mOkCanInstall || mScrollView == null) {
setResult(RESULT_OK, intent);
finish();
} else {
mScrollView.pageScroll(View.FOCUS_DOWN);
}
} else if (v == mCancel) {
setResult(RESULT_CANCELED, intent);
finish();
}
}
}

View File

@ -0,0 +1,207 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import java.util.ArrayList;
import java.util.List;
import eu.chainfire.libsuperuser.Shell;
/**
* Partly based on
* http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/
* https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java
*/
@TargetApi(Build.VERSION_CODES.FROYO)
abstract class InstallIntoSystem {
protected final Context context;
public InstallIntoSystem(final Context context) {
this.context = context;
}
public static InstallIntoSystem create(final Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new LollipopImpl(context);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return new KitKatToLollipopImpl(context);
} else {
return new PreKitKatImpl(context);
}
}
protected abstract String getSystemFolder();
protected void onPreInstall() {
// To be overridden by relevant base class[es]
}
public String getWarningInfo() {
return context.getString(R.string.system_install_question);
}
final void runUninstall() {
final String[] commands = {
"am force-stop org.fdroid.fdroid",
"pm clear org.fdroid.fdroid",
"mount -o rw,remount /system",
"pm uninstall " + context.getPackageName(),
"rm -f " + getInstallPath(),
"sleep 5",
"mount -o ro,remount /system"
};
Shell.SU.run(commands);
}
final void runInstall() {
onPreInstall();
Shell.SU.run(getInstallCommands());
}
protected String getInstallPath() {
return getSystemFolder() + "FDroid.apk";
}
private List<String> getInstallCommands() {
final List<String> commands = new ArrayList<>();
commands.add("mount -o rw,remount /system");
commands.addAll(getCopyToSystemCommands());
commands.add("pm uninstall -k " + context.getPackageName()); // -k to retain data
commands.add("mv " + getInstallPath() + ".tmp " + getInstallPath());
commands.add("pm install -r " + getInstallPath());
commands.add("sleep 5"); // wait until the app is really installed
commands.add("mount -o ro,remount /system");
commands.add("am force-stop org.fdroid.fdroid");
commands.addAll(getPostInstallCommands());
return commands;
}
protected List<String> getCopyToSystemCommands() {
final List<String> commands = new ArrayList<>(2);
commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp");
commands.add("chmod 655 " + getInstallPath() + ".tmp");
return commands;
}
protected List<String> getPostInstallCommands() {
final List<String> commands = new ArrayList<>(1);
commands.add("am start -n org.fdroid.fdroid/.installer.InstallIntoSystemDialogActivity --ez post_install true");
return commands;
}
private static class PreKitKatImpl extends InstallIntoSystem {
public PreKitKatImpl(Context context) {
super(context);
}
@Override
protected String getSystemFolder() {
return "/system/app";
}
}
private static class KitKatToLollipopImpl extends InstallIntoSystem {
public KitKatToLollipopImpl(Context context) {
super(context);
}
/**
* On KitKat, "Some system apps are more system than others"
* https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54
*/
@Override
protected String getSystemFolder() {
return "/system/priv-app/";
}
}
/**
* History of PackageManagerService in Lollipop:
* https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java
*/
private static class LollipopImpl extends InstallIntoSystem {
public LollipopImpl(Context context) {
super(context);
}
@Override
protected void onPreInstall() {
// Setup preference to execute postInstall after reboot
Preferences.get().setPostSystemInstall(true);
}
public String getWarningInfo() {
return context.getString(R.string.system_install_question_lollipop);
}
/**
* Cluster-style layout where each app is placed in a unique directory
*/
@Override
protected String getSystemFolder() {
return "/system/priv-app/FDroid/";
}
/**
* Create app directory
*/
@Override
protected List<String> getCopyToSystemCommands() {
List<String> commands = new ArrayList<>(3);
commands.add("mkdir " + getSystemFolder()); // create app directory if not existing
commands.add("cat " + context.getPackageCodePath() + " > " + getInstallPath() + ".tmp");
commands.add("chmod 655 " + getInstallPath() + ".tmp");
return commands;
}
/**
* TODO: Currently only works with reboot
* <p/>
* File observers on /system/priv-app/ have been removed because they don't work with the new
* cluser-style layout. See
* https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103
* <p/>
* Related stack overflow post: http://stackoverflow.com/q/26487750
*/
@Override
protected List<String> getPostInstallCommands() {
List<String> commands = new ArrayList<>(3);
commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN");
commands.add("sleep 1");
commands.add("reboot");
return commands;
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.fdroid.fdroid.Preferences;
public class InstallIntoSystemBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
if (Preferences.get().isPostSystemInstall()) {
Preferences.get().setPostSystemInstall(false);
Intent postInstall = new Intent(context.getApplicationContext(), InstallIntoSystemDialogActivity.class);
postInstall.setAction(InstallIntoSystemDialogActivity.ACTION_POST_INSTALL);
postInstall.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(postInstall);
}
}
}
}

View File

@ -0,0 +1,375 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NotificationCompat;
import android.text.Html;
import android.util.Log;
import android.view.ContextThemeWrapper;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import eu.chainfire.libsuperuser.Shell;
/**
* Note: This activity has no view on its own, it displays consecutive dialogs.
*/
public class InstallIntoSystemDialogActivity extends FragmentActivity {
private static final String TAG = "InstallIntoSystem";
public static final String ACTION_INSTALL = "install";
public static final String ACTION_UNINSTALL = "uninstall";
public static final String ACTION_POST_INSTALL = "post_install";
public static final String ACTION_FIRST_TIME = "first_time";
String action;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// this activity itself has no content view (see manifest)
if (getIntent().getAction() == null) {
Log.e(TAG, "Please define an action!");
finish();
return;
}
action = getIntent().getAction();
if (ACTION_UNINSTALL.equals(action)) {
uninstall();
} else if (ACTION_INSTALL.equals(action)) {
checkRootTask.execute();
} else if (ACTION_FIRST_TIME.equals(action)) {
checkRootTask.execute();
} else if (ACTION_POST_INSTALL.equals(action)) {
postInstall();
}
}
public static void firstTime(final Context context) {
if (Preferences.get().isFirstTime()) {
Preferences.get().setFirstTime(false);
if (Installer.hasSystemPermissions(context, context.getPackageManager())) {
Preferences.get().setSystemInstallerEnabled(true);
} else {
runFirstTime(context);
}
}
}
public static void runFirstTime(final Context context) {
// don't do a "real" root access, just look up if "su" is present and has a version!
// a real check would annoy the user
AsyncTask<Void, Void, Boolean> checkRoot = new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
return (Shell.SU.version(true) != null);
}
@Override
protected void onPostExecute(Boolean probablyRoot) {
super.onPostExecute(probablyRoot);
if (probablyRoot) {
// looks like we have root, at least su has a version number and is present
Intent installIntent = new Intent(context, InstallIntoSystemDialogActivity.class);
installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_FIRST_TIME);
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent resultPendingIntent =
PendingIntent.getActivity(
context,
0,
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setContentIntent(resultPendingIntent)
.setSmallIcon(R.drawable.ic_stat_notify)
.setContentTitle(context.getString(R.string.system_install_first_time_notification))
.setContentText(context.getString(R.string.system_install_first_time_notification_message_short))
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true)
/*
* Sets the big view "big text" style and supplies the
* text (the user's reminder message) that will be displayed
* in the detail area of the expanded notification.
* These calls are ignored by the support library for
* pre-4.1 devices.
*/
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(context.getString(R.string.system_install_first_time_notification_message)));
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(42, builder.build());
}
}
};
checkRoot.execute();
}
/**
* first time
*/
private void firstTime() {
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
String message = getString(R.string.system_install_first_time_message) + "<br/><br/>" + InstallIntoSystem.create(getApplicationContext()).getWarningInfo();
AlertDialog.Builder builder = new AlertDialog.Builder(theme)
.setMessage(Html.fromHtml(message))
.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
installTask.execute();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
InstallIntoSystemDialogActivity.this.finish();
}
});
builder.create().show();
}
/**
* 1. Check for root access
*/
public AsyncTask<Void, Void, Boolean> checkRootTask = new AsyncTask<Void, Void, Boolean>() {
ProgressDialog mProgressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(getString(R.string.requesting_root_access_body));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
@Override
protected Boolean doInBackground(Void... params) {
return Shell.SU.available();
}
@Override
protected void onPostExecute(Boolean rootGranted) {
super.onPostExecute(rootGranted);
mProgressDialog.dismiss();
if (rootGranted) {
// root access granted
if (ACTION_UNINSTALL.equals(action)) {
uninstallTask.execute();
} else if (ACTION_INSTALL.equals(action)) {
installTask.execute();
} else if (ACTION_FIRST_TIME.equals(action)) {
firstTime();
}
} else {
// root access denied
if (!ACTION_FIRST_TIME.equals(action)) {
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
FDroidApp.getCurThemeResId());
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(theme)
.setTitle(R.string.root_access_denied_title)
.setMessage(getString(R.string.root_access_denied_body))
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
InstallIntoSystemDialogActivity.this.finish();
}
});
alertBuilder.create().show();
}
}
}
};
/**
* 2. Install into system
*/
AsyncTask<Void, Void, Void> installTask = new AsyncTask<Void, Void, Void>() {
ProgressDialog mProgressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(getString(R.string.system_install_installing));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
@Override
protected Void doInBackground(Void... voids) {
InstallIntoSystem.create(getApplicationContext()).runInstall();
return null;
}
};
/**
* 3. Verify that install worked
*/
private void postInstall() {
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
final boolean success = Installer.hasSystemPermissions(this, this.getPackageManager());
// enable system installer on installation success
Preferences.get().setSystemInstallerEnabled(success);
AlertDialog.Builder builder = new AlertDialog.Builder(theme)
.setTitle(success ? R.string.system_install_post_success : R.string.system_install_post_fail)
.setMessage(success ? R.string.system_install_post_success_message : R.string.system_install_post_fail_message)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
InstallIntoSystemDialogActivity.this.setResult(success ? Activity.RESULT_OK : Activity.RESULT_CANCELED);
InstallIntoSystemDialogActivity.this.finish();
startActivity(new Intent(InstallIntoSystemDialogActivity.this, FDroid.class));
}
})
.setCancelable(false);
builder.create().show();
}
private void uninstall() {
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
final boolean systemApp = Installer.hasSystemPermissions(this, this.getPackageManager());
if (systemApp) {
AlertDialog.Builder builder = new AlertDialog.Builder(theme)
.setTitle(R.string.system_uninstall)
.setMessage(R.string.system_uninstall_message)
.setPositiveButton(R.string.system_uninstall_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
checkRootTask.execute();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
InstallIntoSystemDialogActivity.this.finish();
}
});
builder.create().show();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(theme)
.setTitle(R.string.system_permission_denied_title)
.setMessage(getString(R.string.system_permission_denied_body))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
InstallIntoSystemDialogActivity.this.setResult(Activity.RESULT_CANCELED);
InstallIntoSystemDialogActivity.this.finish();
}
});
builder.create().show();
}
}
AsyncTask<Void, Void, Void> uninstallTask = new AsyncTask<Void, Void, Void>() {
ProgressDialog mProgressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
// hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay
ContextThemeWrapper theme = new ContextThemeWrapper(InstallIntoSystemDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(getString(R.string.system_install_uninstalling));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
@Override
protected Void doInBackground(Void... voids) {
InstallIntoSystem.create(getApplicationContext()).runUninstall();
return null;
}
@Override
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
mProgressDialog.dismiss();
// app is uninstalled but still display, kill it!
System.exit(0);
}
};
}

View File

@ -95,28 +95,10 @@ abstract public class Installer {
/**
* Creates a new Installer for installing/deleting processes starting from
* an Activity
*
* @param activity
* @param pm
* @param callback
* @return
* @throws AndroidNotCompatibleException
*/
public static Installer getActivityInstaller(Activity activity, PackageManager pm,
InstallerCallback callback) {
// if root installer has been activated in preferences -> RootInstaller
boolean isRootInstallerEnabled = Preferences.get().isRootInstallerEnabled();
if (isRootInstallerEnabled) {
Log.d(TAG, "root installer preference enabled -> RootInstaller");
try {
return new RootInstaller(activity, pm, callback);
} catch (AndroidNotCompatibleException e) {
Log.e(TAG, "Android not compatible with RootInstaller!", e);
}
}
// system permissions and pref enabled -> SystemInstaller
boolean isSystemInstallerEnabled = Preferences.get().isSystemInstallerEnabled();
if (isSystemInstallerEnabled) {
@ -133,7 +115,7 @@ abstract public class Installer {
}
}
// Fallback -> DefaultInstaller
// else -> DefaultInstaller
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// Default installer on Android >= 4.0
try {
@ -158,38 +140,15 @@ abstract public class Installer {
return null;
}
public static Installer getUnattendedInstaller(Context context, PackageManager pm,
InstallerCallback callback) throws AndroidNotCompatibleException {
// if root installer has been activated in preferences -> RootInstaller
boolean useRootInstaller = Preferences.get().isRootInstallerEnabled();
if (useRootInstaller) {
try {
return new RootInstaller(context, pm, callback);
} catch (AndroidNotCompatibleException e) {
Log.e(TAG, "Android not compatible with RootInstaller!", e);
}
}
if (hasSystemPermissions(context, pm)) {
// we have system permissions!
return new SystemInstaller(context, pm, callback);
} else {
// nope!
throw new AndroidNotCompatibleException();
}
}
public static boolean hasSystemPermissions(Context context, PackageManager pm) {
int checkInstallPermission =
pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName());
int checkDeletePermission =
pm.checkPermission(permission.DELETE_PACKAGES, context.getPackageName());
boolean permissionsGranted =
(checkInstallPermission == PackageManager.PERMISSION_GRANTED
&& checkDeletePermission == PackageManager.PERMISSION_GRANTED);
boolean hasInstallPermission =
(pm.checkPermission(permission.INSTALL_PACKAGES, context.getPackageName())
== PackageManager.PERMISSION_GRANTED);
boolean hasDeletePermission =
(pm.checkPermission(permission.DELETE_PACKAGES, context.getPackageName())
== PackageManager.PERMISSION_GRANTED);
return permissionsGranted;
return (hasInstallPermission && hasDeletePermission);
}
public void installPackage(File apkFile) throws AndroidNotCompatibleException {

View File

@ -1,266 +0,0 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.chainfire.libsuperuser.Shell;
/**
* Installer using a root shell and "pm install", "pm uninstall" commands
*/
public class RootInstaller extends Installer {
private static final String TAG = "RootInstaller";
Shell.Interactive rootSession;
public RootInstaller(Context context, PackageManager pm, InstallerCallback callback)
throws AndroidNotCompatibleException {
super(context, pm, callback);
}
private Shell.Builder createShellBuilder() {
return new Shell.Builder()
.useSU()
.setWantSTDERR(true)
.setWatchdogTimeout(30)
.setMinimalLogging(false);
}
@Override
protected void installPackageInternal(final File apkFile) throws AndroidNotCompatibleException {
rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
// Callback to report whether the shell was successfully
// started up
@Override
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
// NOTE: Additional exit codes:
// Shell.OnCommandResultListener.SHELL_WRONG_UID
// Shell.OnCommandResultListener.SHELL_EXEC_FAILED
Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
} else {
addInstallCommand(apkFile);
}
}
});
}
@Override
protected void installPackageInternal(final List<File> apkFiles)
throws AndroidNotCompatibleException {
rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
// Callback to report whether the shell was successfully
// started up
@Override
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
// NOTE: Additional exit codes:
// Shell.OnCommandResultListener.SHELL_WRONG_UID
// Shell.OnCommandResultListener.SHELL_EXEC_FAILED
Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
} else {
addInstallCommand(apkFiles);
}
}
});
}
@Override
protected void deletePackageInternal(final String packageName)
throws AndroidNotCompatibleException {
rootSession = createShellBuilder().open(new Shell.OnCommandResultListener() {
// Callback to report whether the shell was successfully
// started up
@Override
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
// NOTE: Additional exit codes:
// Shell.OnCommandResultListener.SHELL_WRONG_UID
// Shell.OnCommandResultListener.SHELL_EXEC_FAILED
Log.e(TAG, "Error opening root shell with exitCode " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
} else {
addDeleteCommand(packageName);
}
}
});
}
@Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
// no need to handle onActivityResult
return false;
}
private void addInstallCommand(File apkFile) {
// Like package names, apk files should also only contain letters, numbers, dots, or underscore,
// e.g., org.fdroid.fdroid_9.apk
if (!isValidPackageName(apkFile.getName())) {
Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): "
+ apkFile.getName());
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
return;
}
rootSession.addCommand("pm install -r \"" + apkFile.getAbsolutePath() + "\"", 0,
new Shell.OnCommandResultListener() {
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
// close su shell
rootSession.close();
if (exitCode < 0) {
Log.e(TAG, "Install failed with exit code " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
} else {
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
}
}
});
}
private void addInstallCommand(List<File> apkFiles) {
List<String> commands = new ArrayList<>();
String pm = "pm install -r ";
for (File apkFile : apkFiles) {
// see addInstallCommand()
if (!isValidPackageName(apkFile.getName())) {
Log.e(TAG, "File name is not valid (contains characters other than letters, numbers, dots, or underscore): "
+ apkFile.getName());
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
return;
}
commands.add(pm + "\"" + apkFile.getAbsolutePath() + "\"");
}
rootSession.addCommand(commands, 0,
new Shell.OnCommandResultListener() {
public void onCommandResult(int commandCode, int exitCode,
List<String> output) {
// close su shell
rootSession.close();
if (exitCode < 0) {
Log.e(TAG, "Install failed with exit code " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
} else {
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
}
}
});
}
private void addDeleteCommand(String packageName) {
if (!isValidPackageName(packageName)) {
Log.e(TAG, "Package name is not valid (contains characters other than letters, numbers, dots, or underscore): "
+ packageName);
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
return;
}
rootSession.addCommand("pm uninstall \"" + packageName + "\"", 0,
new Shell.OnCommandResultListener() {
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
// close su shell
rootSession.close();
if (exitCode < 0) {
Log.e(TAG, "Delete failed with exit code " + exitCode);
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
} else {
mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
}
}
});
}
@Override
public boolean supportsUnattendedOperations() {
return true;
}
private static final Pattern PACKAGE_NAME_BLACKLIST = Pattern.compile("[^a-zA-Z0-9\\.\\_]");
/**
* Package names should only contain letters, numbers, dots, and underscores!
* Prevent injection attacks with app names like ";touch $'\057data\057injected'"
*
* @param packageName
* @return
*/
private boolean isValidPackageName(String packageName) {
Matcher matcher = PACKAGE_NAME_BLACKLIST.matcher(packageName);
return !matcher.find();
}
/**
* pm install [-l] [-r] [-t] [-i INSTALLER_PACKAGE_NAME] [-s] [-f] [--algo
* <algorithm name> --key <key-in-hex> --iv <IV-in-hex>] [--originating-uri
* <URI>] [--referrer <URI>] PATH
* <p/>
* pm install: installs a package to the system.
* <p/>
* Options:<br/>
* -l: install the package with FORWARD_LOCK.<br/>
* -r: reinstall an existing app, keeping its data.<br/>
* -t: allow test .apks to be installed.<br/>
* -i: specify the installer package name.<br/>
* -s: install package on sdcard.<br/>
* -f: install package on internal flash.<br/>
* -d: allow version code downgrade.<br/>
* <p/>
* pm uninstall [-k] PACKAGE
* <p/>
* pm uninstall: removes a package from the system.
* <p/>
* Options:<br/>
* -k: keep the data and cache directories around after package removal.
*/
}

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,11 @@
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.IPackageInstallObserver;
import android.content.pm.PackageManager;
@ -28,6 +32,8 @@ import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import org.fdroid.fdroid.R;
import java.io.File;
import java.lang.reflect.Method;
import java.util.List;
@ -60,14 +66,18 @@ public class SystemInstaller extends Installer {
private static final String TAG = "SystemInstaller";
private Activity mActivity;
private final PackageInstallObserver mInstallObserver;
private final PackageDeleteObserver mDeleteObserver;
private Method mInstallMethod;
private Method mDeleteMethod;
public SystemInstaller(Context context, PackageManager pm,
public static final int REQUEST_CONFIRM_PERMS = 0;
public SystemInstaller(Activity activity, PackageManager pm,
InstallerCallback callback) throws AndroidNotCompatibleException {
super(context, pm, callback);
super(activity, pm, callback);
this.mActivity = activity;
// create internal callbacks
mInstallObserver = new PackageInstallObserver();
@ -128,7 +138,35 @@ public class SystemInstaller extends Installer {
@Override
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
Uri packageURI = Uri.fromFile(apkFile);
Uri packageUri = Uri.fromFile(apkFile);
if (hasNewPermissions(packageUri)) {
Intent intent = new Intent(mContext, InstallConfirmActivity.class);
intent.setData(packageUri);
mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS);
} else {
try {
doInstallPackageInternal(packageUri);
} catch (AndroidNotCompatibleException e) {
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
}
}
}
private boolean hasNewPermissions(Uri packageUri) {
AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri);
if (appDiff.mPkgInfo != null) {
AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo);
if (appDiff.mInstalledAppInfo != null) { // it is an update to an existing app
// return false if there are no new permissions
return (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
}
}
// default: show install confirm activity
return true;
}
private void doInstallPackageInternal(Uri packageURI) throws AndroidNotCompatibleException {
try {
mInstallMethod.invoke(mPm, packageURI, mInstallObserver,
INSTALL_REPLACE_EXISTING, null);
@ -137,13 +175,62 @@ public class SystemInstaller extends Installer {
}
}
@Override
protected void installPackageInternal(List<File> apkFiles) throws AndroidNotCompatibleException {
protected void installPackageInternal(List<File> apkFiles)
throws AndroidNotCompatibleException {
// not used
}
@Override
protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException {
protected void deletePackageInternal(final String packageName)
throws AndroidNotCompatibleException {
ApplicationInfo appInfo;
try {
appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Failed to get ApplicationInfo for uninstalling");
return;
}
final boolean isUpdate = ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
int messageId;
if (isUpdate) {
messageId = R.string.uninstall_update_confirm;
} else {
messageId = R.string.uninstall_confirm;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(appInfo.loadLabel(mPm));
builder.setIcon(appInfo.loadIcon(mPm));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
doDeletePackageInternal(packageName);
} catch (AndroidNotCompatibleException e) {
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_OTHER);
}
}
});
builder.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
mCallback.onError(InstallerCallback.OPERATION_DELETE,
InstallerCallback.ERROR_CODE_CANCELED);
}
});
builder.setMessage(messageId);
builder.create().show();
}
private void doDeletePackageInternal(final String packageName)
throws AndroidNotCompatibleException {
try {
mDeleteMethod.invoke(mPm, packageName, mDeleteObserver, 0);
} catch (Exception e) {
@ -153,8 +240,24 @@ public class SystemInstaller extends Installer {
@Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
// no need to handle onActivityResult
return false;
switch (requestCode) {
case REQUEST_CONFIRM_PERMS:
if (resultCode == Activity.RESULT_OK) {
final Uri packageUri = data.getData();
try {
doInstallPackageInternal(packageUri);
} catch (AndroidNotCompatibleException e) {
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
}
} else {
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_CANCELED);
}
return true;
default:
return false;
}
}
@Override

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

@ -1,13 +1,17 @@
package org.fdroid.fdroid.views.fragments;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.support.v4.preference.PreferenceFragment;
import android.text.Html;
import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
@ -15,7 +19,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.PreferencesActivity;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.installer.CheckRootAsyncTask;
import org.fdroid.fdroid.installer.InstallIntoSystemDialogActivity;
import org.fdroid.fdroid.installer.Installer;
import java.util.Locale;
@ -39,7 +43,6 @@ public class PreferencesFragment extends PreferenceFragment
Preferences.PREF_LANGUAGE,
Preferences.PREF_CACHE_APK,
Preferences.PREF_EXPERT,
Preferences.PREF_ROOT_INSTALLER,
Preferences.PREF_SYSTEM_INSTALLER,
Preferences.PREF_ENABLE_PROXY,
Preferences.PREF_PROXY_HOST,
@ -152,10 +155,6 @@ public class PreferencesFragment extends PreferenceFragment
checkSummary(key, R.string.expert_on);
break;
case Preferences.PREF_ROOT_INSTALLER:
checkSummary(key, R.string.root_installer_on);
break;
case Preferences.PREF_SYSTEM_INSTALLER:
checkSummary(key, R.string.system_installer_on);
break;
@ -186,61 +185,6 @@ public class PreferencesFragment extends PreferenceFragment
}
}
/**
* Initializes RootInstaller preference. This method ensures that the preference can only be checked and persisted
* when the user grants root access for F-Droid.
*/
protected void initRootInstallerPreference() {
CheckBoxPreference pref = (CheckBoxPreference) findPreference(Preferences.PREF_ROOT_INSTALLER);
// we are handling persistence ourself!
pref.setPersistent(false);
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
final CheckBoxPreference pref = (CheckBoxPreference) preference;
if (pref.isChecked()) {
CheckRootAsyncTask checkTask = new CheckRootAsyncTask(getActivity(), new CheckRootAsyncTask.CheckRootCallback() {
@Override
public void onRootCheck(boolean rootGranted) {
if (rootGranted) {
// root access granted
SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, true);
editor.commit();
pref.setChecked(true);
} else {
// root access denied
SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false);
editor.commit();
pref.setChecked(false);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
alertBuilder.setTitle(R.string.root_access_denied_title);
alertBuilder.setMessage(getActivity().getString(R.string.root_access_denied_body));
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
}
});
checkTask.execute();
} else {
SharedPreferences.Editor editor = pref.getSharedPreferences().edit();
editor.putBoolean(Preferences.PREF_ROOT_INSTALLER, false);
editor.commit();
pref.setChecked(false);
}
return true;
}
});
}
/**
* Initializes SystemInstaller preference, which can only be enabled when F-Droid is installed as a system-app
*/
@ -272,8 +216,23 @@ public class PreferencesFragment extends PreferenceFragment
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
alertBuilder.setTitle(R.string.system_permission_denied_title);
alertBuilder.setMessage(getActivity().getString(R.string.system_permission_denied_body));
alertBuilder.setNeutralButton(android.R.string.ok, null);
String message = getActivity().getString(R.string.system_permission_denied_body) +
"<br/><br/>";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
message += getActivity().getString(R.string.system_install_question_lollipop);
} else {
message += getActivity().getString(R.string.system_install_question);
}
alertBuilder.setMessage(Html.fromHtml(message));
alertBuilder.setPositiveButton(R.string.system_permission_install_via_root, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent installIntent = new Intent(getActivity(), InstallIntoSystemDialogActivity.class);
installIntent.setAction(InstallIntoSystemDialogActivity.ACTION_INSTALL);
startActivity(installIntent);
}
});
alertBuilder.setNegativeButton(R.string.cancel, null);
alertBuilder.create().show();
}
} else {
@ -288,6 +247,23 @@ public class PreferencesFragment extends PreferenceFragment
});
}
protected void initUninstallSystemAppPreference() {
Preference pref = findPreference(Preferences.PREF_UNINSTALL_SYSTEM_APP);
pref.setPersistent(false);
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent uninstallIntent = new Intent(getActivity(), InstallIntoSystemDialogActivity.class);
uninstallIntent.setAction(InstallIntoSystemDialogActivity.ACTION_UNINSTALL);
startActivity(uninstallIntent);
return true;
}
});
}
private void langSpinner(String key) {
final ListPreference pref = (ListPreference)findPreference(key);
final String[] langValues = getResources().getStringArray(R.array.languageValues);
@ -310,8 +286,8 @@ public class PreferencesFragment extends PreferenceFragment
updateSummary(key, false);
}
initRootInstallerPreference();
initSystemInstallerPreference();
initUninstallSystemAppPreference();
}
@Override

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;

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 49 KiB