diff options
author | 2022-11-17 16:24:19 -0800 | |
---|---|---|
committer | 2022-12-07 17:42:50 +0000 | |
commit | 2ef3f709ece0ed759dcda3ef53ac7a9bcb36e87b (patch) | |
tree | 326ad119e4694aa92212de35bf78158805aff867 | |
parent | 04e240e492440c40668b49246c55f7bf4fcaaea8 (diff) |
Add initial PermissionRationaleActivity
Activity shows details about permission data usage by the app
NO_IFTTT=initial ifchange addition
Bug: 256913489
Test: atest PermissionRationaleTest
Test: atest PermissionRationalePermissionGrantDialogTest
Change-Id: I092f6a5ecd1e6bc41d083456b20b017adb6519a1
20 files changed, 1303 insertions, 3 deletions
diff --git a/PermissionController/AndroidManifest.xml b/PermissionController/AndroidManifest.xml index 004d87064..a5d912ae7 100644 --- a/PermissionController/AndroidManifest.xml +++ b/PermissionController/AndroidManifest.xml @@ -249,6 +249,17 @@ </intent-filter> </activity> + <activity android:name="com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity" + android:configChanges="keyboardHidden|screenSize" + android:excludeFromRecents="true" + android:exported="false" + android:theme="@style/GrantPermissions.FilterTouches" + android:visibleToInstantApps="true" + android:inheritShowWhenLocked="true" + android:hardwareAccelerated="false" + android:canDisplayOnRemoteDevices="false"> + </activity> + <activity android:name="com.android.permissioncontroller.permission.ui.ManagePermissionsActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:label="@string/app_permissions" diff --git a/PermissionController/res/drawable-v34/ic_business.xml b/PermissionController/res/drawable-v34/ic_business.xml new file mode 100644 index 000000000..23ee37d0e --- /dev/null +++ b/PermissionController/res/drawable-v34/ic_business.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,7L12,3L2,3v18h20L22,7L12,7zM6,19L4,19v-2h2v2zM6,15L4,15v-2h2v2zM6,11L4,11L4,9h2v2zM6,7L4,7L4,5h2v2zM10,19L8,19v-2h2v2zM10,15L8,15v-2h2v2zM10,11L8,11L8,9h2v2zM10,7L8,7L8,5h2v2zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2L12,9h8v10zM18,11h-2v2h2v-2zM18,15h-2v2h2v-2z"/> +</vector> diff --git a/PermissionController/res/drawable-v34/ic_collections_bookmark.xml b/PermissionController/res/drawable-v34/ic_collections_bookmark.xml new file mode 100644 index 000000000..29f9e569c --- /dev/null +++ b/PermissionController/res/drawable-v34/ic_collections_bookmark.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L8,16L8,4h5v7l2.5,-1.88L18,11L18,4h2v12z"/> +</vector> diff --git a/PermissionController/res/drawable-v34/ic_gear.xml b/PermissionController/res/drawable-v34/ic_gear.xml new file mode 100644 index 000000000..958284d4c --- /dev/null +++ b/PermissionController/res/drawable-v34/ic_gear.xml @@ -0,0 +1,13 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M13.85,22.25h-3.7c-0.74,0 -1.36,-0.54 -1.45,-1.27l-0.27,-1.89c-0.27,-0.14 -0.53,-0.29 -0.79,-0.46l-1.8,0.72c-0.7,0.26 -1.47,-0.03 -1.81,-0.65L2.2,15.53c-0.35,-0.66 -0.2,-1.44 0.36,-1.88l1.53,-1.19c-0.01,-0.15 -0.02,-0.3 -0.02,-0.46 0,-0.15 0.01,-0.31 0.02,-0.46l-1.52,-1.19c-0.59,-0.45 -0.74,-1.26 -0.37,-1.88l1.85,-3.19c0.34,-0.62 1.11,-0.9 1.79,-0.63l1.81,0.73c0.26,-0.17 0.52,-0.32 0.78,-0.46l0.27,-1.91c0.09,-0.7 0.71,-1.25 1.44,-1.25h3.7c0.74,0 1.36,0.54 1.45,1.27l0.27,1.89c0.27,0.14 0.53,0.29 0.79,0.46l1.8,-0.72c0.71,-0.26 1.48,0.03 1.82,0.65l1.84,3.18c0.36,0.66 0.2,1.44 -0.36,1.88l-1.52,1.19c0.01,0.15 0.02,0.3 0.02,0.46s-0.01,0.31 -0.02,0.46l1.52,1.19c0.56,0.45 0.72,1.23 0.37,1.86l-1.86,3.22c-0.34,0.62 -1.11,0.9 -1.8,0.63l-1.8,-0.72c-0.26,0.17 -0.52,0.32 -0.78,0.46l-0.27,1.91c-0.1,0.68 -0.72,1.22 -1.46,1.22zM10.62,20.25h2.76l0.37,-2.55 0.53,-0.22c0.44,-0.18 0.88,-0.44 1.34,-0.78l0.45,-0.34 2.38,0.96 1.38,-2.4 -2.03,-1.58 0.07,-0.56c0.03,-0.26 0.06,-0.51 0.06,-0.78s-0.03,-0.53 -0.06,-0.78l-0.07,-0.56 2.03,-1.58 -1.39,-2.4 -2.39,0.96 -0.45,-0.35c-0.42,-0.32 -0.87,-0.58 -1.33,-0.77l-0.52,-0.22 -0.37,-2.55h-2.76l-0.37,2.55 -0.53,0.21c-0.44,0.19 -0.88,0.44 -1.34,0.79l-0.45,0.33 -2.38,-0.95 -1.39,2.39 2.03,1.58 -0.07,0.56c-0.03,0.26 -0.06,0.53 -0.06,0.79s0.02,0.53 0.06,0.78l0.07,0.56 -2.03,1.58 1.38,2.4 2.39,-0.96 0.45,0.35c0.43,0.33 0.86,0.58 1.33,0.77l0.53,0.22 0.38,2.55z"/> + <path + android:fillColor="@android:color/white" + android:pathData="M12,12m-3.5,0a3.5,3.5 0,1 1,7 0a3.5,3.5 0,1 1,-7 0"/> +</vector> diff --git a/PermissionController/res/drawable-v34/ic_info.xml b/PermissionController/res/drawable-v34/ic_info.xml new file mode 100644 index 000000000..35f7f5f61 --- /dev/null +++ b/PermissionController/res/drawable-v34/ic_info.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/> +</vector> diff --git a/PermissionController/res/layout-v34/permission_rationale.xml b/PermissionController/res/layout-v34/permission_rationale.xml new file mode 100644 index 000000000..c3b782d75 --- /dev/null +++ b/PermissionController/res/layout-v34/permission_rationale.xml @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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. + --> + +<!-- + ~ A lot of content in this file is identical to grant_permissions.xml + ~ Be sure to update both files when making changes. + --> + +<!-- In (hopefully very rare) case dialog is too high: allow scrolling --> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + style="@style/PermissionRationaleScrollView"> + + <LinearLayout + android:id="@+id/grant_singleton" + android:importantForAccessibility="no" + android:focusable="false" + style="@style/PermissionRationaleSingleton"> + + <!-- The dialog --> + <LinearLayout + android:id="@+id/grant_dialog" + android:theme="@style/Theme.PermissionGrantDialog" + android:importantForAccessibility="no" + style="@style/PermissionRationaleDialog"> + + <LinearLayout + android:id="@+id/content_container" + style="@style/PermissionRationaleContent"> + + <LinearLayout + style="@style/PermissionRationaleTitleContainer"> + + <ImageView + android:id="@+id/permission_icon" + android:importantForAccessibility="no" + android:src="@drawable/ic_shield_exclamation_outline" + style="@style/PermissionRationaleTitleIcon" /> + + <TextView + android:id="@+id/permission_rationale_title" + android:text="@string/permission_rationale_title" + style="@style/PermissionRationaleTitleMessage" /> + + </LinearLayout> + + <LinearLayout style="@style/PermissionRationaleSectionOuterContainer"> + <ImageView + android:id="@+id/purpose_icon" + android:importantForAccessibility="no" + android:src="@drawable/ic_info" + style="@style/PermissionRationaleSectionIcon" /> + <LinearLayout style="@style/PermissionRationaleSectionInnerContainer"> + <TextView + android:id="@+id/purpose_title" + android:text="@string/permission_rationale_purpose_title" + style="@style/PermissionRationaleSectionTitle" /> + <TextView + android:id="@+id/purpose_message" + android:text="@string/permission_rationale_purpose_message" + style="@style/PermissionRationaleSectionMessage" /> + </LinearLayout> + </LinearLayout> + <LinearLayout style="@style/PermissionRationaleSectionOuterContainer"> + <ImageView + android:id="@+id/third_party_icon" + android:importantForAccessibility="no" + android:src="@drawable/ic_business" + style="@style/PermissionRationaleSectionIcon" /> + <LinearLayout style="@style/PermissionRationaleSectionInnerContainer"> + <TextView + android:id="@+id/third_party_title" + android:text="@string/permission_rationale_thirdparty_title" + style="@style/PermissionRationaleSectionTitle" /> + <TextView + android:id="@+id/third_party_message" + android:text="@string/permission_rationale_thirdparty_message" + style="@style/PermissionRationaleSectionMessage" /> + </LinearLayout> + </LinearLayout> + <LinearLayout style="@style/PermissionRationaleSectionOuterContainer"> + <ImageView + android:id="@+id/settings_icon" + android:importantForAccessibility="no" + android:src="@drawable/ic_gear" + style="@style/PermissionRationaleSectionIcon" /> + <LinearLayout style="@style/PermissionRationaleSectionInnerContainer"> + <TextView + android:id="@+id/settings_title" + android:text="@string/permission_rationale_permission_settings_title" + style="@style/PermissionRationaleSectionTitle" /> + <TextView + android:id="@+id/settings_message" + android:text="@string/permission_rationale_permission_settings_message" + style="@style/PermissionRationaleSectionMessage" /> + </LinearLayout> + </LinearLayout> + <LinearLayout style="@style/PermissionRationaleSectionOuterContainer"> + <ImageView + android:id="@+id/learn_more_icon" + android:importantForAccessibility="no" + android:src="@drawable/ic_collections_bookmark" + style="@style/PermissionRationaleSectionIcon" /> + <LinearLayout style="@style/PermissionRationaleSectionInnerContainer"> + <TextView + android:id="@+id/learn_more_message" + android:text="@string/permission_rationale_permission_learn_more_title" + style="@style/PermissionRationaleSectionTitle" /> + </LinearLayout> + </LinearLayout> + + <LinearLayout style="@style/PermissionRationaleButtonContainer"> + <!-- TODO(b/260269197): update back button style --> + <Button + android:id="@+id/back_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="36dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:text="@string/back" /> + </LinearLayout> + + </LinearLayout> + </LinearLayout> + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/PermissionController/res/values-v34/strings.xml b/PermissionController/res/values-v34/strings.xml new file mode 100644 index 000000000..412467779 --- /dev/null +++ b/PermissionController/res/values-v34/strings.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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. + --> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Permission Rationale - Start --> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Title message shown for Permission Rationale dialog [CHAR LIMIT=50] --> + <string name="permission_rationale_title">More about allowing this access</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Title shown for Permission Rationale data sharing purposes section [CHAR LIMIT=50] --> + <string name="permission_rationale_purpose_title">Why the app may share data?</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Message shown to the user letting them know that data will be shared and for which + purposes. It will also display where the information was provided from via the install_source + placeholder, this will map to app store labels/names. Purposes will be comma delimited and are + one or many of the following: "app functionality", "analytics", "developer communications", + "advertising or marketing", "fraud prevention, security, and compliance", "personalization", + "account management". Example usage as follows: "This app developer stated to App Store that it + may share for app functionality, analytics, developer communications, advertising or marketing, + fraud prevention, security, and compliance, personalization, account management. This may be + updated in the future." [CHAR LIMIT=300] --> + <string name="permission_rationale_purpose_message">This app developer stated to <annotation id="link"><xliff:g id="install_source" example="App Store">%1$s</xliff:g></annotation> that it may share for <xliff:g id="purpose_list" example="purpose 1, purpose 2, purpose 3">%2$s</xliff:g>\nThis may be updated in the future.</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Message shown to the user letting them know that data will be shared and for which + purposes. It will also display that the information was provided via the device manufacturer + (company responsible for making and released the device). Purposes will be comma delimited and + are one or many of the following: "app functionality", "analytics", "developer communications", + "advertising or marketing", "fraud prevention, security, and compliance", "personalization", + "account management". Example usage as follows: "This app developer stated to the device + manufacturer that it may share for app functionality, analytics, developer communications, + advertising or marketing, fraud prevention, security, and compliance, personalization, account + management. This may be updated in the future." [CHAR LIMIT=300] --> + <string name="permission_rationale_purpose_default_source_message">This app developer stated to the device manufacturer that it may share for <xliff:g id="purpose_list" example="purpose 1, purpose 2, purpose 3">%1$s</xliff:g>\nThis may be updated in the future.</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Title shown for Permission Rationale third party description section [CHAR LIMIT=50] --> + <string name="permission_rationale_thirdparty_title">What are third parties?</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Message shown to the user that explains what a "third party" is [CHAR LIMIT=100] --> + <string name="permission_rationale_thirdparty_message">Third parties are other companies or organizations</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Title shown for Permission Rationale permission settings section [CHAR LIMIT=50] --> + <string name="permission_rationale_permission_settings_title">Change or revoke access any time</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Message shown to the user letting them where to change permission settings in the future [CHAR LIMIT=100] --> + <string name="permission_rationale_permission_settings_message">Go to your <annotation id="link">privacy settings</annotation>, under <xliff:g id="permission_name" example="location">%1$s</xliff:g></string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Title shown for Permission Rationale "learn more about data sharing" section [CHAR LIMIT=50] --> + <string name="permission_rationale_permission_learn_more_title"><annotation id="link">Learn more</annotation> about data sharing</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for app functionality. This may be updated in the future." + [CHAR LIMIT=30] --> + <string name="permission_rational_purpose_app_functionality">app functionality</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for analytics. This may be updated in the future." + [CHAR LIMIT=30] --> + <string name="permission_rational_purpose_analytics">analytics</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for developer communications. This may be updated in the future." + [CHAR LIMIT=50] --> + <string name="permission_rational_purpose_developer_communications">developer communications</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for advertising or marketing. This may be updated in the future." + [CHAR LIMIT=50] --> + <string name="permission_rational_purpose_advertising">advertising or marketing</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for fraud prevention, security, and compliance. This may be + updated in the future." [CHAR LIMIT=75] --> + <string name="permission_rational_purpose_fraud_prevention_security">fraud prevention, security, and compliance</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for personalization. This may be updated in the future." + [CHAR LIMIT=50] --> + <string name="permission_rational_purpose_personalization">personalization</string> + + <!-- TODO(b/259279178): update with finalized permission rationale strings --> + <!-- Permission usage purpose shown for Permission Rationale. This will be used with the + permission_rationale_purpose_message and permission_rationale_purpose_default_source_message + strings. Example usage with this string as follows: "This app developer stated to the device + manufacturer that it may share for account management. This may be updated in the future." + [CHAR LIMIT=50] --> + <string name="permission_rational_purpose_account_management">account management</string> + + <!-- Permission Rationale - End --> + +</resources> diff --git a/PermissionController/res/values/overlayable.xml b/PermissionController/res/values/overlayable.xml index ed8dc8969..6f7d5a8ac 100644 --- a/PermissionController/res/values/overlayable.xml +++ b/PermissionController/res/values/overlayable.xml @@ -69,6 +69,26 @@ <!-- END PERMISSION GRANT DIALOG --> + <!-- START PERMISSION RATIONALE DIALOG --> + + <item type="style" name="PermissionRationaleScrollView" /> + <item type="style" name="PermissionRationaleSingleton" /> + <item type="style" name="PermissionRationaleDialog" /> + <item type="style" name="PermissionRationaleContent" /> + + <item type="style" name="PermissionRationaleTitleContainer" /> + <item type="style" name="PermissionRationaleTitleIcon" /> + <item type="style" name="PermissionRationaleTitleMessage" /> + + <item type="style" name="PermissionRationaleSectionOuterContainer" /> + <item type="style" name="PermissionRationaleSectionIcon" /> + <item type="style" name="PermissionRationaleSectionInnerContainer" /> + <item type="style" name="PermissionRationaleSectionTitle" /> + <item type="style" name="PermissionRationaleSectionMessage" /> + + <item type="style" name="PermissionRationaleButtonContainer" /> + + <!-- END PERMISSION RATIONALE DIALOG --> <!-- START PERMISSION REVIEW SCREEN --> <item type="style" name="PermissionReview" /> diff --git a/PermissionController/res/values/styles.xml b/PermissionController/res/values/styles.xml index 60f39874b..041747ac3 100644 --- a/PermissionController/res/values/styles.xml +++ b/PermissionController/res/values/styles.xml @@ -198,7 +198,7 @@ <item name="android:layout_width">20dp</item> <item name="android:layout_height">20dp</item> <item name="android:layout_gravity">start|center_vertical</item> - <item name="android:scaleType">fitCenter</item> + <item name="android:scaleType">centerInside</item> <item name="android:tint">?android:attr/textColorSecondary</item> </style> @@ -217,7 +217,7 @@ <item name="android:layout_width">20dp</item> <item name="android:layout_height">20dp</item> <item name="android:layout_gravity">end|center_vertical</item> - <item name="android:scaleType">fitCenter</item> + <item name="android:scaleType">centerInside</item> <item name="android:tint">?android:attr/textColorSecondary</item> </style> @@ -262,6 +262,113 @@ <!-- END PERMISSION GRANT DIALOG --> + <!-- START PERMISSION RATIONALE DIALOG --> + + <style name="PermissionRationaleScrollView"> + <item name="android:scrollbars">none</item> + <item name="android:fillViewport">true</item> + <item name="android:clipChildren">false</item> + </style> + + <style name="PermissionRationaleSingleton"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:gravity">center</item> + </style> + + <style name="PermissionRationaleDialog"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:background">?android:attr/windowBackground</item> + <item name="android:orientation">vertical</item> + <item name="android:showDividers">middle</item> + </style> + + <style name="PermissionRationaleContent"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:orientation">vertical</item> + <item name="android:paddingTop">24dp</item> + <item name="android:paddingBottom">18dp</item> + <item name="android:paddingStart">24dp</item> + <item name="android:paddingEnd">24dp</item> + </style> + + <style name="PermissionRationaleTitleContainer"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:orientation">vertical</item> + <item name="android:gravity">center</item> + </style> + + <style name="PermissionRationaleTitleIcon"> + <item name="android:layout_width">32dp</item> + <item name="android:layout_height">32dp</item> + <item name="android:layout_marginBottom">16dp</item> + <item name="android:tint">?android:attr/textColorSecondary</item> + <item name="android:scaleType">centerInside</item> + </style> + + <style name="PermissionRationaleTitleMessage" + parent="@android:style/TextAppearance.DeviceDefault.WindowTitle"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:gravity">center</item> + </style> + + <style name="PermissionRationaleSectionOuterContainer"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:orientation">horizontal</item> + <item name="android:layout_marginTop">16dp</item> + </style> + + <style name="PermissionRationaleSectionIcon"> + <item name="android:layout_width">20dp</item> + <item name="android:layout_height">20dp</item> + <item name="android:tint">?android:attr/textColorSecondary</item> + <item name="android:scaleType">centerInside</item> + </style> + + <style name="PermissionRationaleSectionInnerContainer"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:orientation">vertical</item> + <item name="android:layout_marginStart">16dp</item> + </style> + + <style name="PermissionRationaleSectionTitle" + parent="@android:style/TextAppearance.DeviceDefault.Medium"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:lineSpacingMultiplier">1.25</item> + </style> + + <style name="PermissionRationaleSectionMessage" + parent="@android:style/TextAppearance.DeviceDefault"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginTop">4dp</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:lineSpacingMultiplier">1.25</item> + </style> + + <style name="PermissionRationaleButtonContainer"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginTop">32dp</item> + <item name="android:orientation">horizontal</item> + <item name="android:gravity">end</item> + </style> + + <!-- END PERMISSION RATIONALE DIALOG --> + <!-- START PERMISSION REVIEW SCREEN --> <style name="PermissionReview"> diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java index 37acc9ef0..393ed8194 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java @@ -527,6 +527,7 @@ public class GrantPermissionsActivity extends SettingsActivity } } + // LINT.IfChange(dispatchTouchEvent) @Override public boolean dispatchTouchEvent(MotionEvent ev) { View rootView = getWindow().getDecorView(); @@ -544,6 +545,7 @@ public class GrantPermissionsActivity extends SettingsActivity } return super.dispatchTouchEvent(ev); } + // LINT.ThenChange(PermissionRationaleActivity.java:dispatchTouchEvent) @Override protected void onSaveInstanceState(@NonNull Bundle outState) { @@ -618,6 +620,11 @@ public class GrantPermissionsActivity extends SettingsActivity } @Override + public void onPermissionRationaleClicked(String groupName) { + mViewModel.showPermissionRationaleActivity(this, groupName); + } + + @Override public void onBackPressed() { if (mViewHandler == null) { return; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsViewHandler.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsViewHandler.java index 03f6d604c..6aaa836b7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsViewHandler.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsViewHandler.java @@ -57,6 +57,8 @@ public interface GrantPermissionsViewHandler { void onPermissionGrantResult(String groupName, List<String> affectedForegroundPermissions, @Result int result); + + void onPermissionRationaleClicked(String groupName); } /** diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt index e39a8fbad..883ff86d8 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt @@ -437,7 +437,7 @@ class GrantPermissionsViewHandlerImpl( val id = view.id if (id == R.id.permission_rationale_container) { - // TODO(b/256913489): Implement Permission rationale details activity + resultListener.onPermissionRationaleClicked(groupName) return } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/PermissionRationaleViewHandlerImpl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/PermissionRationaleViewHandlerImpl.kt new file mode 100644 index 000000000..10cf03793 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/PermissionRationaleViewHandlerImpl.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 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 com.android.permissioncontroller.permission.ui.handheld.v34 + +import android.app.Activity +import android.os.Build +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.transition.ChangeBounds +import android.transition.TransitionManager +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import android.view.WindowManager +import android.view.animation.AnimationUtils +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.RequiresApi +import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler +import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler.Result.Companion.CANCELLED + +/** + * Handheld implementation of [PermissionRationaleViewHandler]. Used for managing the presentation + * and user interaction of the "permission rationale" user interface. + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +class PermissionRationaleViewHandlerImpl( + private val mActivity: Activity, + private val resultListener: PermissionRationaleViewHandler.ResultListener +) : PermissionRationaleViewHandler, OnClickListener { + + private var groupName: String? = null + private var purposeMessage: CharSequence? = null + private var settingsMessage: CharSequence? = null + private var learnMoreMessage: CharSequence? = null + + private var rootView: ViewGroup? = null + private var purposeMessageView: TextView? = null + private var settingsMessageView: TextView? = null + private var learnMoreMessageView: TextView? = null + private var backButton: Button? = null + + override fun saveInstanceState(outState: Bundle) { + outState.putString(ARG_GROUP_NAME, groupName) + outState.putCharSequence(ARG_PURPOSE_MESSAGE, purposeMessage) + outState.putCharSequence(ARG_SETTINGS_MESSAGE, settingsMessage) + outState.putCharSequence(ARG_LEARN_MORE_MESSAGE, learnMoreMessage) + } + + override fun loadInstanceState(savedInstanceState: Bundle) { + groupName = savedInstanceState.getString(ARG_GROUP_NAME) + purposeMessage = savedInstanceState.getCharSequence(ARG_PURPOSE_MESSAGE) + settingsMessage = savedInstanceState.getCharSequence(ARG_SETTINGS_MESSAGE) + learnMoreMessage = savedInstanceState.getCharSequence(ARG_LEARN_MORE_MESSAGE) + } + + override fun updateUi( + groupName: String, + purposeMessage: CharSequence, + settingsMessage: CharSequence, + learnMoreMessage: CharSequence + ) { + this.groupName = groupName + this.purposeMessage = purposeMessage + this.settingsMessage = settingsMessage + this.learnMoreMessage = learnMoreMessage + + // If view already created, update all children + if (rootView != null) { + updateAll() + } + } + + private fun updateAll() { + updatePurposeMessage() + updateSettingsMessage() + updateLearnMoreMessage() + + // Animate change in size + // Grow or shrink the content container to size of new content + val growShrinkToNewContentSize = ChangeBounds() + growShrinkToNewContentSize.duration = ANIMATION_DURATION_MILLIS + growShrinkToNewContentSize.interpolator = AnimationUtils.loadInterpolator(mActivity, + android.R.interpolator.fast_out_slow_in) + TransitionManager.beginDelayedTransition(rootView, growShrinkToNewContentSize) + } + + override fun createView(): View { + // Make this activity be Non-IME target to prevent hiding keyboard flicker when it show up. + mActivity.window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + + val rootView = LayoutInflater.from(mActivity) + .inflate(R.layout.permission_rationale, null) as ViewGroup + + // Uses the vertical gravity of the PermissionGrantSingleton style to position the window + val gravity = rootView.requireViewById<LinearLayout>(R.id.grant_singleton).gravity + val verticalGravity = Gravity.VERTICAL_GRAVITY_MASK and gravity + mActivity.window.setGravity(Gravity.CENTER_HORIZONTAL or verticalGravity) + + // Cancel dialog + rootView.findViewById<View>(R.id.grant_singleton)!!.setOnClickListener(this) + // Swallow click event + rootView.findViewById<View>(R.id.grant_dialog)!!.setOnClickListener(this) + + purposeMessageView = rootView.findViewById(R.id.purpose_message) + purposeMessageView!!.movementMethod = LinkMovementMethod.getInstance() + + settingsMessageView = rootView.findViewById(R.id.settings_message) + settingsMessageView!!.movementMethod = LinkMovementMethod.getInstance() + + learnMoreMessageView = rootView.findViewById(R.id.learn_more_message) + learnMoreMessageView!!.movementMethod = LinkMovementMethod.getInstance() + + backButton = rootView.findViewById<Button>(R.id.back_button) + backButton!!.setOnClickListener(this) + + this.rootView = rootView + + // If ui model present, update all children + if (groupName != null) { + updateAll() + } + + return rootView + } + + override fun onClick(view: View) { + val id = view.id + + if (id == R.id.grant_singleton) { + onCancelled() + return + } + + if (id == R.id.back_button) { + onCancelled() + } + } + + override fun onBackPressed() { + onCancelled() + } + + override fun onCancelled() { + resultListener.onPermissionRationaleResult(groupName, CANCELLED) + } + + private fun updatePurposeMessage() { + if (purposeMessage == null) { + purposeMessageView!!.visibility = View.GONE + } else { + purposeMessageView!!.text = purposeMessage + purposeMessageView!!.visibility = View.VISIBLE + } + } + + private fun updateSettingsMessage() { + if (settingsMessage == null) { + settingsMessageView!!.visibility = View.GONE + } else { + settingsMessageView!!.text = settingsMessage + settingsMessageView!!.visibility = View.VISIBLE + } + } + + private fun updateLearnMoreMessage() { + if (learnMoreMessage == null) { + learnMoreMessageView!!.visibility = View.GONE + } else { + learnMoreMessageView!!.text = learnMoreMessage + learnMoreMessageView!!.visibility = View.VISIBLE + } + } + + companion object { + private val TAG = PermissionRationaleViewHandlerImpl::class.java.simpleName + + const val ARG_GROUP_NAME = "ARG_GROUP_NAME" + const val ARG_PURPOSE_MESSAGE = "ARG_PURPOSE_MESSAGE" + const val ARG_SETTINGS_MESSAGE = "ARG_SETTINGS_MESSAGE" + const val ARG_LEARN_MORE_MESSAGE = "ARG_LEARN_MORE_MESSAGE" + + // Animation parameters. + private const val ANIMATION_DURATION_MILLIS: Long = 200 + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/package-info.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/package-info.java new file mode 100644 index 000000000..48eb8661a --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/v34/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2022 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. + */ + +@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +package com.android.permissioncontroller.permission.ui.handheld.v34; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt index c6c5cef8e..0b311746f 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -118,6 +118,7 @@ import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandle import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT +import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity import com.android.permissioncontroller.permission.utils.AdminRestrictedPermissionsUtils import com.android.permissioncontroller.permission.utils.KotlinUtils import com.android.permissioncontroller.permission.utils.KotlinUtils.getDefaultPrecision @@ -1345,6 +1346,26 @@ class GrantPermissionsViewModel( } } + /** + * Shows the Permission Rationale Dialog. For use with U+ only, otherwise no-op. + * + * @param activity The current activity + * @param groupName The name of the permission group whose fragment should be opened + */ + fun showPermissionRationaleActivity(activity: Activity, groupName: String) { + if (!SdkLevel.isAtLeastU()) { + return + } + + val intent = Intent(activity, PermissionRationaleActivity::class.java).apply { + putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) + putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName) + putExtra(Constants.EXTRA_SESSION_ID, sessionId) + } + // TODO(b/260789748): setup similar result callback as the sendToSettingsFromLink method + activity.startActivity(intent) + } + private fun startAppPermissionFragment(activity: Activity, groupName: String) { val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSION) .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt new file mode 100644 index 000000000..c88e8a7b6 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 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 com.android.permissioncontroller.permission.ui.model.v34 + +import android.app.Application +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.core.util.Consumer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.permission.safetylabel.DataCategory +import com.android.permission.safetylabel.DataType +import com.android.permission.safetylabel.DataTypeConstants +import com.android.permission.safetylabel.SafetyLabel +import com.android.permissioncontroller.permission.data.SafetyLabelLiveData +import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData +import com.android.permissioncontroller.permission.utils.SafetyLabelPermissionMapping + +/** + * [ViewModel] for the [PermissionRationaleActivity]. Gets all information required safety label and + * links required to inform user of data sharing usages by the app when granting this permission + * + * @param app: The current application + * @param packageName: The packageName permissions are being requested for + * @param permissionGroupName: The permission group requested + * @param sessionId: A long to identify this session + * @param storedState: Previous state, if this activity was stopped and is being recreated + */ +class PermissionRationaleViewModel( + private val app: Application, + private val packageName: String, + private val permissionGroupName: String, + // TODO(b/259961958): add PermissionRationale metrics + private val sessionId: Long, + private val storedState: Bundle? +) : ViewModel() { + private val safetyLabelLiveData = SafetyLabelLiveData[packageName] + + var activityResultCallback: Consumer<Intent>? = null + + /** + * A class which represents a permission rationale for permission group, and messages which + * should be shown with it. + */ + data class PermissionRationaleInfo( + val groupName: String, + val installSourceName: String?, + val purposeSet: Set<Int> + ) + + /** A [LiveData] which holds the currently pending PermissionRationaleInfo */ + val permissionRationaleInfoLiveData = + object : SmartUpdateMediatorLiveData<PermissionRationaleInfo>() { + + init { + addSource(safetyLabelLiveData) { onUpdate() } + + // Load package state, if available + onUpdate() + } + + override fun onUpdate() { + if (safetyLabelLiveData.isStale) { + return + } + + val safetyLabel = safetyLabelLiveData.value + if (safetyLabel == null) { + Log.e(LOG_TAG, "Safety label for $packageName not found") + value = null + return + } + + // TODO(b/260144598): link to app store + value = + PermissionRationaleInfo( + permissionGroupName, + null, + getSafetyLabelSharingPurposesForGroup(safetyLabel, permissionGroupName)) + } + + private fun getSafetyLabelSharingPurposesForGroup( + safetyLabel: SafetyLabel, + groupName: String + ): Set<Int> { + val purposeSet = mutableSetOf<Int>() + val categoriesForPermission: List<String> = + SafetyLabelPermissionMapping.getCategoriesForPermissionGroup(groupName) + categoriesForPermission.forEach categoryLoop@{ category -> + val dataCategory: DataCategory? = safetyLabel.dataLabel.dataShared[category] + if (dataCategory == null) { + // Continue to next + return@categoryLoop + } + val typesForCategory = DataTypeConstants.getValidDataTypesForCategory(category) + typesForCategory.forEach typeLoop@{ type -> + val dataType: DataType? = dataCategory.dataTypes[type] + if (dataType == null) { + // Continue to next + return@typeLoop + } + if (dataType.purposeSet.isNotEmpty()) { + purposeSet.addAll(dataType.purposeSet) + } + } + } + + return purposeSet + } + } + + companion object { + private val LOG_TAG = PermissionRationaleViewModel::class.java.simpleName + } +} + +/** Factory for a [PermissionRationaleViewModel] */ +class PermissionRationaleViewModelFactory( + private val app: Application, + private val packageName: String, + private val permissionGroupName: String, + private val sessionId: Long, + private val savedState: Bundle? +) : ViewModelProvider.Factory { + override fun <T : ViewModel> create(modelClass: Class<T>): T { + @Suppress("UNCHECKED_CAST") + return PermissionRationaleViewModel( + app, packageName, permissionGroupName, sessionId, savedState) + as T + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/package-info.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/package-info.java new file mode 100644 index 000000000..4e3697cfc --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2022 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. + */ + +@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +package com.android.permissioncontroller.permission.ui.model.v34; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java new file mode 100644 index 000000000..b81c802da --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2022 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 com.android.permissioncontroller.permission.ui.v34; + +import static android.content.Intent.EXTRA_PERMISSION_GROUP_NAME; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ACCOUNT_MANAGEMENT; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ADVERTISING; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ANALYTICS; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_APP_FUNCTIONALITY; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_DEVELOPER_COMMUNICATIONS; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_FRAUD_PREVENTION_SECURITY; +import static com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_PERSONALIZATION; +import static com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler.Result.CANCELLED; + +import android.content.Intent; +import android.icu.lang.UCharacter; +import android.icu.text.ListFormatter; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.android.permission.safetylabel.DataPurposeConstants.Purpose; +import com.android.permissioncontroller.Constants; +import com.android.permissioncontroller.DeviceUtils; +import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.ui.SettingsActivity; +import com.android.permissioncontroller.permission.ui.handheld.v34.PermissionRationaleViewHandlerImpl; +import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel; +import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModel.PermissionRationaleInfo; +import com.android.permissioncontroller.permission.ui.model.v34.PermissionRationaleViewModelFactory; +import com.android.permissioncontroller.permission.utils.KotlinUtils; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * An activity which displays runtime permission rationale on behalf of an app. This activity is + * based on GrantPermissionActivity to keep view behavior and theming consistent. + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public class PermissionRationaleActivity extends SettingsActivity implements + PermissionRationaleViewHandler.ResultListener { + + private static final String LOG_TAG = PermissionRationaleActivity.class.getSimpleName(); + + private static final String KEY_SESSION_ID = PermissionRationaleActivity.class.getName() + + "_SESSION_ID"; + + /** Unique Id of a request. Inherited from GrantPermissionDialog if provide via intent extra */ + private long mSessionId; + /** Package that shall have permissions granted */ + private String mTargetPackage; + /** The permission group that initiated the permission rationale details activity */ + private String mPermissionGroupName; + /** The permission rationale info resulting from the specified permisison and group */ + private PermissionRationaleInfo mPermissionRationaleInfo; + + private PermissionRationaleViewHandler mViewHandler; + private PermissionRationaleViewModel mViewModel; + + private float mOriginalDimAmount; + private View mRootView; + + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (icicle == null) { + mSessionId = + getIntent().getLongExtra(Constants.EXTRA_SESSION_ID, new Random().nextLong()); + } else { + mSessionId = icicle.getLong(KEY_SESSION_ID); + } + + getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + + mPermissionGroupName = getIntent().getStringExtra(EXTRA_PERMISSION_GROUP_NAME); + if (mPermissionGroupName == null) { + Log.e( + LOG_TAG, + "null EXTRA_PERMISSION_GROUP_NAME. Must be set for permission rationale"); + finishAfterTransition(); + return; + } + + mTargetPackage = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); + if (mTargetPackage == null) { + Log.e(LOG_TAG, "null EXTRA_PACKAGE_NAME. Must be set for permission rationale"); + finishAfterTransition(); + return; + } + + setFinishOnTouchOutside(false); + + setTitle(R.string.permission_rationale_title); + + if (DeviceUtils.isTelevision(this) + || DeviceUtils.isWear(this) + || DeviceUtils.isAuto(this)) { + finishAfterTransition(); + } else { + mViewHandler = new PermissionRationaleViewHandlerImpl(this, this); + } + + PermissionRationaleViewModelFactory factory = new PermissionRationaleViewModelFactory( + getApplication(), mTargetPackage, mPermissionGroupName, mSessionId, icicle); + mViewModel = factory.create(PermissionRationaleViewModel.class); + mViewModel.getPermissionRationaleInfoLiveData() + .observe(this, this::onPermissionRationaleInfoLoad); + + mRootView = mViewHandler.createView(); + mRootView.setVisibility(View.GONE); + setContentView(mRootView); + Window window = getWindow(); + WindowManager.LayoutParams layoutParams = window.getAttributes(); + mOriginalDimAmount = layoutParams.dimAmount; + window.setAttributes(layoutParams); + + if (getResources().getBoolean(R.bool.config_useWindowBlur)) { + java.util.function.Consumer<Boolean> blurEnabledListener = enabled -> { + mViewHandler.onBlurEnabledChanged(window, enabled); + }; + mRootView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + window.getWindowManager().addCrossWindowBlurEnabledListener( + blurEnabledListener); + } + + @Override + public void onViewDetachedFromWindow(View v) { + window.getWindowManager().removeCrossWindowBlurEnabledListener( + blurEnabledListener); + } + }); + } + // Restore UI state after lifecycle events. This has to be before we show the first request, + // as the UI behaves differently for updates and initial creations. + if (icicle != null) { + mViewHandler.loadInstanceState(icicle); + } else { + // Do not show screen dim until data is loaded + window.setDimAmount(0f); + } + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + if (mViewHandler == null) { + return; + } + + mViewHandler.saveInstanceState(outState); + + outState.putLong(KEY_SESSION_ID, mSessionId); + } + + @Override + public void onBackPressed() { + if (mViewHandler == null) { + return; + } + mViewHandler.onBackPressed(); + } + + // LINT.IfChange(dispatchTouchEvent) + /** + * Used to dismiss dialog when tapping outside of dialog bounds + * Follows the same logic as GrantPermissionActivity + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + View rootView = getWindow().getDecorView(); + if (rootView.getTop() != 0) { + // We are animating the top view, need to compensate for that in motion events. + ev.setLocation(ev.getX(), ev.getY() - rootView.getTop()); + } + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + if ((x < 0) || (y < 0) || (x > (rootView.getWidth())) || (y > (rootView.getHeight()))) { + if (MotionEvent.ACTION_DOWN == ev.getAction()) { + mViewHandler.onCancelled(); + } + finishAfterTransition(); + } + return super.dispatchTouchEvent(ev); + } + // LINT.ThenChange(GrantPermissionsActivity.java:dispatchTouchEvent) + + @Override + public void onPermissionRationaleResult(@Nullable String groupName, int result) { + if (result == CANCELLED) { + finishAfterTransition(); + } + } + + private void onPermissionRationaleInfoLoad(PermissionRationaleInfo permissionRationaleInfo) { + if (permissionRationaleInfo == null) { + finishAfterTransition(); + return; + } + + mPermissionRationaleInfo = permissionRationaleInfo; + showPermissionRationale(); + } + + private void showPermissionRationale() { + String groupName = mPermissionRationaleInfo.getGroupName(); + String installSourceName = mPermissionRationaleInfo.getInstallSourceName(); + + List<String> purposesList = + new ArrayList<>(mPermissionRationaleInfo.getPurposeSet().size()); + for (@Purpose int purpose : mPermissionRationaleInfo.getPurposeSet()) { + purposesList.add(getStringForPurpose(purpose)); + } + + // TODO(b/260144215): update purposes join based on l18n feedback + String purposesString = ListFormatter.getInstance().format(purposesList); + + // TODO(b/260144598): link to app store + CharSequence purposeMessage; + if (installSourceName == null || installSourceName.isEmpty()) { + purposeMessage = + getString(R.string.permission_rationale_purpose_default_source_message, + purposesString); + } else { + purposeMessage = + getString(R.string.permission_rationale_purpose_message, + installSourceName, + purposesString); + } + + // TODO(b/260144330): link to permission settings + String permissionGroupLabel = + KotlinUtils.INSTANCE.getPermGroupLabel(this, groupName).toString(); + CharSequence settingsMessage = + getString(R.string.permission_rationale_permission_settings_message, + UCharacter.toLowerCase(permissionGroupLabel)); + + // TODO(b/259963582): link to safety label help center article + CharSequence learnMoreMessage = + getString(R.string.permission_rationale_permission_learn_more_title); + + mViewHandler.updateUi( + groupName, + purposeMessage, + settingsMessage, + learnMoreMessage + ); + + getWindow().setDimAmount(mOriginalDimAmount); + if (mRootView.getVisibility() == View.GONE) { + InputMethodManager manager = getSystemService(InputMethodManager.class); + manager.hideSoftInputFromWindow(mRootView.getWindowToken(), 0); + mRootView.setVisibility(View.VISIBLE); + } + } + + private String getStringForPurpose(@Purpose int purpose) { + switch (purpose) { + case PURPOSE_APP_FUNCTIONALITY: + return getString(R.string.permission_rational_purpose_app_functionality); + case PURPOSE_ANALYTICS: + return getString(R.string.permission_rational_purpose_analytics); + case PURPOSE_DEVELOPER_COMMUNICATIONS: + return getString(R.string.permission_rational_purpose_developer_communications); + case PURPOSE_FRAUD_PREVENTION_SECURITY: + return getString(R.string.permission_rational_purpose_fraud_prevention_security); + case PURPOSE_ADVERTISING: + return getString(R.string.permission_rational_purpose_advertising); + case PURPOSE_PERSONALIZATION: + return getString(R.string.permission_rational_purpose_personalization); + case PURPOSE_ACCOUNT_MANAGEMENT: + return getString(R.string.permission_rational_purpose_account_management); + default: + throw new IllegalArgumentException("Invalid purpose: " + purpose); + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleViewHandler.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleViewHandler.kt new file mode 100644 index 000000000..7b2295921 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleViewHandler.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 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 com.android.permissioncontroller.permission.ui.v34 + +import android.os.Build +import android.os.Bundle +import android.view.View +import android.view.Window +import androidx.annotation.IntDef +import androidx.annotation.RequiresApi + +/** + * Class for managing the presentation and user interaction of the "permission rationale" user + * interface. + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +interface PermissionRationaleViewHandler { + @Retention(AnnotationRetention.SOURCE) + @IntDef(Result.CANCELLED) + annotation class Result { + companion object { + const val CANCELLED = -1 + } + } + + /** + * Listener interface for getting notified when the user responds to a permission rationale + * user action. + */ + interface ResultListener { + fun onPermissionRationaleResult(groupName: String?, @Result result: Int) + } + + /** + * Creates and returns the view hierarchy that is managed by this view handler. This must be + * called before [.updateUi]. + */ + fun createView(): View + + /** + * Updates the view hierarchy to reflect the specified state. + * + * Note that this must be called at least once before showing the UI to the user to properly + * initialize the UI. + * + * @param groupName the name of the permission group + * @param purposeMessage the data usage purposes message to display the user + * @param settingsMessage the settings link message to display the user + * @param learnMoreMessage the more info about safety labels message to display the user + */ + fun updateUi( + groupName: String, + purposeMessage: CharSequence, + settingsMessage: CharSequence, + learnMoreMessage: CharSequence + ) + + /** + * Called by [PermissionRationaleActivity] to save the state of this view handler to the + * specified bundle. + */ + fun saveInstanceState(outState: Bundle) + + /** + * Called by [PermissionRationaleActivity] to load the state of this view handler from the + * specified bundle. + */ + fun loadInstanceState(savedInstanceState: Bundle) + + /** Gives a chance for handling the back key. */ + fun onBackPressed() + + /** + * Handles cancel event for the permission rationale dialog. + */ + fun onCancelled() {} + + /** + * Called by [PermissionRationaleActivity] to allow the handler to update the ui when blur is + * enabled/disabled. + */ + fun onBlurEnabledChanged(window: Window?, enabled: Boolean) {} +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/package-info.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/package-info.java new file mode 100644 index 000000000..0e68b52e0 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2022 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. + */ + +@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +package com.android.permissioncontroller.permission.ui.v34; |