[automerger skipped] Merge mainline-release 6664920 to stage-aosp-master - DO NOT MERGE am: 6ec6bf7c8b -s ours am: 0baa475205 -s ours am: b199a570d4 -s ours am skip reason: Change-Id I929cfc3549e4b5790a15b08f5a618d03f9987ae3 with SHA-1 3c04de1557 is in history Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/PackageInstaller/+/12579360 Change-Id: I75f435f0e0405038f231e40b1fe66f6e0e4de69f
diff --git a/Android.bp b/Android.bp index 7b50233..8d3b2de 100644 --- a/Android.bp +++ b/Android.bp
@@ -94,7 +94,7 @@ "SettingsLibProgressBar", "androidx.annotation_annotation", "permissioncontroller-statsd", - "car-ui-lib", + "car-ui-lib-overlayable", "libprotobuf-java-lite", "SettingsLibUtils", ],
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f8856ae..08d4d0b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml
@@ -45,6 +45,8 @@ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED" /> + <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> @@ -122,6 +124,7 @@ <action android:name="android.intent.action.MANAGE_APP_PERMISSION" /> <action android:name="android.intent.action.MANAGE_PERMISSION_APPS" /> <action android:name="android.intent.action.MANAGE_PERMISSIONS" /> + <action android:name="android.intent.action.REVIEW_PERMISSION_USAGE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> @@ -132,6 +135,7 @@ android:theme="@android:style/Theme.NoDisplay"> <intent-filter android:priority="1"> <action android:name="com.android.permissioncontroller.settingssearch.action.MANAGE_PERMISSION_APPS" /> + <action android:name="com.android.permissioncontroller.settingssearch.action.REVIEW_PERMISSION_USAGE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> @@ -160,6 +164,17 @@ android:excludeFromRecents="true" android:theme="@style/PermissionDialog.FilterTouches" /> + <activity android:name="com.android.permissioncontroller.permission.ui.ReviewOngoingUsageActivity" + android:excludeFromRecents="true" + android:theme="@style/PermissionDialog" + android:launchMode="singleInstance" + android:permission="android.permission.GRANT_RUNTIME_PERMISSIONS" > + <intent-filter android:priority="1"> + <action android:name="android.intent.action.REVIEW_ONGOING_PERMISSION_USAGE" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <activity android:name="com.android.permissioncontroller.permission.ui.ReviewAccessibilityServicesActivity" android:excludeFromRecents="true" android:theme="@style/PermissionDialog.FilterTouches"
diff --git a/res/layout/image_view.xml b/res/layout/image_view.xml index f2906b2..22d9726 100644 --- a/res/layout/image_view.xml +++ b/res/layout/image_view.xml
@@ -19,7 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" style="@style/ImageView"> - <com.android.permissioncontroller.permission.ui.PreferenceImageView + <com.android.permissioncontroller.permission.debug.PreferenceImageView android:id="@+id/icon" style="@style/ImageViewIcon" />
diff --git a/res/layout/ongoing_usage_dialog_content.xml b/res/layout/ongoing_usage_dialog_content.xml new file mode 100644 index 0000000..f9a0f07 --- /dev/null +++ b/res/layout/ongoing_usage_dialog_content.xml
@@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 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. +--> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + style="@style/PermissionUsageDialogContainerScrollView"> + + <LinearLayout + android:id="@+id/dialog_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + style="@style/PermissionUsageDialogContainerLayout"> + + <TextView + android:id="@+id/title" + style="@style/PermissionUsageDialogTitle"/> + + <LinearLayout + android:id="@+id/items_container" + style="@style/PermissionUsageDialogItemsContainer"/> + + <TextView + android:id="@+id/other_use_header" + android:text="@string/other_use" + style="@style/PermissionUsageDialogOtherUseHeader"/> + + <TextView + android:id="@+id/other_use_content" + style="@style/PermissionUsageDialogOtherUseContent"/> + + <View + android:id="@+id/other_use_inside_spacer" + style="@style/PermissionUsageDialogOtherUseInsideSpacer"/> + + <TextView + android:id="@+id/system_use_content" + style="@style/PermissionUsageDialogSystemUseContent"/> + + </LinearLayout> + +</ScrollView>
diff --git a/res/layout/ongoing_usage_dialog_item.xml b/res/layout/ongoing_usage_dialog_item.xml new file mode 100644 index 0000000..b18f152 --- /dev/null +++ b/res/layout/ongoing_usage_dialog_item.xml
@@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/PermissionUsageDialogItemContainer"> + + <FrameLayout + style="@style/PermissionUsageDialogItemIconFrame"> + + <ImageView + android:id="@+id/app_icon" + style="@style/PermissionUsageDialogItemIcon"/> + + </FrameLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/PermissionUsageDialogAppAndPermissions"> + + <TextView + android:id="@+id/app_name" + style="@style/PermissionUsageDialogItemAppName"/> + + <TextView + android:id="@+id/permissionsList" + style="@style/PermissionUsageDialogItemPermissionsList" + android:visibility="gone" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/icons" + style="@style/PermissionUsageDialogItemIconsContainer" + android:visibility="gone" /> + +</LinearLayout> \ No newline at end of file
diff --git a/res/layout/title_summary_image_view.xml b/res/layout/title_summary_image_view.xml index 31eaa1f..75b51e1 100644 --- a/res/layout/title_summary_image_view.xml +++ b/res/layout/title_summary_image_view.xml
@@ -20,7 +20,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> - <com.android.permissioncontroller.permission.ui.PreferenceImageView + <com.android.permissioncontroller.permission.debug.PreferenceImageView android:id="@+id/icon" style="@style/TitleSummaryImageViewIcon" />
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml index b90b743..aa683a4 100644 --- a/res/values/overlayable.xml +++ b/res/values/overlayable.xml
@@ -149,8 +149,14 @@ <item type="style" name="PermissionUsageDialogItemContainer" /> <item type="style" name="PermissionUsageDialogItemIconFrame" /> <item type="style" name="PermissionUsageDialogItemIcon" /> + <item type="style" name="PermissionUsageDialogAppAndPermissions" /> <item type="style" name="PermissionUsageDialogItemAppName" /> + <item type="style" name="PermissionUsageDialogItemPermissionsList" /> <item type="style" name="PermissionUsageDialogItemIconsContainer" /> + <item type="style" name="PermissionUsageDialogOtherUseHeader" /> + <item type="style" name="PermissionUsageDialogOtherUseContent" /> + <item type="style" name="PermissionUsageDialogOtherUseInsideSpacer" /> + <item type="style" name="PermissionUsageDialogSystemUseContent" /> <!-- END ONGOING USAGE DIALOG --> <!-- START REQUEST ROLE DIALOG TITLE -->
diff --git a/res/values/strings.xml b/res/values/strings.xml index 9180ca9..e7804ff 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml
@@ -287,9 +287,114 @@ <!-- Help URL, application permissions [DO NOT TRANSLATE] --> <string name="help_app_permissions" translatable="false"></string> + <!-- Title for permission usage [CHAR LIMIT=30] --> + <string name="permission_usage_title">Dashboard</string> + + <!-- Summary for showing a single permission access and the number of accesses [CHAR LIMIT=80] --> + <plurals name="permission_usage_summary"> + <item quantity="one">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> access</item> + <item quantity="other">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> accesses</item> + </plurals> + + <!-- Summary for showing a single permission access and the number of accesses, including those in the background [CHAR LIMIT=80] --> + <plurals name="permission_usage_summary_background"> + <item quantity="one">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> access (<xliff:g id="num" example="7">%3$s</xliff:g> in background)</item> + <item quantity="other">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> accesses (<xliff:g id="num" example="7">%3$s</xliff:g> in background)</item> + </plurals> + + <!-- Summary for showing a single permission access and the number of accesses [CHAR LIMIT=120] --> + <plurals name="permission_usage_summary_duration"> + <item quantity="one">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> access\nDuration: <xliff:g id="time" example="2 hours">%3$s</xliff:g></item> + <item quantity="other">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> accesses\nDuration: <xliff:g id="time" example="2 hours">%3$s</xliff:g></item> + </plurals> + + <!-- Summary for showing a single permission access and the number of accesses, including those in the background [CHAR LIMIT=120] --> + <plurals name="permission_usage_summary_background_duration"> + <item quantity="one">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> access (<xliff:g id="num" example="7">%3$s</xliff:g> in background)\nDuration: <xliff:g id="time" example="2 hours">%3$s</xliff:g></item> + <item quantity="other">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\n<xliff:g id="num" example="42">%2$s</xliff:g> accesses (<xliff:g id="num" example="7">%3$s</xliff:g> in background)\nDuration: <xliff:g id="time" example="2 hours">%3$s</xliff:g></item> + </plurals> + + <!-- Summary for showing a single permission access and the time of the last access when it was in the background [CHAR LIMIT=80] --> + <string name="permission_usage_summary_background">Last access: <xliff:g id="time" example="12:10 PM">%1$s</xliff:g>\nLast accessed in the background</string> + + <!-- Description for showing permission accesses with any permission [CHAR LIMIT=30] --> + <string name="permission_usage_any_permission">Any permission</string> + + <!-- Description for showing permission accesses accessed any time [CHAR LIMIT=30] --> + <string name="permission_usage_any_time">Any time</string> + + <!-- Description for showing permissions accessed in the last 7 days [CHAR LIMIT=30] --> + <string name="permission_usage_last_7_days">Last 7 days</string> + + <!-- Description for showing permissions accessed in the last day [CHAR LIMIT=30] --> + <string name="permission_usage_last_day">Last 24 hours</string> + + <!-- Description for showing permissions accessed in the last hour [CHAR LIMIT=30] --> + <string name="permission_usage_last_hour">Last 1 hour</string> + + <!-- Description for showing permissions accessed in the last 15 minutes [CHAR LIMIT=30] --> + <string name="permission_usage_last_15_minutes">Last 15 minutes</string> + + <!-- Description for showing permissions accessed in the last minute [CHAR LIMIT=30] --> + <string name="permission_usage_last_minute">Last 1 minute</string> + + <!-- Label when no apps have used the requested permissions [CHAR LIMIT=30] --> + <string name="no_permission_usages">No permission usages</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_any_time">Most recent access at any time</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_last_7_days">Most recent access in last 7 days</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_last_day">Most recent access in last 24 hours</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_last_hour">Most recent access in last 1 hour</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_last_15_minutes">Most recent access in last 15 minutes</string> + + <!-- Label for the title of the list of permission usages that shows which apps used which permissions[CHAR LIMIT=50] --> + <string name="permission_usage_list_title_last_minute">Most recent access in last 1 minute</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_any_time">Permission usage at any time</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_last_7_days">Permission usage in last 7 days</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_last_day">Permission usage in last 24 hours</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_last_hour">Permission usage in last 1 hour</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_last_15_minutes">Permission usage in last 15 minutes</string> + + <!-- Label for the title of the permission bar chart showing how often the most common permissions are used [CHAR LIMIT=50] --> + <string name="permission_usage_bar_chart_title_last_minute">Permission usage in last 1 minute</string> + + <!-- Label for the bars on the chart that shows how many apps have used various permissions [CHAR LIMIT=10] --> + <plurals name="permission_usage_bar_label"> + <item quantity="one">1 app</item> + <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> apps</item> + </plurals> + + <!-- Label for the button to bring the user to view the details of recent permission accesses [CHAR LIMIT=42] --> + <string name="permission_usage_view_details">See all in Dashboard</string> + <!-- DO NOT TRANSLATE Summary placeholder --> <string name="summary_placeholder" translatable="false"> </string> + <!-- Label for filtered view that shows permission usages of a single permission [CHAR LIMIT=40] --> + <string name="app_permission_usage_filter_label">Filtered by: <xliff:g id="perm" example="Location">%1$s</xliff:g> </string> + + <!-- Label for the text that removes the filter by permission to view all usages [CHAR LIMIT=none] --> + <string name="app_permission_usage_remove_filter">Remove filter</string> + <!-- Label for the title of the dialog allowing filtering by permissions [CHAR LIMIT=none] --> <string name="filter_by_title">Filter by</string> @@ -299,9 +404,45 @@ <!-- Label for the menu item allowing filtering by time [CHAR LIMIT=none] --> <string name="filter_by_time">Filter by time</string> + <!-- Label for sorting usages by the number of permissions used [CHAR LIMIT=30] --> + <string name="sort_spinner_most_permissions">Most permissions</string> + + <!-- Label for sorting usages by the number of accesses [CHAR LIMIT=30] --> + <string name="sort_spinner_most_accesses">Most accesses</string> + + <!-- Label for sorting usages by the most recent accesses [CHAR LIMIT=30] --> + <string name="sort_spinner_recent">Recent</string> + + <!-- Label for sorting usages by which app used a permission most recently [CHAR LIMIT=30] --> + <string name="sort_by_app">Sort by app usage</string> + + <!-- Label for sorting usages by access time [CHAR LIMIT=30] --> + <string name="sort_by_time">Sort by time</string> + <!-- Separator for a list of items. Include spaces before and after if needed [CHAR LIMIT=10] --> <string name="item_separator">,\u0020</string> + <!-- Label for refreshing the list of permission usages. [CHAR LIMIT=30] --> + <string name="permission_usage_refresh">Refresh</string> + + <!-- Subtitle for showing how many apps have accessed a given permission [CHAR LIMIT=20] --> + <plurals name="permission_usage_permission_filter_subtitle"> + <item quantity="one">1 app</item> + <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> apps</item> + </plurals> + + <!-- Help URL, permission usage [DO NOT TRANSLATE] --> + <string name="help_permission_usage" translatable="false"></string> + + <!-- Title for permission usage [CHAR LIMIT=30] --> + <string name="app_permission_usage_title">App permissions usage</string> + + <!-- Summary for an app's use of a permission [CHAR LIMIT=none] --> + <string name="app_permission_usage_summary">Access: <xliff:g id="num" example="2">%1$s</xliff:g> times. Total duration: <xliff:g id="duration" example="2 hours">%2$s</xliff:g>. Last used <xliff:g id="time" example="2 hours">%3$s</xliff:g> ago.</string> + + <!-- Summary for an app's use of a permission without duration [CHAR LIMIT=none] --> + <string name="app_permission_usage_summary_no_duration">Access: <xliff:g id="num" example="2">%1$s</xliff:g> times. Last used <xliff:g id="time" example="2 hours">%2$s</xliff:g> ago.</string> + <!-- Title for the dialog button to allow a permission grant when you cannot only allow in the foreground. [CHAR LIMIT=60] --> <string name="app_permission_button_allow">Allow</string> @@ -335,6 +476,9 @@ <!-- Text for linking to the page that shows the apps with a given permission [CHAR LIMIT=none] --> <string name="app_permission_footer_permission_apps_link">See all apps with this permission</string> + <!-- Label for the assistant mic display switch [CHAR LIMIT=60] --> + <string name="assistant_mic_label">Show assistant microphone usage</string> + <!-- Label for the auto revoke switch [CHAR LIMIT=60] --> <string name="auto_revoke_label">Remove permissions if app isn\u2019t used</string> @@ -740,9 +884,41 @@ <!-- Label for the button to set an application as the default application [CHAR LIMIT=20] --> <string name="request_role_set_as_default">Set as default</string> + <!-- Message telling the user that a phone call is currently using the microphone [CHAR LIMIT=none] --> + <string name="phone_call_uses_microphone">Microphone is used in <b>phone call</b></string> + <!-- Message telling the user that a phone call is currently using the microphone and the camera [CHAR LIMIT=none] --> + <string name="phone_call_uses_microphone_and_camera">Camera and Microphone are used in <b>video call</b></string> + <!-- Message telling the user that a phone call is currently using the camera [CHAR LIMIT=none] --> + <string name="phone_call_uses_camera">Camera is used in <b>video call</b></string> + + <!-- Message telling the user that a system service is currently using the microphone [CHAR LIMIT=none] --> + <string name="system_uses_microphone">Microphone is accessed using system service</string> + <!-- Message telling the user that a system service is currently using the microphone and the camera [CHAR LIMIT=none] --> + <string name="system_uses_microphone_and_camera">Camera and Microphone are accessed using system service</string> + <!-- Message telling the user that a system service is currently using the camera [CHAR LIMIT=none] --> + <string name="system_uses_camera">Camera is accessed using system service</string> + + <!-- Line above a list of other apps and system service that are currently microphone or camera [CHAR LIMIT=60] --> + <string name="other_use">Other use:</string> + + <!-- Action for accepting the Ongoing usage dialog [CHAR LIMIT=10]--> + <string name="ongoing_usage_dialog_ok">Got it</string> + + <!-- Title for Ongoing usage dialog title when multiple apps are using app ops [CHAR LIMIT=NONE] --> + <string name="ongoing_usage_dialog_title">Recent use of <xliff:g id="types_list" example="camera( and location)">%s</xliff:g></string> + + <!-- Separator for permissions. Include spaces before and after if needed [CHAR LIMIT=10] --> + <string name="ongoing_usage_dialog_separator">,\u0020</string> + + <!-- Separator for permissions, before last type. Include spaces before and after if needed [CHAR LIMIT=10] --> + <string name="ongoing_usage_dialog_last_separator">\u0020and\u0020</string> + <!-- Keyword in the Settings app's search functionality that can be used to find links to the default app management screens [CHAR LIMIT=none] --> <string name="default_app_search_keyword">default apps</string> + <!-- List of the two permission group names for microphone (android:string/permgrouplab_microphone) and camera (android:string/permgrouplab_camera) [CHAR LIMIT=60]--> + <string name="permgroup_list_microphone_and_camera">Microphone & Camera</string> + <!-- Accessibility label for button that opens a settings screen [CHAR LIMIT=NONE] --> <string name="settings_button">Settings</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml index 188c29a..c1c8e2d 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml
@@ -630,24 +630,63 @@ <item name="android:layout_gravity">center</item> </style> - <style name="PermissionUsageDialogItemAppName" - parent="@android:style/TextAppearance.DeviceDefault"> + <style name="PermissionUsageDialogAppAndPermissions"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_weight">1</item> + <item name="android:orientation">vertical</item> <item name="android:gravity">start|center_vertical</item> + </style> + + <style name="PermissionUsageDialogItemAppName" + parent="@android:style/TextAppearance.DeviceDefault"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> <item name="android:textDirection">locale</item> <item name="android:textSize">16sp</item> <item name="android:layout_marginStart">16dp</item> </style> + <style name="PermissionUsageDialogItemPermissionsList"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:textColor">?android:textColorSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + <style name="PermissionUsageDialogItemIconsContainer"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">match_parent</item> <item name="android:orientation">horizontal</item> <item name="android:gravity">end|center_vertical</item> <item name="android:layout_gravity">end</item> - <item name="android:visibility">gone</item> + </style> + + <style name="PermissionUsageDialogOtherUseHeader"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + + <style name="PermissionUsageDialogOtherUseContent"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + + <style name="PermissionUsageDialogOtherUseInsideSpacer"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_height">16dp</item> + </style> + + <style name="PermissionUsageDialogSystemUseContent"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> </style> <!-- END ONGOING USAGE DIALOG -->
diff --git a/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt b/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt index 7f0a104..71cb3ef 100644 --- a/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt +++ b/src/com/android/permissioncontroller/permission/data/AppPermGroupUiInfoLiveData.kt
@@ -54,6 +54,7 @@ ) : SmartUpdateMediatorLiveData<AppPermGroupUiInfo>(), LocationUtils.LocationListener { private var isSpecialLocation = false + private val isMicrophone = permGroupName == Manifest.permission_group.MICROPHONE private val packageInfoLiveData = LightPackageInfoLiveData[packageName, user] private val permGroupLiveData = PermGroupLiveData[permGroupName] private val permissionStateLiveData = PermStateLiveData[packageName, permGroupName, user] @@ -294,7 +295,6 @@ override fun onActive() { super.onActive() - if (isSpecialLocation) { LocationUtils.addLocationListener(this) updateIfActive()
diff --git a/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt b/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt new file mode 100644 index 0000000..3d03bde --- /dev/null +++ b/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt
@@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 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.data + +import android.app.AppOpsManager +import android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED +import android.app.Application +import android.os.UserHandle +import android.util.Log +import com.android.permissioncontroller.PermissionControllerApplication +import kotlinx.coroutines.Job + +/** + * LiveData that loads the last usage of each of a list of app ops for every package. + * + * <p>For app-ops with duration the end of the access is considered. + * + * @param app The current application + * @param opNames The names of the app ops we wish to search for + * @param usageDurationMs how much ago can an access have happened to be considered + */ +// TODO: listen for updates +class OpUsageLiveData( + private val app: Application, + private val opNames: List<String>, + private val usageDurationMs: Long +) : SmartAsyncMediatorLiveData<@JvmSuppressWildcards Map<String, List<OpAccess>>>() { + val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! + + override suspend fun loadDataAndPostValue(job: Job) { + val now = System.currentTimeMillis() + val opMap = mutableMapOf<String, MutableList<OpAccess>>() + + val packageOps = appOpsManager.getPackagesForOps(opNames.toTypedArray()) + for (packageOp in packageOps) { + for (opEntry in packageOp.ops) { + val user = UserHandle.getUserHandleForUid(packageOp.uid) + val lastAccessTime: Long = opEntry.getLastAccessTime(OP_FLAGS_ALL_TRUSTED) + + if (lastAccessTime == -1L) { + // There was no access, so skip + continue + } + + var lastAccessDuration = opEntry.getLastDuration(OP_FLAGS_ALL_TRUSTED) + + // Some accesses have no duration + if (lastAccessDuration == -1L) { + lastAccessDuration = 0 + } + + if (opEntry.isRunning || + lastAccessTime + lastAccessDuration > (now - usageDurationMs)) { + val accessList = opMap.getOrPut(opEntry.opStr) { mutableListOf() } + val accessTime = if (opEntry.isRunning) { + -1 + } else { + lastAccessTime + } + accessList.add(OpAccess(packageOp.packageName, user, accessTime)) + // TODO ntmyren: remove logs once b/160724034 is fixed + Log.i("OpUsageLiveData", "adding ${opEntry.opStr} for " + + "${packageOp.packageName}, access time of $lastAccessTime, isRunning: " + + "${opEntry.isRunning} current time $now, duration $lastAccessDuration") + } else { + // TODO ntmyren: remove logs once b/160724034 is fixed + Log.i("OpUsageLiveData", "NOT adding ${opEntry.opStr} for " + + "${packageOp.packageName}, access time of $lastAccessTime, isRunning: " + + "${opEntry.isRunning} current time $now, duration $lastAccessDuration") + } + } + } + + postValue(opMap) + } + + override fun onActive() { + super.onActive() + updateAsync() + } + + companion object : DataRepository<Pair<List<String>, Long>, OpUsageLiveData>() { + override fun newValue(key: Pair<List<String>, Long>): OpUsageLiveData { + return OpUsageLiveData(PermissionControllerApplication.get(), key.first, key.second) + } + + operator fun get(ops: List<String>, usageDurationMs: Long): OpUsageLiveData { + return get(ops to usageDurationMs) + } + } +} + +data class OpAccess(val packageName: String?, val user: UserHandle?, val lastAccessTime: Long) { + companion object { + const val IS_RUNNING = -1L + } + + fun isRunning() = lastAccessTime == IS_RUNNING +}
diff --git a/src/com/android/permissioncontroller/permission/debug/ExpandablePreferenceGroup.java b/src/com/android/permissioncontroller/permission/debug/ExpandablePreferenceGroup.java new file mode 100644 index 0000000..676a63c --- /dev/null +++ b/src/com/android/permissioncontroller/permission/debug/ExpandablePreferenceGroup.java
@@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 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.debug; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceViewHolder; + +import com.android.permissioncontroller.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A preference group that expands/collapses its children when clicked. + */ +public class ExpandablePreferenceGroup extends PreferenceGroup { + private @NonNull Context mContext; + private @NonNull List<Preference> mPreferences; + private @NonNull List<Pair<Integer, CharSequence>> mSummaryIcons; + private boolean mExpanded; + + public ExpandablePreferenceGroup(@NonNull Context context) { + super(context, null); + + mContext = context; + mPreferences = new ArrayList<>(); + mSummaryIcons = new ArrayList<>(); + mExpanded = false; + + setLayoutResource(R.layout.preference_usage); + setWidgetLayoutResource(R.layout.image_view); + setOnPreferenceClickListener(preference -> { + if (!mExpanded) { + int numPreferences = mPreferences.size(); + for (int i = 0; i < numPreferences; i++) { + super.addPreference(mPreferences.get(i)); + } + } else { + removeAll(); + } + mExpanded = !mExpanded; + return true; + }); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + ImageView icon = (ImageView) holder.findViewById(android.R.id.icon); + int rightIconSize = mContext.getResources().getDimensionPixelSize( + R.dimen.secondary_app_icon_size); + icon.setMaxWidth(rightIconSize); + icon.setMaxHeight(rightIconSize); + + super.onBindViewHolder(holder); + + TextView summary = (TextView) holder.findViewById(android.R.id.summary); + summary.setMaxLines(1); + summary.setEllipsize(TextUtils.TruncateAt.END); + + ImageView rightImageView = holder.findViewById( + android.R.id.widget_frame).findViewById(R.id.icon); + if (mExpanded) { + rightImageView.setImageResource(R.drawable.ic_arrow_up); + } else { + rightImageView.setImageResource(R.drawable.ic_arrow_down); + } + + holder.setDividerAllowedAbove(false); + holder.setDividerAllowedBelow(false); + + holder.findViewById(R.id.title_widget_frame).setVisibility(View.GONE); + + ViewGroup summaryFrame = (ViewGroup) holder.findViewById(R.id.summary_widget_frame); + if (mSummaryIcons.isEmpty()) { + summaryFrame.setVisibility(View.GONE); + } else { + summaryFrame.removeAllViews(); + int numIcons = mSummaryIcons.size(); + for (int i = 0; i < numIcons; i++) { + LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); + ViewGroup group = (ViewGroup) inflater.inflate(R.layout.title_summary_image_view, + null); + ImageView imageView = group.requireViewById(R.id.icon); + Pair<Integer, CharSequence> summaryIcons = mSummaryIcons.get(i); + imageView.setImageResource(summaryIcons.first); + if (summaryIcons.second != null) { + imageView.setContentDescription(summaryIcons.second); + } + summaryFrame.addView(group); + } + } + } + + @Override + public boolean addPreference(Preference preference) { + mPreferences.add(preference); + return true; + } + + /** + * Show the given icon next to this preference's summary. + * + * @param resId the resourceId of the drawable to use as the icon. + */ + public void addSummaryIcon(@DrawableRes int resId, @Nullable CharSequence contentDescription) { + mSummaryIcons.add(Pair.create(resId, contentDescription)); + } +}
diff --git a/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragment.java b/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragment.java new file mode 100644 index 0000000..0888b63 --- /dev/null +++ b/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragment.java
@@ -0,0 +1,1087 @@ +/* + * Copyright (C) 2020 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.debug; + +import static android.Manifest.permission_group.CAMERA; +import static android.Manifest.permission_group.LOCATION; +import static android.Manifest.permission_group.MICROPHONE; + +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MINUTES; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.Html; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import android.widget.TextView; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + +import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.model.AppPermissionUsage; +import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; +import com.android.permissioncontroller.permission.model.AppPermissionGroup; +import com.android.permissioncontroller.permission.model.legacy.PermissionApps; +import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; +import com.android.permissioncontroller.permission.ui.handheld.PermissionControlPreference; +import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader; +import com.android.permissioncontroller.permission.utils.Utils; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.widget.ActionBarShadowController; +import com.android.settingslib.widget.BarChartInfo; +import com.android.settingslib.widget.BarChartPreference; +import com.android.settingslib.widget.BarViewInfo; + +import java.lang.annotation.Retention; +import java.text.Collator; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Show the usage of all apps of all permission groups. + * + * <p>Shows a filterable list of app usage of permission groups, each of which links to + * AppPermissionsFragment. + */ +public class PermissionUsageFragment extends SettingsWithLargeHeader implements + PermissionUsages.PermissionsUsagesChangeCallback { + private static final String LOG_TAG = "PermissionUsageFragment"; + + @Retention(SOURCE) + @IntDef(value = {SORT_RECENT, SORT_RECENT_APPS}) + @interface SortOption {} + static final int SORT_RECENT = 1; + static final int SORT_RECENT_APPS = 2; + + private static final int MENU_SORT_BY_APP = MENU_HIDE_SYSTEM + 1; + private static final int MENU_SORT_BY_TIME = MENU_HIDE_SYSTEM + 2; + private static final int MENU_FILTER_BY_PERMISSIONS = MENU_HIDE_SYSTEM + 3; + private static final int MENU_FILTER_BY_TIME = MENU_HIDE_SYSTEM + 4; + private static final int MENU_REFRESH = MENU_HIDE_SYSTEM + 5; + + private static final String KEY_SHOW_SYSTEM_PREFS = "_show_system"; + private static final String SHOW_SYSTEM_KEY = PermissionUsageFragment.class.getName() + + KEY_SHOW_SYSTEM_PREFS; + private static final String KEY_PERM_NAME = "_perm_name"; + private static final String PERM_NAME_KEY = PermissionUsageFragment.class.getName() + + KEY_PERM_NAME; + private static final String KEY_TIME_INDEX = "_time_index"; + private static final String TIME_INDEX_KEY = PermissionUsageFragment.class.getName() + + KEY_TIME_INDEX; + private static final String KEY_SORT = "_sort"; + private static final String SORT_KEY = PermissionUsageFragment.class.getName() + + KEY_SORT; + + /** + * The maximum number of columns shown in the bar chart. + */ + private static final int MAXIMUM_NUM_BARS = 4; + + private @NonNull PermissionUsages mPermissionUsages; + private @Nullable List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>(); + + private Collator mCollator; + + private @NonNull List<TimeFilterItem> mFilterTimes; + private int mFilterTimeIndex; + private String mFilterGroup; + private @SortOption int mSort; + + private boolean mShowSystem; + private boolean mHasSystemApps; + private MenuItem mShowSystemMenu; + private MenuItem mHideSystemMenu; + private MenuItem mSortByApp; + private MenuItem mSortByTime; + + private ArrayMap<String, Integer> mGroupAppCounts = new ArrayMap<>(); + + private boolean mFinishedInitialLoad; + + /** + * @return A new fragment + */ + public static @NonNull PermissionUsageFragment newInstance(@Nullable String groupName, + long numMillis) { + PermissionUsageFragment fragment = new PermissionUsageFragment(); + Bundle arguments = new Bundle(); + if (groupName != null) { + arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName); + } + arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mFinishedInitialLoad = false; + mSort = SORT_RECENT_APPS; + mFilterGroup = null; + initializeTimeFilter(); + if (savedInstanceState != null) { + mShowSystem = savedInstanceState.getBoolean(SHOW_SYSTEM_KEY); + mFilterGroup = savedInstanceState.getString(PERM_NAME_KEY); + mFilterTimeIndex = savedInstanceState.getInt(TIME_INDEX_KEY); + mSort = savedInstanceState.getInt(SORT_KEY); + } + + setLoading(true, false); + setHasOptionsMenu(true); + ActionBar ab = getActivity().getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + if (mFilterGroup == null) { + mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); + } + + Context context = getPreferenceManager().getContext(); + mCollator = Collator.getInstance( + context.getResources().getConfiguration().getLocales().get(0)); + mPermissionUsages = new PermissionUsages(context); + + reloadData(); + } + + @Override + public void onStart() { + super.onStart(); + getActivity().setTitle(R.string.permission_usage_title); + } + + /** + * Initialize the time filter to show the smallest entry greater than the time passed in as an + * argument. If nothing is passed, this simply initializes the possible values. + */ + private void initializeTimeFilter() { + Context context = getPreferenceManager().getContext(); + mFilterTimes = new ArrayList<>(); + mFilterTimes.add(new TimeFilterItem(Long.MAX_VALUE, + context.getString(R.string.permission_usage_any_time), + R.string.permission_usage_list_title_any_time, + R.string.permission_usage_bar_chart_title_any_time)); + mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(7), + context.getString(R.string.permission_usage_last_7_days), + R.string.permission_usage_list_title_last_7_days, + R.string.permission_usage_bar_chart_title_last_7_days)); + mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(1), + context.getString(R.string.permission_usage_last_day), + R.string.permission_usage_list_title_last_day, + R.string.permission_usage_bar_chart_title_last_day)); + mFilterTimes.add(new TimeFilterItem(HOURS.toMillis(1), + context.getString(R.string.permission_usage_last_hour), + R.string.permission_usage_list_title_last_hour, + R.string.permission_usage_bar_chart_title_last_hour)); + mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(15), + context.getString(R.string.permission_usage_last_15_minutes), + R.string.permission_usage_list_title_last_15_minutes, + R.string.permission_usage_bar_chart_title_last_15_minutes)); + mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(1), + context.getString(R.string.permission_usage_last_minute), + R.string.permission_usage_list_title_last_minute, + R.string.permission_usage_bar_chart_title_last_minute)); + + long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS); + long supremum = Long.MAX_VALUE; + int supremumIndex = -1; + int numTimes = mFilterTimes.size(); + for (int i = 0; i < numTimes; i++) { + long curTime = mFilterTimes.get(i).getTime(); + if (curTime >= numMillis && curTime <= supremum) { + supremum = curTime; + supremumIndex = i; + } + } + if (supremumIndex != -1) { + mFilterTimeIndex = supremumIndex; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(SHOW_SYSTEM_KEY, mShowSystem); + outState.putString(PERM_NAME_KEY, mFilterGroup); + outState.putInt(TIME_INDEX_KEY, mFilterTimeIndex); + outState.putInt(SORT_KEY, mSort); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + mSortByApp = menu.add(Menu.NONE, MENU_SORT_BY_APP, Menu.NONE, R.string.sort_by_app); + mSortByTime = menu.add(Menu.NONE, MENU_SORT_BY_TIME, Menu.NONE, R.string.sort_by_time); + menu.add(Menu.NONE, MENU_FILTER_BY_PERMISSIONS, Menu.NONE, R.string.filter_by_permissions); + menu.add(Menu.NONE, MENU_FILTER_BY_TIME, Menu.NONE, R.string.filter_by_time); + if (mHasSystemApps) { + mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, + R.string.menu_show_system); + mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, + R.string.menu_hide_system); + } + + HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_permission_usage, + getClass().getName()); + MenuItem refresh = menu.add(Menu.NONE, MENU_REFRESH, Menu.NONE, + R.string.permission_usage_refresh); + refresh.setIcon(R.drawable.ic_refresh); + refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + updateMenu(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getActivity().finish(); + return true; + case MENU_SORT_BY_APP: + mSort = SORT_RECENT_APPS; + updateUI(); + updateMenu(); + break; + case MENU_SORT_BY_TIME: + mSort = SORT_RECENT; + updateUI(); + updateMenu(); + break; + case MENU_FILTER_BY_PERMISSIONS: + showPermissionFilterDialog(); + break; + case MENU_FILTER_BY_TIME: + showTimeFilterDialog(); + break; + case MENU_SHOW_SYSTEM: + case MENU_HIDE_SYSTEM: + mShowSystem = item.getItemId() == MENU_SHOW_SYSTEM; + // We already loaded all data, so don't reload + updateUI(); + updateMenu(); + break; + case MENU_REFRESH: + reloadData(); + break; + } + return super.onOptionsItemSelected(item); + } + + private void updateMenu() { + if (mHasSystemApps) { + /* Do not show system apps for now + mShowSystemMenu.setVisible(!mShowSystem); + mHideSystemMenu.setVisible(mShowSystem); + */ + mShowSystemMenu.setVisible(false); + mHideSystemMenu.setVisible(false); + } + + mSortByApp.setVisible(mSort != SORT_RECENT_APPS); + mSortByTime.setVisible(mSort != SORT_RECENT); + } + + @Override + public void onPermissionUsagesChanged() { + if (mPermissionUsages.getUsages().isEmpty()) { + return; + } + mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); + + // Ensure the group name is valid. + if (getGroup(mFilterGroup) == null) { + mFilterGroup = null; + } + + updateUI(); + } + + @Override + public int getEmptyViewString() { + return R.string.no_permission_usages; + } + + private void updateUI() { + if (mAppPermissionUsages.isEmpty() || getActivity() == null) { + return; + } + Context context = getActivity(); + + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + screen = getPreferenceManager().createPreferenceScreen(context); + setPreferenceScreen(screen); + } + screen.removeAll(); + + Preference countsWarningPreference = new Preference(getContext()) { + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + ((TextView) holder.itemView.findViewById(android.R.id.title)) + .setTextColor(Color.RED); + holder.itemView.setBackgroundColor(Color.YELLOW); + } + }; + + StringBuffer accounts = new StringBuffer(); + for (UserHandle user : getContext().getSystemService(UserManager.class).getAllProfiles()) { + for (Account account : getContext().createContextAsUser(user, 0).getSystemService(AccountManager.class).getAccounts()) { + accounts.append(", " + account.name); + } + } + if (accounts.length() > 0) { + accounts.delete(0, 2); + } + + countsWarningPreference.setTitle(Html.fromHtml("<b>INTERNAL ONLY</b> - For debugging.<br/><br/>" + + "- Access counts do not reflect amount of private data accessed.<br/>" + + "- Data might not be accurate.<br/><br/>" + + "Accounts: " + accounts, Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH)); + countsWarningPreference.setIcon(R.drawable.ic_info); + screen.addPreference(countsWarningPreference); + + boolean seenSystemApp = false; + + final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex); + long curTime = System.currentTimeMillis(); + long startTime = Math.max(timeFilterItem == null ? 0 : (curTime - timeFilterItem.getTime()), + Instant.EPOCH.toEpochMilli()); + + List<Pair<AppPermissionUsage, GroupUsage>> usages = new ArrayList<>(); + mGroupAppCounts.clear(); + ArrayList<PermissionApp> permApps = new ArrayList<>(); + int numApps = mAppPermissionUsages.size(); + for (int appNum = 0; appNum < numApps; appNum++) { + AppPermissionUsage appUsage = mAppPermissionUsages.get(appNum); + boolean used = false; + List<GroupUsage> appGroups = appUsage.getGroupUsages(); + int numGroups = appGroups.size(); + for (int groupNum = 0; groupNum < numGroups; groupNum++) { + GroupUsage groupUsage = appGroups.get(groupNum); + long lastAccessTime = groupUsage.getLastAccessTime(); + + if (groupUsage.getAccessCount() <= 0) { + continue; + } + if (lastAccessTime == 0) { + Log.w(LOG_TAG, + "Unexpected access time of 0 for " + appUsage.getApp().getKey() + " " + + groupUsage.getGroup().getName()); + continue; + } + if (lastAccessTime < startTime) { + continue; + } + final boolean isSystemApp = !Utils.isGroupOrBgGroupUserSensitive( + groupUsage.getGroup()); + seenSystemApp = seenSystemApp || isSystemApp; + if (isSystemApp && !mShowSystem) { + continue; + } + + used = true; + addGroupUser(groupUsage.getGroup().getName()); + + // Filter out usages that aren't of the filtered permission group. + // We do this after we call addGroupUser so we compute the correct usage counts + // for the permission filter dialog but before we add the usage to our list. + if (mFilterGroup != null && !mFilterGroup.equals(groupUsage.getGroup().getName())) { + continue; + } + + usages.add(Pair.create(appUsage, appGroups.get(groupNum))); + } + if (used) { + permApps.add(appUsage.getApp()); + addGroupUser(null); + } + } + + if (mHasSystemApps != seenSystemApp) { + mHasSystemApps = seenSystemApp; + getActivity().invalidateOptionsMenu(); + } + + // Update header. + if (mFilterGroup == null) { + screen.addPreference(createBarChart(usages, timeFilterItem, context)); + hideHeader(); + } else { + AppPermissionGroup group = getGroup(mFilterGroup); + if (group != null) { + setHeader(Utils.applyTint(context, context.getDrawable(group.getIconResId()), + android.R.attr.colorControlNormal), + context.getString(R.string.app_permission_usage_filter_label, + group.getLabel()), null, null, true); + setSummary(context.getString(R.string.app_permission_usage_remove_filter), v -> { + onPermissionGroupSelected(null); + }); + } + } + + // Add the preference header. + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + if (timeFilterItem != null) { + category.setTitle(timeFilterItem.getListTitleRes()); + } + + // Sort the apps. + if (mSort == SORT_RECENT) { + usages.sort(PermissionUsageFragment::compareAccessRecency); + } else if (mSort == SORT_RECENT_APPS) { + if (mFilterGroup == null) { + usages.sort(PermissionUsageFragment::compareAccessAppRecency); + } else { + usages.sort(PermissionUsageFragment::compareAccessTime); + } + } else { + Log.w(LOG_TAG, "Unexpected sort option: " + mSort); + } + + // If there are no entries, don't show anything. + if (usages.isEmpty()) { + screen.removeAll(); + } + + new PermissionApps.AppDataLoader(context, () -> { + ExpandablePreferenceGroup parent = null; + AppPermissionUsage lastAppPermissionUsage = null; + String lastAccessTimeString = null; + List<CharSequence> groups = new ArrayList<>(); + + final int numUsages = usages.size(); + for (int usageNum = 0; usageNum < numUsages; usageNum++) { + final Pair<AppPermissionUsage, GroupUsage> usage = usages.get(usageNum); + AppPermissionUsage appPermissionUsage = usage.first; + GroupUsage groupUsage = usage.second; + + String accessTimeString = UtilsKt.getAbsoluteLastUsageString(context, groupUsage); + + if (lastAppPermissionUsage != appPermissionUsage || (mSort == SORT_RECENT + && !accessTimeString.equals(lastAccessTimeString))) { + setPermissionSummary(parent, groups); + // Add a "parent" entry for the app that will expand to the individual entries. + parent = createExpandablePreferenceGroup(context, appPermissionUsage, + mSort == SORT_RECENT ? accessTimeString : null); + category.addPreference(parent); + lastAppPermissionUsage = appPermissionUsage; + groups = new ArrayList<>(); + } + + parent.addPreference(createPermissionUsagePreference(context, appPermissionUsage, + groupUsage, accessTimeString)); + groups.add(groupUsage.getGroup().getLabel()); + lastAccessTimeString = accessTimeString; + } + + setPermissionSummary(parent, groups); + + setLoading(false, true); + mFinishedInitialLoad = true; + setProgressBarVisible(false); + mPermissionUsages.stopLoader(getActivity().getLoaderManager()); + }).execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()])); + } + + private void addGroupUser(String app) { + Integer count = mGroupAppCounts.get(app); + if (count == null) { + mGroupAppCounts.put(app, 1); + } else { + mGroupAppCounts.put(app, count + 1); + } + } + + private void setPermissionSummary(@NonNull ExpandablePreferenceGroup pref, + @NonNull List<CharSequence> groups) { + if (pref == null) { + return; + } + StringBuilder sb = new StringBuilder(); + int numGroups = groups.size(); + for (int i = 0; i < numGroups; i++) { + sb.append(groups.get(i)); + if (i < numGroups - 1) { + sb.append(getString(R.string.item_separator)); + } + } + pref.setSummary(sb.toString()); + } + + /** + * Reloads the data to show. + */ + private void reloadData() { + final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex); + final long filterTimeBeginMillis = Math.max(System.currentTimeMillis() + - timeFilterItem.getTime(), Instant.EPOCH.toEpochMilli()); + mPermissionUsages.load(null /*filterPackageName*/, null /*filterPermissionGroups*/, + filterTimeBeginMillis, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST + | PermissionUsages.USAGE_FLAG_HISTORICAL, getActivity().getLoaderManager(), + false /*getUiInfo*/, false /*getNonPlatformPermissions*/, this /*callback*/, + false /*sync*/); + if (mFinishedInitialLoad) { + setProgressBarVisible(true); + } + } + /** + * Create a bar chart showing the permissions that are used by the most apps. + * + * @param usages the usages + * @param timeFilterItem the time filter, or null if no filter is set + * @param context the context + * + * @return the Preference representing the bar chart + */ + private BarChartPreference createBarChart( + @NonNull List<Pair<AppPermissionUsage, GroupUsage>> usages, + @Nullable TimeFilterItem timeFilterItem, @NonNull Context context) { + ArrayList<AppPermissionGroup> groups = new ArrayList<>(); + ArrayMap<String, Integer> groupToAppCount = new ArrayMap<>(); + int usageCount = usages.size(); + for (int i = 0; i < usageCount; i++) { + Pair<AppPermissionUsage, GroupUsage> usage = usages.get(i); + GroupUsage groupUsage = usage.second; + Integer count = groupToAppCount.get(groupUsage.getGroup().getName()); + if (count == null) { + groups.add(groupUsage.getGroup()); + groupToAppCount.put(groupUsage.getGroup().getName(), 1); + } else { + groupToAppCount.put(groupUsage.getGroup().getName(), count + 1); + } + } + + groups.sort((x, y) -> { + String xName = x.getName(); + String yName = y.getName(); + int usageDiff = compareLong(groupToAppCount.get(xName), groupToAppCount.get(yName)); + if (usageDiff != 0) { + return usageDiff; + } + if (xName.equals(LOCATION)) { + return -1; + } else if (yName.equals(LOCATION)) { + return 1; + } else if (xName.equals(MICROPHONE)) { + return -1; + } else if (yName.equals(MICROPHONE)) { + return 1; + } else if (xName.equals(CAMERA)) { + return -1; + } else if (yName.equals(CAMERA)) { + return 1; + } + return x.getName().compareTo(y.getName()); + }); + + BarChartInfo.Builder builder = new BarChartInfo.Builder(); + if (timeFilterItem != null) { + builder.setTitle(timeFilterItem.getGraphTitleRes()); + } + + int numBarsToShow = Math.min(groups.size(), MAXIMUM_NUM_BARS); + for (int i = 0; i < numBarsToShow; i++) { + AppPermissionGroup group = groups.get(i); + int count = groupToAppCount.get(group.getName()); + Drawable icon = Utils.applyTint(context, + Utils.loadDrawable(context.getPackageManager(), group.getIconPkg(), + group.getIconResId()), android.R.attr.colorControlNormal); + BarViewInfo barViewInfo = new BarViewInfo(icon, count, group.getLabel(), + context.getResources().getQuantityString(R.plurals.permission_usage_bar_label, + count, count), group.getLabel()); + barViewInfo.setClickListener(v -> onPermissionGroupSelected(group.getName())); + builder.addBarViewInfo(barViewInfo); + } + + BarChartPreference barChart = new BarChartPreference(context, null); + barChart.initializeBarChart(builder.build()); + return barChart; + } + + /** + * Create an expandable preference group that can hold children. + * + * @param context the context + * @param appPermissionUsage the permission usage for an app + * + * @return the expandable preference group. + */ + private ExpandablePreferenceGroup createExpandablePreferenceGroup(@NonNull Context context, + @NonNull AppPermissionUsage appPermissionUsage, @Nullable String summaryString) { + ExpandablePreferenceGroup preference = new ExpandablePreferenceGroup(context); + preference.setTitle(appPermissionUsage.getApp().getLabel()); + preference.setIcon(appPermissionUsage.getApp().getIcon()); + if (summaryString != null) { + preference.setSummary(summaryString); + } + return preference; + } + + /** + * Create a preference representing an app's use of a permission + * + * @param context the context + * @param appPermissionUsage the permission usage for the app + * @param groupUsage the permission item to add + * @param accessTimeStr the string representing the access time + * + * @return the Preference + */ + private PermissionControlPreference createPermissionUsagePreference(@NonNull Context context, + @NonNull AppPermissionUsage appPermissionUsage, + @NonNull GroupUsage groupUsage, @NonNull String accessTimeStr) { + final PermissionControlPreference pref = new PermissionControlPreference(context, + groupUsage.getGroup(), PermissionUsageFragment.class.getName()); + + final AppPermissionGroup group = groupUsage.getGroup(); + pref.setTitle(group.getLabel()); + pref.setUsageSummary(groupUsage, accessTimeStr); + pref.setTitleIcons(Collections.singletonList(group.getIconResId())); + pref.setKey(group.getApp().packageName + "," + group.getName()); + pref.useSmallerIcon(); + pref.setRightIcon(context.getDrawable(R.drawable.ic_settings_outline)); + return pref; + } + + /** + * Compare two usages by whichever app was used most recently. If the two represent the same + * app, sort by which group was used most recently. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x a usage. + * @param y a usage. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareAccessAppRecency(@NonNull Pair<AppPermissionUsage, GroupUsage> x, + @NonNull Pair<AppPermissionUsage, GroupUsage> y) { + if (x.first.getApp().getKey().equals(y.first.getApp().getKey())) { + return compareAccessTime(x.second, y.second); + } + return compareAccessTime(x.first, y.first); + } + + /** + * Compare two usages by their access time. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x a usage. + * @param y a usage. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareAccessTime(@NonNull Pair<AppPermissionUsage, GroupUsage> x, + @NonNull Pair<AppPermissionUsage, GroupUsage> y) { + return compareAccessTime(x.second, y.second); + } + + /** + * Compare two usages by their access time. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x a usage. + * @param y a usage. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareAccessTime(@NonNull GroupUsage x, @NonNull GroupUsage y) { + final int timeDiff = compareLong(x.getLastAccessTime(), y.getLastAccessTime()); + if (timeDiff != 0) { + return timeDiff; + } + // Make sure we lose no data if same + return x.hashCode() - y.hashCode(); + } + + /** + * Compare two AppPermissionUsage by their access time. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x an AppPermissionUsage. + * @param y an AppPermissionUsage. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareAccessTime(@NonNull AppPermissionUsage x, + @NonNull AppPermissionUsage y) { + final int timeDiff = compareLong(x.getLastAccessTime(), y.getLastAccessTime()); + if (timeDiff != 0) { + return timeDiff; + } + // Make sure we lose no data if same + return x.hashCode() - y.hashCode(); + } + + /** + * Compare two longs. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x the first long. + * @param y the second long. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareLong(long x, long y) { + if (x > y) { + return -1; + } else if (x < y) { + return 1; + } + return 0; + } + + /** + * Compare two usages by recency of access. + * + * Can be used as a {@link java.util.Comparator}. + * + * @param x a usage. + * @param y a usage. + * + * @return see {@link java.util.Comparator#compare(Object, Object)}. + */ + private static int compareAccessRecency(@NonNull Pair<AppPermissionUsage, GroupUsage> x, + @NonNull Pair<AppPermissionUsage, GroupUsage> y) { + final int timeDiff = compareAccessTime(x, y); + if (timeDiff != 0) { + return timeDiff; + } + // Make sure we lose no data if same + return x.hashCode() - y.hashCode(); + } + + /** + * Get the permission groups declared by the OS. + * + * @return a list of the permission groups declared by the OS. + */ + private @NonNull List<AppPermissionGroup> getOSPermissionGroups() { + final List<AppPermissionGroup> groups = new ArrayList<>(); + final Set<String> seenGroups = new ArraySet<>(); + final int numGroups = mAppPermissionUsages.size(); + for (int i = 0; i < numGroups; i++) { + final AppPermissionUsage appUsage = mAppPermissionUsages.get(i); + final List<GroupUsage> groupUsages = appUsage.getGroupUsages(); + final int groupUsageCount = groupUsages.size(); + for (int j = 0; j < groupUsageCount; j++) { + final GroupUsage groupUsage = groupUsages.get(j); + if (Utils.isModernPermissionGroup(groupUsage.getGroup().getName())) { + if (seenGroups.add(groupUsage.getGroup().getName())) { + groups.add(groupUsage.getGroup()); + } + } + } + } + return groups; + } + + /** + * Get an AppPermissionGroup that represents the given permission group (and an arbitrary app). + * + * @param groupName The name of the permission group. + * + * @return an AppPermissionGroup rerepsenting the given permission group or null if no such + * AppPermissionGroup is found. + */ + private @Nullable AppPermissionGroup getGroup(@NonNull String groupName) { + List<AppPermissionGroup> groups = getOSPermissionGroups(); + int numGroups = groups.size(); + for (int i = 0; i < numGroups; i++) { + if (groups.get(i).getName().equals(groupName)) { + return groups.get(i); + } + } + return null; + } + + /** + * Show a dialog that allows selecting a permission group by which to filter the entries. + */ + private void showPermissionFilterDialog() { + Context context = getPreferenceManager().getContext(); + + // Get the permission labels. + List<AppPermissionGroup> groups = getOSPermissionGroups(); + groups.sort( + (x, y) -> mCollator.compare(x.getLabel().toString(), y.getLabel().toString())); + + // Create the dialog entries. + String[] groupNames = new String[groups.size() + 1]; + CharSequence[] groupLabels = new CharSequence[groupNames.length]; + int[] groupAccessCounts = new int[groupNames.length]; + groupNames[0] = null; + groupLabels[0] = context.getString(R.string.permission_usage_any_permission); + Integer allAccesses = mGroupAppCounts.get(null); + if (allAccesses == null) { + allAccesses = 0; + } + groupAccessCounts[0] = allAccesses; + int selection = 0; + int numGroups = groups.size(); + for (int i = 0; i < numGroups; i++) { + AppPermissionGroup group = groups.get(i); + groupNames[i + 1] = group.getName(); + groupLabels[i + 1] = group.getLabel(); + Integer appCount = mGroupAppCounts.get(group.getName()); + if (appCount == null) { + appCount = 0; + } + groupAccessCounts[i + 1] = appCount; + if (group.getName().equals(mFilterGroup)) { + selection = i + 1; + } + } + + // Create the dialog + Bundle args = new Bundle(); + args.putCharSequence(PermissionsFilterDialog.TITLE, + context.getString(R.string.filter_by_title)); + args.putCharSequenceArray(PermissionsFilterDialog.ELEMS, groupLabels); + args.putInt(PermissionsFilterDialog.SELECTION, selection); + args.putStringArray(PermissionsFilterDialog.GROUPS, groupNames); + args.putIntArray(PermissionsFilterDialog.ACCESS_COUNTS, groupAccessCounts); + PermissionsFilterDialog chooserDialog = new PermissionsFilterDialog(); + chooserDialog.setArguments(args); + chooserDialog.setTargetFragment(this, 0); + chooserDialog.show(getFragmentManager().beginTransaction(), + PermissionsFilterDialog.class.getName()); + } + + /** + * Callback when the user selects a permission group by which to filter. + * + * @param selectedGroup The PermissionGroup to use to filter entries, or null if we should show + * all entries. + */ + private void onPermissionGroupSelected(@Nullable String selectedGroup) { + Fragment frag = newInstance(selectedGroup, mFilterTimes.get(mFilterTimeIndex).getTime()); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, frag) + .addToBackStack("PermissionUsage") + .commit(); + } + + /** + * A dialog that allows the user to select a permission group by which to filter entries. + * + * @see #showPermissionFilterDialog() + */ + public static class PermissionsFilterDialog extends DialogFragment { + private static final String TITLE = PermissionsFilterDialog.class.getName() + ".arg.title"; + private static final String ELEMS = PermissionsFilterDialog.class.getName() + ".arg.elems"; + private static final String SELECTION = PermissionsFilterDialog.class.getName() + + ".arg.selection"; + private static final String GROUPS = PermissionsFilterDialog.class.getName() + + ".arg.groups"; + private static final String ACCESS_COUNTS = PermissionsFilterDialog.class.getName() + + ".arg.access_counts"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder b = new AlertDialog.Builder(getActivity()) + .setView(createDialogView()); + + return b.create(); + } + + private @NonNull View createDialogView() { + PermissionUsageFragment fragment = (PermissionUsageFragment) getTargetFragment(); + CharSequence[] elems = getArguments().getCharSequenceArray(ELEMS); + String[] groups = getArguments().getStringArray(GROUPS); + int[] accessCounts = getArguments().getIntArray(ACCESS_COUNTS); + int selectedIndex = getArguments().getInt(SELECTION); + + LayoutInflater layoutInflater = LayoutInflater.from(fragment.getActivity()); + View view = layoutInflater.inflate(R.layout.permission_filter_dialog, null); + ViewGroup itemsListView = view.requireViewById(R.id.items_container); + + ((TextView) view.requireViewById(R.id.title)).setText( + getArguments().getCharSequence(TITLE)); + + ActionBarShadowController.attachToView(view.requireViewById(R.id.title_container), + getLifecycle(), view.requireViewById(R.id.scroll_view)); + + for (int i = 0; i < elems.length; i++) { + String groupName = groups[i]; + View itemView = layoutInflater.inflate(R.layout.permission_filter_dialog_item, + itemsListView, false); + + ((TextView) itemView.requireViewById(R.id.title)).setText(elems[i]); + ((TextView) itemView.requireViewById(R.id.summary)).setText( + getActivity().getResources().getQuantityString( + R.plurals.permission_usage_permission_filter_subtitle, + accessCounts[i], accessCounts[i])); + + itemView.setOnClickListener((v) -> { + dismissAllowingStateLoss(); + fragment.onPermissionGroupSelected(groupName); + }); + + RadioButton radioButton = itemView.requireViewById(R.id.radio_button); + radioButton.setChecked(i == selectedIndex); + radioButton.setOnClickListener((v) -> { + dismissAllowingStateLoss(); + fragment.onPermissionGroupSelected(groupName); + }); + + itemsListView.addView(itemView); + } + + return view; + } + } + + private void showTimeFilterDialog() { + Context context = getPreferenceManager().getContext(); + + CharSequence[] labels = new CharSequence[mFilterTimes.size()]; + for (int i = 0; i < labels.length; i++) { + labels[i] = mFilterTimes.get(i).getLabel(); + } + + // Create the dialog + Bundle args = new Bundle(); + args.putCharSequence(TimeFilterDialog.TITLE, + context.getString(R.string.filter_by_title)); + args.putCharSequenceArray(TimeFilterDialog.ELEMS, labels); + args.putInt(TimeFilterDialog.SELECTION, mFilterTimeIndex); + TimeFilterDialog chooserDialog = new TimeFilterDialog(); + chooserDialog.setArguments(args); + chooserDialog.setTargetFragment(this, 0); + chooserDialog.show(getFragmentManager().beginTransaction(), + TimeFilterDialog.class.getName()); + } + + /** + * Callback when the user selects a time by which to filter. + * + * @param selectedIndex The index of the dialog option selected by the user. + */ + private void onTimeSelected(int selectedIndex) { + mFilterTimeIndex = selectedIndex; + reloadData(); + } + + /** + * A dialog that allows the user to select a time by which to filter entries. + * + * @see #showTimeFilterDialog() + */ + public static class TimeFilterDialog extends DialogFragment { + private static final String TITLE = TimeFilterDialog.class.getName() + ".arg.title"; + private static final String ELEMS = TimeFilterDialog.class.getName() + ".arg.elems"; + private static final String SELECTION = TimeFilterDialog.class.getName() + ".arg.selection"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + PermissionUsageFragment fragment = (PermissionUsageFragment) getTargetFragment(); + CharSequence[] elems = getArguments().getCharSequenceArray(ELEMS); + AlertDialog.Builder b = new AlertDialog.Builder(getActivity()) + .setTitle(getArguments().getCharSequence(TITLE)) + .setSingleChoiceItems(elems, getArguments().getInt(SELECTION), + (dialog, which) -> { + dismissAllowingStateLoss(); + fragment.onTimeSelected(which); + } + ); + + return b.create(); + } + } + + /** + * A class representing a given time, e.g., "in the last hour". + */ + private static class TimeFilterItem { + private final long mTime; + private final @NonNull String mLabel; + private final @StringRes int mListTitleRes; + private final @StringRes int mGraphTitleRes; + + TimeFilterItem(long time, @NonNull String label, @StringRes int listTitleRes, + @StringRes int graphTitleRes) { + mTime = time; + mLabel = label; + mListTitleRes = listTitleRes; + mGraphTitleRes = graphTitleRes; + } + + /** + * Get the time represented by this object in milliseconds. + * + * @return the time represented by this object. + */ + public long getTime() { + return mTime; + } + + public @NonNull String getLabel() { + return mLabel; + } + + public @StringRes int getListTitleRes() { + return mListTitleRes; + } + + public @StringRes int getGraphTitleRes() { + return mGraphTitleRes; + } + } +}
diff --git a/src/com/android/permissioncontroller/permission/debug/PermissionUsages.java b/src/com/android/permissioncontroller/permission/debug/PermissionUsages.java new file mode 100644 index 0000000..4f06132 --- /dev/null +++ b/src/com/android/permissioncontroller/permission/debug/PermissionUsages.java
@@ -0,0 +1,362 @@ +/* + * Copyright (C) 2020 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.debug; + +import android.app.AppOpsManager; +import android.app.AppOpsManager.HistoricalOps; +import android.app.AppOpsManager.HistoricalOpsRequest; +import android.app.AppOpsManager.HistoricalPackageOps; +import android.app.AppOpsManager.HistoricalUidOps; +import android.app.AppOpsManager.PackageOps; +import android.app.LoaderManager; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.content.Loader; +import android.media.AudioManager; +import android.media.AudioRecordingConfiguration; +import android.os.Bundle; +import android.os.Process; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.permissioncontroller.permission.model.AppPermissionUsage; +import com.android.permissioncontroller.permission.model.AppPermissionUsage.Builder; +import com.android.permissioncontroller.permission.model.AppPermissionGroup; +import com.android.permissioncontroller.permission.model.Permission; +import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; +import com.android.permissioncontroller.permission.model.legacy.PermissionGroup; +import com.android.permissioncontroller.permission.model.legacy.PermissionGroups; +import com.android.permissioncontroller.permission.utils.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Loads all permission usages for a set of apps and permission groups. + */ +public final class PermissionUsages implements LoaderCallbacks<List<AppPermissionUsage>> { + public static final int USAGE_FLAG_LAST = 1 << 0; + public static final int USAGE_FLAG_HISTORICAL = 1 << 2; + + private final ArrayList<AppPermissionUsage> mUsages = new ArrayList<>(); + private final @NonNull Context mContext; + + private static final String KEY_FILTER_UID = "KEY_FILTER_UID"; + private static final String KEY_FILTER_PACKAGE_NAME = "KEY_FILTER_PACKAGE_NAME"; + private static final String KEY_FILTER_PERMISSION_GROUP = "KEY_FILTER_PERMISSION_GROUP"; + private static final String KEY_FILTER_BEGIN_TIME_MILLIS = "KEY_FILTER_BEGIN_TIME_MILLIS"; + private static final String KEY_FILTER_END_TIME_MILLIS = "KEY_FILTER_END_TIME_MILLIS"; + private static final String KEY_USAGE_FLAGS = "KEY_USAGE_FLAGS"; + private static final String KEY_GET_UI_INFO = "KEY_GET_UI_INFO"; + private static final String KEY_GET_NON_PLATFORM_PERMISSIONS = + "KEY_GET_NON_PLATFORM_PERMISSIONS"; + + private @Nullable PermissionsUsagesChangeCallback mCallback; + + public interface PermissionsUsagesChangeCallback { + void onPermissionUsagesChanged(); + } + + public PermissionUsages(@NonNull Context context) { + mContext = context; + } + + public void load(@Nullable String filterPackageName, + @Nullable String[] filterPermissionGroups, long filterBeginTimeMillis, + long filterEndTimeMillis, int usageFlags, @NonNull LoaderManager loaderManager, + boolean getUiInfo, boolean getNonPlatformPermissions, + @NonNull PermissionsUsagesChangeCallback callback, boolean sync) { + load(Process.INVALID_UID, filterPackageName, filterPermissionGroups, filterBeginTimeMillis, + filterEndTimeMillis, usageFlags, loaderManager, getUiInfo, + getNonPlatformPermissions, callback, sync); + } + + public void load(int filterUid, @Nullable String filterPackageName, + @Nullable String[] filterPermissionGroups, long filterBeginTimeMillis, + long filterEndTimeMillis, int usageFlags, @NonNull LoaderManager loaderManager, + boolean getUiInfo, boolean getNonPlatformPermissions, + @NonNull PermissionsUsagesChangeCallback callback, boolean sync) { + mCallback = callback; + final Bundle args = new Bundle(); + args.putInt(KEY_FILTER_UID, filterUid); + args.putString(KEY_FILTER_PACKAGE_NAME, filterPackageName); + args.putStringArray(KEY_FILTER_PERMISSION_GROUP, filterPermissionGroups); + args.putLong(KEY_FILTER_BEGIN_TIME_MILLIS, filterBeginTimeMillis); + args.putLong(KEY_FILTER_END_TIME_MILLIS, filterEndTimeMillis); + args.putInt(KEY_USAGE_FLAGS, usageFlags); + args.putBoolean(KEY_GET_UI_INFO, getUiInfo); + args.putBoolean(KEY_GET_NON_PLATFORM_PERMISSIONS, getNonPlatformPermissions); + if (sync) { + final UsageLoader loader = new UsageLoader(mContext, args); + final List<AppPermissionUsage> usages = loader.loadInBackground(); + onLoadFinished(loader, usages); + } else { + loaderManager.restartLoader(1, args, this); + } + } + + @Override + public Loader<List<AppPermissionUsage>> onCreateLoader(int id, Bundle args) { + return new UsageLoader(mContext, args); + } + + @Override + public void onLoadFinished(@NonNull Loader<List<AppPermissionUsage>> loader, + List<AppPermissionUsage> usages) { + mUsages.clear(); + mUsages.addAll(usages); + if (mCallback != null) { + mCallback.onPermissionUsagesChanged(); + } + } + + @Override + public void onLoaderReset(@NonNull Loader<List<AppPermissionUsage>> loader) { + mUsages.clear(); + mCallback.onPermissionUsagesChanged(); + } + + public @NonNull List<AppPermissionUsage> getUsages() { + return mUsages; + } + + public void stopLoader(@NonNull LoaderManager loaderManager) { + loaderManager.destroyLoader(1); + } + + public static @Nullable AppPermissionUsage.GroupUsage loadLastGroupUsage( + @NonNull Context context, @NonNull AppPermissionGroup group) { + final ArraySet<String> opNames = new ArraySet<>(); + final List<Permission> permissions = group.getPermissions(); + final int permCount = permissions.size(); + for (int i = 0; i < permCount; i++) { + final Permission permission = permissions.get(i); + final String opName = permission.getAppOp(); + if (opName != null) { + opNames.add(opName); + } + } + final String[] opNamesArray = opNames.toArray(new String[opNames.size()]); + final List<PackageOps> usageOps = context.getSystemService(AppOpsManager.class) + .getOpsForPackage(group.getApp().applicationInfo.uid, + group.getApp().packageName, opNamesArray); + if (usageOps == null || usageOps.isEmpty()) { + return null; + } + return new AppPermissionUsage.GroupUsage(group, usageOps.get(0), null); + } + + private static final class UsageLoader extends AsyncTaskLoader<List<AppPermissionUsage>> { + private final int mFilterUid; + private @Nullable String mFilterPackageName; + private @Nullable String[] mFilterPermissionGroups; + private final long mFilterBeginTimeMillis; + private final long mFilterEndTimeMillis; + private final int mUsageFlags; + private final boolean mGetUiInfo; + private final boolean mGetNonPlatformPermissions; + + UsageLoader(@NonNull Context context, @NonNull Bundle args) { + super(context); + mFilterUid = args.getInt(KEY_FILTER_UID); + mFilterPackageName = args.getString(KEY_FILTER_PACKAGE_NAME); + mFilterPermissionGroups = args.getStringArray(KEY_FILTER_PERMISSION_GROUP); + mFilterBeginTimeMillis = args.getLong(KEY_FILTER_BEGIN_TIME_MILLIS); + mFilterEndTimeMillis = args.getLong(KEY_FILTER_END_TIME_MILLIS); + mUsageFlags = args.getInt(KEY_USAGE_FLAGS); + mGetUiInfo = args.getBoolean(KEY_GET_UI_INFO); + mGetNonPlatformPermissions = args.getBoolean(KEY_GET_NON_PLATFORM_PERMISSIONS); + } + + @Override + protected void onStartLoading() { + forceLoad(); + } + + @Override + public @NonNull List<AppPermissionUsage> loadInBackground() { + final List<PermissionGroup> groups = PermissionGroups.getPermissionGroups( + getContext(), this::isLoadInBackgroundCanceled, mGetUiInfo, + mGetNonPlatformPermissions, mFilterPermissionGroups, mFilterPackageName); + if (groups.isEmpty()) { + return Collections.emptyList(); + } + + final List<AppPermissionUsage> usages = new ArrayList<>(); + final ArraySet<String> opNames = new ArraySet<>(); + final ArrayMap<Pair<Integer, String>, AppPermissionUsage.Builder> usageBuilders = + new ArrayMap<>(); + + final int groupCount = groups.size(); + for (int groupIdx = 0; groupIdx < groupCount; groupIdx++) { + final PermissionGroup group = groups.get(groupIdx); + // Filter out third party permissions + if (!group.getDeclaringPackage().equals(Utils.OS_PKG)) { + continue; + } + + groups.add(group); + + final List<PermissionApp> permissionApps = group.getPermissionApps().getApps(); + final int appCount = permissionApps.size(); + for (int appIdx = 0; appIdx < appCount; appIdx++) { + final PermissionApp permissionApp = permissionApps.get(appIdx); + if (mFilterUid != Process.INVALID_UID + && permissionApp.getAppInfo().uid != mFilterUid) { + continue; + } + + final AppPermissionGroup appPermGroup = permissionApp.getPermissionGroup(); + if (!Utils.shouldShowPermission(getContext(), appPermGroup)) { + continue; + } + final Pair<Integer, String> usageKey = Pair.create(permissionApp.getUid(), + permissionApp.getPackageName()); + AppPermissionUsage.Builder usageBuilder = usageBuilders.get(usageKey); + if (usageBuilder == null) { + usageBuilder = new Builder(permissionApp); + usageBuilders.put(usageKey, usageBuilder); + } + usageBuilder.addGroup(appPermGroup); + final List<Permission> permissions = appPermGroup.getPermissions(); + final int permCount = permissions.size(); + for (int permIdx = 0; permIdx < permCount; permIdx++) { + final Permission permission = permissions.get(permIdx); + final String opName = permission.getAppOp(); + if (opName != null) { + opNames.add(opName); + } + } + } + } + + if (usageBuilders.isEmpty()) { + return Collections.emptyList(); + } + + final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); + + // Get last usage data and put in a map for a quick lookup. + final ArrayMap<Pair<Integer, String>, PackageOps> lastUsages = + new ArrayMap<>(usageBuilders.size()); + final String[] opNamesArray = opNames.toArray(new String[opNames.size()]); + if ((mUsageFlags & USAGE_FLAG_LAST) != 0) { + final List<PackageOps> usageOps; + if (mFilterPackageName != null || mFilterUid != Process.INVALID_UID) { + usageOps = appOpsManager.getOpsForPackage(mFilterUid, mFilterPackageName, + opNamesArray); + } else { + usageOps = appOpsManager.getPackagesForOps(opNamesArray); + } + if (usageOps != null && !usageOps.isEmpty()) { + final int usageOpsCount = usageOps.size(); + for (int i = 0; i < usageOpsCount; i++) { + final PackageOps usageOp = usageOps.get(i); + lastUsages.put(Pair.create(usageOp.getUid(), usageOp.getPackageName()), + usageOp); + } + } + } + + if (isLoadInBackgroundCanceled()) { + return Collections.emptyList(); + } + + // Get historical usage data and put in a map for a quick lookup + final ArrayMap<Pair<Integer, String>, HistoricalPackageOps> historicalUsages = + new ArrayMap<>(usageBuilders.size()); + if ((mUsageFlags & USAGE_FLAG_HISTORICAL) != 0) { + final AtomicReference<HistoricalOps> historicalOpsRef = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + final HistoricalOpsRequest request = new HistoricalOpsRequest.Builder( + mFilterBeginTimeMillis, mFilterEndTimeMillis) + .setUid(mFilterUid) + .setPackageName(mFilterPackageName) + .setOpNames(new ArrayList<>(opNames)) + .setFlags(AppOpsManager.OP_FLAGS_ALL_TRUSTED) + .build(); + appOpsManager.getHistoricalOps(request, Runnable::run, + (HistoricalOps ops) -> { + historicalOpsRef.set(ops); + latch.countDown(); + }); + try { + latch.await(5, TimeUnit.DAYS); + } catch (InterruptedException ignored) { } + + final HistoricalOps historicalOps = historicalOpsRef.get(); + if (historicalOps != null) { + final int uidCount = historicalOps.getUidCount(); + for (int i = 0; i < uidCount; i++) { + final HistoricalUidOps uidOps = historicalOps.getUidOpsAt(i); + final int packageCount = uidOps.getPackageCount(); + for (int j = 0; j < packageCount; j++) { + final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(j); + historicalUsages.put( + Pair.create(uidOps.getUid(), packageOps.getPackageName()), + packageOps); + } + } + } + } + + // Get audio recording config + List<AudioRecordingConfiguration> allRecordings = getContext() + .getSystemService(AudioManager.class).getActiveRecordingConfigurations(); + SparseArray<ArrayList<AudioRecordingConfiguration>> recordingsByUid = + new SparseArray<>(); + + final int recordingsCount = allRecordings.size(); + for (int i = 0; i < recordingsCount; i++) { + AudioRecordingConfiguration recording = allRecordings.get(i); + + ArrayList<AudioRecordingConfiguration> recordings = recordingsByUid.get( + recording.getClientUid()); + if (recordings == null) { + recordings = new ArrayList<>(); + recordingsByUid.put(recording.getClientUid(), recordings); + } + recordings.add(recording); + } + + // Construct the historical usages based on data we fetched + final int builderCount = usageBuilders.size(); + for (int i = 0; i < builderCount; i++) { + final Pair<Integer, String> key = usageBuilders.keyAt(i); + final Builder usageBuilder = usageBuilders.valueAt(i); + final PackageOps lastUsage = lastUsages.get(key); + usageBuilder.setLastUsage(lastUsage); + final HistoricalPackageOps historicalUsage = historicalUsages.get(key); + usageBuilder.setHistoricalUsage(historicalUsage); + usageBuilder.setRecordingConfiguration(recordingsByUid.get(key.first)); + usages.add(usageBuilder.build()); + } + + return usages; + } + } +}
diff --git a/src/com/android/permissioncontroller/permission/debug/PreferenceImageView.java b/src/com/android/permissioncontroller/permission/debug/PreferenceImageView.java new file mode 100644 index 0000000..378601d --- /dev/null +++ b/src/com/android/permissioncontroller/permission/debug/PreferenceImageView.java
@@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 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.debug; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Extension of ImageView that correctly applies maxWidth and maxHeight. + */ +public class PreferenceImageView extends ImageView { + + public PreferenceImageView(Context context) { + this(context, null); + } + + public PreferenceImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int maxWidth = getMaxWidth(); + if (maxWidth != Integer.MAX_VALUE + && (maxWidth < widthSize || widthMode == MeasureSpec.UNSPECIFIED)) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); + } + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + final int maxHeight = getMaxHeight(); + if (maxHeight != Integer.MAX_VALUE + && (maxHeight < heightSize || heightMode == MeasureSpec.UNSPECIFIED)) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +}
diff --git a/src/com/android/permissioncontroller/permission/debug/Utils.kt b/src/com/android/permissioncontroller/permission/debug/Utils.kt new file mode 100644 index 0000000..0c01b90 --- /dev/null +++ b/src/com/android/permissioncontroller/permission/debug/Utils.kt
@@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 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.debug + +import android.content.Context +import android.icu.util.Calendar +import android.provider.DeviceConfig +import android.text.format.DateFormat.getMediumDateFormat +import android.text.format.DateFormat.getTimeFormat +import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage +import java.util.Locale + +/** Whether to show the Permissions Hub. */ +private const val PROPERTY_PERMISSIONS_HUB_2_ENABLED = "permissions_hub_2_enabled" + +/** Whether to show the mic and camera icons. */ +const val PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled" + +/** + * Whether the Permissions Hub 2 flag is enabled + * + * @return whether the flag is enabled + */ +fun isPermissionsHub2FlagEnabled(): Boolean { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + PROPERTY_PERMISSIONS_HUB_2_ENABLED, false) +} +/** + * Whether to show the Permissions Dashboard + * + * @return whether to show the Permissions Dashboard. + */ +fun shouldShowPermissionsDashboard(): Boolean { + return isPermissionsHub2FlagEnabled() +} + +/** + * Whether the Camera and Mic Icons are enabled by flag. + * + * @return whether the Camera and Mic Icons are enabled. + */ +fun isCameraMicIconsFlagEnabled(): Boolean { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + PROPERTY_CAMERA_MIC_ICONS_ENABLED, true) +} + +/** + * Whether to show Camera and Mic Icons. They should be shown if the permission hub, or the icons + * specifically, are enabled. + * + * @return whether to show the icons. + */ +fun shouldShowCameraMicIndicators(): Boolean { + return isCameraMicIconsFlagEnabled() || isPermissionsHub2FlagEnabled() +} + +/** + * Build a string representing the given time if it happened on the current day and the date + * otherwise. + * + * @param context the context. + * @param lastAccessTime the time in milliseconds. + * + * @return a string representing the time or date of the given time or null if the time is 0. + */ +fun getAbsoluteTimeString(context: Context, lastAccessTime: Long): String? { + if (lastAccessTime == 0L) { + return null + } + return if (isToday(lastAccessTime)) { + getTimeFormat(context).format(lastAccessTime) + } else { + getMediumDateFormat(context).format(lastAccessTime) + } +} + +/** + * Build a string representing the time of the most recent permission usage if it happened on + * the current day and the date otherwise. + * + * @param context the context. + * @param groupUsage the permission usage. + * + * @return a string representing the time or date of the most recent usage or null if there are + * no usages. + */ +fun getAbsoluteLastUsageString(context: Context, groupUsage: GroupUsage?): String? { + return if (groupUsage == null) { + null + } else getAbsoluteTimeString(context, groupUsage.lastAccessTime) +} + +/** + * Build a string representing the duration of a permission usage. + * + * @return a string representing the duration of this app's usage or null if there are no + * usages. + */ +fun getUsageDurationString(context: Context, groupUsage: GroupUsage?): String? { + return if (groupUsage == null) { + null + } else getTimeDiffStr(context, groupUsage.accessDuration) +} + +/** + * Build a string representing the number of milliseconds passed in. It rounds to the nearest + * unit. For example, given a duration of 3500 and an English locale, this can return + * "3 seconds". + * @param context The context. + * @param duration The number of milliseconds. + * @return a string representing the given number of milliseconds. + */ +fun getTimeDiffStr(context: Context, duration: Long): String { + val seconds = Math.max(1, duration / 1000) + if (seconds < 60) { + return context.resources.getQuantityString(R.plurals.seconds, seconds.toInt(), + seconds) + } + val minutes = seconds / 60 + if (minutes < 60) { + return context.resources.getQuantityString(R.plurals.minutes, minutes.toInt(), + minutes) + } + val hours = minutes / 60 + if (hours < 24) { + return context.resources.getQuantityString(R.plurals.hours, hours.toInt(), hours) + } + val days = hours / 24 + return context.resources.getQuantityString(R.plurals.days, days.toInt(), days) +} + +/** + * Check whether the given time (in milliseconds) is in the current day. + * + * @param time the time in milliseconds + * + * @return whether the given time is in the current day. + */ +private fun isToday(time: Long): Boolean { + val today: Calendar = Calendar.getInstance(Locale.getDefault()) + today.setTimeInMillis(System.currentTimeMillis()) + today.set(Calendar.HOUR_OF_DAY, 0) + today.set(Calendar.MINUTE, 0) + today.set(Calendar.SECOND, 0) + today.set(Calendar.MILLISECOND, 0) + val date: Calendar = Calendar.getInstance(Locale.getDefault()) + date.setTimeInMillis(time) + return !date.before(today) +} \ No newline at end of file
diff --git a/src/com/android/permissioncontroller/permission/model/AppPermissionUsage.java b/src/com/android/permissioncontroller/permission/model/AppPermissionUsage.java new file mode 100644 index 0000000..d45e9b4 --- /dev/null +++ b/src/com/android/permissioncontroller/permission/model/AppPermissionUsage.java
@@ -0,0 +1,301 @@ +/* + * Copyright (C) 2020 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.model; + +import static android.Manifest.permission_group.MICROPHONE; + +import android.app.AppOpsManager; +import android.app.AppOpsManager.HistoricalOp; +import android.app.AppOpsManager.HistoricalPackageOps; +import android.app.AppOpsManager.OpEntry; +import android.app.AppOpsManager.PackageOps; +import android.media.AudioRecordingConfiguration; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Stats for permission usage of an app. This data is for a given time period, + * i.e. does not contain the full history. + */ +public final class AppPermissionUsage { + private final @NonNull List<GroupUsage> mGroupUsages = new ArrayList<>(); + private final @NonNull PermissionApp mPermissionApp; + + private AppPermissionUsage(@NonNull PermissionApp permissionApp, + @NonNull List<AppPermissionGroup> groups, @Nullable PackageOps lastUsage, + @Nullable HistoricalPackageOps historicalUsage, + @Nullable ArrayList<AudioRecordingConfiguration> recordings) { + mPermissionApp = permissionApp; + final int groupCount = groups.size(); + for (int i = 0; i < groupCount; i++) { + final AppPermissionGroup group = groups.get(i); + + /** + * TODO: HACK HACK HACK. + * + * Exclude for the UIDs that are currently silenced. This happens if an app keeps + * recording while in the background for more than a few seconds. + */ + if (recordings != null && group.getName().equals(MICROPHONE)) { + boolean isSilenced = false; + int recordingsCount = recordings.size(); + for (int recordingNum = 0; recordingNum < recordingsCount; recordingNum++) { + AudioRecordingConfiguration recording = recordings.get(recordingNum); + if (recording.isClientSilenced()) { + isSilenced = true; + break; + } + } + + if (isSilenced) { + continue; + } + } + + mGroupUsages.add(new GroupUsage(group, lastUsage, historicalUsage)); + } + } + + public @NonNull PermissionApp getApp() { + return mPermissionApp; + } + + public @NonNull String getPackageName() { + return mPermissionApp.getPackageName(); + } + + public int getUid() { + return mPermissionApp.getUid(); + } + + public long getLastAccessTime() { + long lastAccessTime = 0; + final int permissionCount = mGroupUsages.size(); + for (int i = 0; i < permissionCount; i++) { + final GroupUsage groupUsage = mGroupUsages.get(i); + lastAccessTime = Math.max(lastAccessTime, groupUsage.getLastAccessTime()); + } + return lastAccessTime; + } + + public long getAccessCount() { + long accessCount = 0; + final int permissionCount = mGroupUsages.size(); + for (int i = 0; i < permissionCount; i++) { + final GroupUsage permission = mGroupUsages.get(i); + accessCount += permission.getAccessCount(); + } + return accessCount; + } + + public @NonNull List<GroupUsage> getGroupUsages() { + return mGroupUsages; + } + + /** + * Stats for permission usage of a permission group. This data is for a + * given time period, i.e. does not contain the full history. + */ + public static class GroupUsage { + private final @NonNull AppPermissionGroup mGroup; + private final @Nullable PackageOps mLastUsage; + private final @Nullable HistoricalPackageOps mHistoricalUsage; + + public GroupUsage(@NonNull AppPermissionGroup group, @Nullable PackageOps lastUsage, + @Nullable HistoricalPackageOps historicalUsage) { + mGroup = group; + mLastUsage = lastUsage; + mHistoricalUsage = historicalUsage; + } + + public long getLastAccessTime() { + if (mLastUsage == null) { + return 0; + } + return lastAccessAggregate( + (op) -> op.getLastAccessTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + public long getLastAccessForegroundTime() { + if (mLastUsage == null) { + return 0; + } + return lastAccessAggregate( + (op) -> op.getLastAccessForegroundTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + public long getLastAccessBackgroundTime() { + if (mLastUsage == null) { + return 0; + } + return lastAccessAggregate( + (op) -> op.getLastAccessBackgroundTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + public long getForegroundAccessCount() { + if (mHistoricalUsage == null) { + return 0; + } + return extractAggregate((HistoricalOp op) + -> op.getForegroundAccessCount(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + public long getBackgroundAccessCount() { + if (mHistoricalUsage == null) { + return 0; + } + return extractAggregate((HistoricalOp op) + -> op.getBackgroundAccessCount(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + public long getAccessCount() { + if (mHistoricalUsage == null) { + return 0; + } + return extractAggregate((HistoricalOp op) -> + op.getForegroundAccessCount(AppOpsManager.OP_FLAGS_ALL_TRUSTED) + + op.getBackgroundAccessCount(AppOpsManager.OP_FLAGS_ALL_TRUSTED) + ); + } + + public long getLastAccessDuration() { + if (mLastUsage == null) { + return 0; + } + return lastAccessAggregate( + (op) -> op.getLastDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED)); + } + + + public long getAccessDuration() { + if (mHistoricalUsage == null) { + return 0; + } + return extractAggregate((HistoricalOp op) -> + op.getForegroundAccessDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED) + + op.getBackgroundAccessDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED) + ); + } + + public boolean isRunning() { + if (mLastUsage == null) { + return false; + } + final ArrayList<Permission> permissions = mGroup.getPermissions(); + final int permissionCount = permissions.size(); + for (int i = 0; i < permissionCount; i++) { + final Permission permission = permissions.get(i); + final String opName = permission.getAppOp(); + final List<OpEntry> ops = mLastUsage.getOps(); + final int opCount = ops.size(); + for (int j = 0; j < opCount; j++) { + final OpEntry op = ops.get(j); + if (op.getOpStr().equals(opName) && op.isRunning()) { + return true; + } + } + } + return false; + } + + private long extractAggregate(@NonNull Function<HistoricalOp, Long> extractor) { + long aggregate = 0; + final ArrayList<Permission> permissions = mGroup.getPermissions(); + final int permissionCount = permissions.size(); + for (int i = 0; i < permissionCount; i++) { + final Permission permission = permissions.get(i); + final String opName = permission.getAppOp(); + final HistoricalOp historicalOp = mHistoricalUsage.getOp(opName); + if (historicalOp != null) { + aggregate += extractor.apply(historicalOp); + } + } + return aggregate; + } + + private long lastAccessAggregate(@NonNull Function<OpEntry, Long> extractor) { + long aggregate = 0; + final ArrayList<Permission> permissions = mGroup.getPermissions(); + final int permissionCount = permissions.size(); + for (int permissionNum = 0; permissionNum < permissionCount; permissionNum++) { + final Permission permission = permissions.get(permissionNum); + final String opName = permission.getAppOp(); + final List<OpEntry> ops = mLastUsage.getOps(); + final int opCount = ops.size(); + for (int opNum = 0; opNum < opCount; opNum++) { + final OpEntry op = ops.get(opNum); + if (op.getOpStr().equals(opName)) { + aggregate = Math.max(aggregate, extractor.apply(op)); + } + } + } + return aggregate; + } + + public @NonNull AppPermissionGroup getGroup() { + return mGroup; + } + } + + public static class Builder { + private final @NonNull List<AppPermissionGroup> mGroups = new ArrayList<>(); + private final @NonNull PermissionApp mPermissionApp; + private @Nullable PackageOps mLastUsage; + private @Nullable HistoricalPackageOps mHistoricalUsage; + private @Nullable ArrayList<AudioRecordingConfiguration> mAudioRecordingConfigurations; + + public Builder(@NonNull PermissionApp permissionApp) { + mPermissionApp = permissionApp; + } + + public @NonNull Builder addGroup(@NonNull AppPermissionGroup group) { + mGroups.add(group); + return this; + } + + public @NonNull Builder setLastUsage(@Nullable PackageOps lastUsage) { + mLastUsage = lastUsage; + return this; + } + + public @NonNull Builder setHistoricalUsage(@Nullable HistoricalPackageOps historicalUsage) { + mHistoricalUsage = historicalUsage; + return this; + } + + public @NonNull Builder setRecordingConfiguration( + @Nullable ArrayList<AudioRecordingConfiguration> recordings) { + mAudioRecordingConfigurations = recordings; + return this; + } + + public @NonNull AppPermissionUsage build() { + if (mGroups.isEmpty()) { + throw new IllegalStateException("mGroups cannot be empty."); + } + return new AppPermissionUsage(mPermissionApp, mGroups, mLastUsage, mHistoricalUsage, + mAudioRecordingConfigurations); + } + } +}
diff --git a/src/com/android/permissioncontroller/permission/model/legacy/PermissionApps.java b/src/com/android/permissioncontroller/permission/model/legacy/PermissionApps.java index e837ade..e493e08 100644 --- a/src/com/android/permissioncontroller/permission/model/legacy/PermissionApps.java +++ b/src/com/android/permissioncontroller/permission/model/legacy/PermissionApps.java
@@ -421,6 +421,19 @@ return mAppPermissionGroup; } + /** + * Load this app's label and icon if they were not previously loaded. + * + * @param appDataCache the cache of already-loaded labels and icons. + */ + public void loadLabelAndIcon(@NonNull AppDataCache appDataCache) { + if (mInfo.packageName.equals(mLabel) || mIcon == null) { + Pair<String, Drawable> appData = appDataCache.getAppData(getUid(), mInfo); + mLabel = appData.first; + mIcon = appData.second; + } + } + @Override public int compareTo(PermissionApp another) { final int result = mLabel.compareTo(another.mLabel); @@ -520,4 +533,33 @@ public interface Callback { void onPermissionsLoaded(PermissionApps permissionApps); } + + /** + * Class used to asynchronously load apps' labels and icons. + */ + public static class AppDataLoader extends AsyncTask<PermissionApp, Void, Void> { + + private final Context mContext; + private final Runnable mCallback; + + public AppDataLoader(Context context, Runnable callback) { + mContext = context; + mCallback = callback; + } + + @Override + protected Void doInBackground(PermissionApp... args) { + AppDataCache appDataCache = new AppDataCache(mContext.getPackageManager(), mContext); + int numArgs = args.length; + for (int i = 0; i < numArgs; i++) { + args[i].loadLabelAndIcon(appDataCache); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + mCallback.run(); + } + } }
diff --git a/src/com/android/permissioncontroller/permission/model/legacy/PermissionGroup.java b/src/com/android/permissioncontroller/permission/model/legacy/PermissionGroup.java index da1039d..6b951c9 100644 --- a/src/com/android/permissioncontroller/permission/model/legacy/PermissionGroup.java +++ b/src/com/android/permissioncontroller/permission/model/legacy/PermissionGroup.java
@@ -78,6 +78,13 @@ return mGranted; } + /** + * @return The PermissionApps object for this permission group. + */ + public PermissionApps getPermissionApps() { + return mPermApps; + } + @Override public int compareTo(PermissionGroup another) { return mLabel.toString().compareTo(another.mLabel.toString());
diff --git a/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java b/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java index 04336df..7297792 100644 --- a/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java +++ b/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java
@@ -46,6 +46,8 @@ import com.android.permissioncontroller.DeviceUtils; import com.android.permissioncontroller.PermissionControllerStatsLog; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.debug.PermissionUsageFragment; +import com.android.permissioncontroller.permission.debug.UtilsKt; import com.android.permissioncontroller.permission.ui.auto.AutoAllAppPermissionsFragment; import com.android.permissioncontroller.permission.ui.auto.AutoAppPermissionsFragment; import com.android.permissioncontroller.permission.ui.auto.AutoManageStandardPermissionsFragment; @@ -141,6 +143,16 @@ } break; + case Intent.ACTION_REVIEW_PERMISSION_USAGE: { + if (!UtilsKt.shouldShowPermissionsDashboard()) { + finish(); + return; + } + + String groupName = getIntent().getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME); + androidXFragment = PermissionUsageFragment.newInstance(groupName, Long.MAX_VALUE); + } break; + case Intent.ACTION_MANAGE_APP_PERMISSION: { if (DeviceUtils.isAuto(this) || DeviceUtils.isTelevision(this) || DeviceUtils.isWear(this)) {
diff --git a/src/com/android/permissioncontroller/permission/ui/ReviewOngoingUsageActivity.java b/src/com/android/permissioncontroller/permission/ui/ReviewOngoingUsageActivity.java new file mode 100644 index 0000000..2082b76 --- /dev/null +++ b/src/com/android/permissioncontroller/permission/ui/ReviewOngoingUsageActivity.java
@@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 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; + +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + +import com.android.permissioncontroller.DeviceUtils; +import com.android.permissioncontroller.permission.ui.handheld.ReviewOngoingUsageFragment; +import com.android.permissioncontroller.permission.debug.UtilsKt; + +/** + * A dialog listing the currently uses of camera, microphone, and location. + */ +public final class ReviewOngoingUsageActivity extends FragmentActivity { + + // Number of milliseconds in the past to look for accesses if nothing was specified. + private static final long DEFAULT_MILLIS = 5000; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!UtilsKt.shouldShowCameraMicIndicators()) { + finish(); + return; + } + + getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + + long numMillis = getIntent().getLongExtra(Intent.EXTRA_DURATION_MILLIS, DEFAULT_MILLIS); + getSupportFragmentManager().beginTransaction().replace(android.R.id.content, + ReviewOngoingUsageFragment.newInstance(numMillis)).commit(); + } + + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // in automotive mode, there's no system wide back button, so need to add that + if (DeviceUtils.isAuto(this)) { + onBackPressed(); + } else { + finish(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} \ No newline at end of file
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java index e3f0f98..cd8c27f 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
@@ -81,6 +81,9 @@ private static final String AUTO_REVOKE_CATEGORY_KEY = "_AUTO_REVOKE_KEY"; private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY"; private static final String AUTO_REVOKE_SUMMARY_KEY = "_AUTO_REVOKE_SUMMARY_KEY"; + private static final String ASSISTANT_MIC_CATEGORY_KEY = "_ASSISTANT_MIC_KEY"; + private static final String ASSISTANT_MIC_SWITCH_KEY = "_ASSISTANT_MIC_SWITCH_KEY"; + private static final String ASSISTANT_MIC_SUMMARY_KEY = "_ASSISTANT_MIC_SUMMARY_KEY"; static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"; @@ -346,10 +349,12 @@ Preference autoRevokeSummary = autoRevokeCategory.findPreference(AUTO_REVOKE_SUMMARY_KEY); if (!state.isEnabledGlobal() || !state.getShouldShowSwitch()) { + autoRevokeCategory.setVisible(false); autoRevokeSwitch.setVisible(false); autoRevokeSummary.setVisible(false); return; } + autoRevokeCategory.setVisible(true); autoRevokeSwitch.setVisible(true); autoRevokeSummary.setVisible(true); autoRevokeSwitch.setChecked(state.isEnabledForApp());
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/AutoRevokeFragment.kt b/src/com/android/permissioncontroller/permission/ui/handheld/AutoRevokeFragment.kt index f423555..e1364f3 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/AutoRevokeFragment.kt +++ b/src/com/android/permissioncontroller/permission/ui/handheld/AutoRevokeFragment.kt
@@ -39,6 +39,7 @@ import com.android.permissioncontroller.permission.ui.model.AutoRevokeViewModelFactory import com.android.permissioncontroller.permission.utils.IPC import com.android.permissioncontroller.permission.utils.KotlinUtils +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -105,7 +106,9 @@ GlobalScope.launch(IPC) { delay(SHOW_LOAD_DELAY_MS) if (!viewModel.areAutoRevokedPackagesLoaded()) { - setLoading(true, false) + GlobalScope.launch(Main) { + setLoading(true, true) + } } } }
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java index 261f82e..ddae029 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/ManageStandardPermissionsFragment.java
@@ -17,10 +17,14 @@ import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; +import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldShowPermissionsDashboard; import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; +import android.content.Intent; import android.os.Bundle; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import androidx.lifecycle.ViewModelProvider; @@ -28,6 +32,7 @@ import androidx.preference.PreferenceScreen; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity; import com.android.permissioncontroller.permission.ui.model.ManageStandardPermissionsViewModel; import com.android.permissioncontroller.permission.ui.model.ManageStandardPermissionsViewModelFactory; import com.android.permissioncontroller.permission.utils.Utils; @@ -40,6 +45,8 @@ private static final String AUTO_REVOKE_KEY = "auto_revoke_key"; private static final String LOG_TAG = ManageStandardPermissionsFragment.class.getSimpleName(); + private static final int MENU_PERMISSION_USAGE = MENU_HIDE_SYSTEM + 1; + private ManageStandardPermissionsViewModel mViewModel; /** @@ -87,14 +94,28 @@ @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - pressBack(this); - return true; + switch (item.getItemId()) { + case android.R.id.home: + pressBack(this); + return true; + case MENU_PERMISSION_USAGE: + getActivity().startActivity(new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE) + .setClass(getContext(), ManagePermissionsActivity.class)); + return true; } return super.onOptionsItemSelected(item); } @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + if (shouldShowPermissionsDashboard()) { + menu.add(Menu.NONE, MENU_PERMISSION_USAGE, Menu.NONE, R.string.permission_usage_title); + } + } + + @Override protected PreferenceScreen updatePermissionsUi() { PreferenceScreen screen = super.updatePermissionsUi(); if (screen == null) {
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionAppsFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionAppsFragment.java index b73b426..7c9c46b 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionAppsFragment.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionAppsFragment.java
@@ -22,6 +22,7 @@ import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED; import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED; +import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldShowPermissionsDashboard; import static com.android.permissioncontroller.permission.ui.Category.ALLOWED; import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND; import static com.android.permissioncontroller.permission.ui.Category.ASK; @@ -52,6 +53,7 @@ import com.android.permissioncontroller.PermissionControllerStatsLog; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.ui.Category; +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity; import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel; import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory; import com.android.permissioncontroller.permission.utils.KotlinUtils; @@ -82,6 +84,8 @@ private static final String STORAGE_ALLOWED_SCOPED = "allowed_storage_scoped"; private static final int SHOW_LOAD_DELAY_MS = 200; + private static final int MENU_PERMISSION_USAGE = MENU_HIDE_SYSTEM + 1; + /** * Create a bundle with the arguments needed by this fragment * @@ -152,6 +156,10 @@ updateMenu(mViewModel.getShouldShowSystemLiveData().getValue()); } + if (shouldShowPermissionsDashboard()) { + menu.add(Menu.NONE, MENU_PERMISSION_USAGE, Menu.NONE, R.string.permission_usage_title); + } + HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, getClass().getName()); } @@ -167,6 +175,11 @@ case MENU_HIDE_SYSTEM: mViewModel.updateShowSystem(item.getItemId() == MENU_SHOW_SYSTEM); break; + case MENU_PERMISSION_USAGE: + getActivity().startActivity(new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE) + .setClass(getContext(), ManagePermissionsActivity.class) + .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName)); + return true; } return super.onOptionsItemSelected(item); }
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java index 60dfbe0..fe6ee35 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java
@@ -16,7 +16,11 @@ package com.android.permissioncontroller.permission.ui.handheld; +import static android.Manifest.permission_group.CAMERA; +import static android.Manifest.permission_group.MICROPHONE; + import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; +import static com.android.permissioncontroller.permission.debug.UtilsKt.getUsageDurationString; import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; import static com.android.permissioncontroller.permission.ui.handheld.AppPermissionFragment.GRANT_CATEGORY; import static com.android.permissioncontroller.permission.utils.KotlinUtilsKt.navigateSafe; @@ -39,8 +43,9 @@ import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.model.AppPermissionGroup; +import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; +import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog; import com.android.permissioncontroller.permission.utils.LocationUtils; @@ -95,6 +100,18 @@ } /** + * Sets this preference's right icon. + * + * Note that this must be called before preference layout to take effect. + * + * @param widgetIcon the icon to use. + */ + public void setRightIcon(@NonNull Drawable widgetIcon) { + mWidgetIcon = widgetIcon; + setWidgetLayoutResource(R.layout.image_view); + } + + /** * Sets this preference's left icon to be smaller than normal. * * Note that this must be called before preference layout to take effect. @@ -128,6 +145,56 @@ setSummary(""); } + /** + * Sets this preference's summary based on its permission usage. + * + * @param groupUsage the usage information + * @param accessTimeStr the string representing the last access time + */ + public void setUsageSummary(@NonNull GroupUsage groupUsage, @NonNull String accessTimeStr) { + long backgroundAccessCount = groupUsage.getBackgroundAccessCount(); + long duration = 0; + String groupName = groupUsage.getGroup().getName(); + if (groupName.equals(CAMERA) || groupName.equals(MICROPHONE)) { + duration = groupUsage.getAccessDuration(); + } + if (backgroundAccessCount == 0) { + long numForegroundAccesses = groupUsage.getForegroundAccessCount(); + if (duration == 0) { + setSummary(mContext.getResources().getQuantityString( + R.plurals.permission_usage_summary, (int) numForegroundAccesses, + accessTimeStr, numForegroundAccesses)); + } else { + setSummary(mContext.getResources().getQuantityString( + R.plurals.permission_usage_summary_duration, (int) numForegroundAccesses, + accessTimeStr, numForegroundAccesses, + getUsageDurationString(mContext, groupUsage))); + } + } else { + long numAccesses = groupUsage.getAccessCount(); + if (duration == 0) { + setSummary(mContext.getResources().getQuantityString( + R.plurals.permission_usage_summary_background, (int) numAccesses, + accessTimeStr, numAccesses, backgroundAccessCount)); + } else { + setSummary(mContext.getResources().getQuantityString( + R.plurals.permission_usage_summary_background_duration, (int) numAccesses, + accessTimeStr, numAccesses, backgroundAccessCount, + getUsageDurationString(mContext, groupUsage))); + } + } + } + + /** + * Sets this preference to show the given icons to the left of its title. + * + * @param titleIcons the icons to show. + */ + public void setTitleIcons(@NonNull List<Integer> titleIcons) { + mTitleIcons = titleIcons; + setLayoutResource(R.layout.preference_usage); + } + @Override public void onBindViewHolder(PreferenceViewHolder holder) { if (mUseSmallerIcon) {
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionsFrameFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionsFrameFragment.java index 78fd84e..ccbbd26 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/PermissionsFrameFragment.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/PermissionsFrameFragment.java
@@ -39,8 +39,8 @@ private static final String LOG_TAG = PermissionsFrameFragment.class.getSimpleName(); static final int MENU_ALL_PERMS = Menu.FIRST + 1; - static final int MENU_SHOW_SYSTEM = Menu.FIRST + 2; - static final int MENU_HIDE_SYSTEM = Menu.FIRST + 3; + public static final int MENU_SHOW_SYSTEM = Menu.FIRST + 2; + public static final int MENU_HIDE_SYSTEM = Menu.FIRST + 3; private ViewGroup mPreferencesContainer;
diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java new file mode 100644 index 0000000..5a0120f --- /dev/null +++ b/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java
@@ -0,0 +1,386 @@ +/* + * Copyright (C) 2020 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; + +import static android.Manifest.permission_group.CAMERA; +import static android.Manifest.permission_group.LOCATION; +import static android.Manifest.permission_group.MICROPHONE; + +import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED; +import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS; +import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM; +import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldShowPermissionsDashboard; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.UserHandle; +import android.text.Html; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; +import androidx.preference.PreferenceFragmentCompat; + +import com.android.permissioncontroller.PermissionControllerStatsLog; +import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.data.OpAccess; +import com.android.permissioncontroller.permission.data.OpUsageLiveData; +import com.android.permissioncontroller.permission.debug.PermissionUsages; +import com.android.permissioncontroller.permission.model.AppPermissionGroup; +import com.android.permissioncontroller.permission.model.AppPermissionUsage; +import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; +import com.android.permissioncontroller.permission.model.legacy.PermissionApps; +import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; +import com.android.permissioncontroller.permission.utils.KotlinUtils; +import com.android.permissioncontroller.permission.utils.Utils; + +import java.text.Collator; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A dialog listing the currently uses of camera, microphone, and location. + */ +public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { + + // TODO: Replace with OPSTR... APIs + static final String PHONE_CALL = "android:phone_call_microphone"; + static final String VIDEO_CALL = "android:phone_call_camera"; + + private @NonNull PermissionUsages mPermissionUsages; + private boolean mPermissionUsagesLoaded; + private @Nullable AlertDialog mDialog; + private OpUsageLiveData mOpUsageLiveData; + private @Nullable Map<String, List<OpAccess>> mOpUsage; + private ArraySet<String> mSystemUsage = new ArraySet<>(0); + private long mStartTime; + + /** + * @return A new {@link ReviewOngoingUsageFragment} + */ + public static ReviewOngoingUsageFragment newInstance(long numMillis) { + ReviewOngoingUsageFragment fragment = new ReviewOngoingUsageFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS); + + mPermissionUsages = new PermissionUsages(getActivity()); + mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli()); + String[] permissions = new String[]{CAMERA, MICROPHONE}; + if (shouldShowPermissionsDashboard()) { + permissions = new String[] {CAMERA, LOCATION, MICROPHONE}; + } + ArrayList<String> appOps = new ArrayList<>(List.of(PHONE_CALL, VIDEO_CALL)); + mOpUsageLiveData = OpUsageLiveData.Companion.get(appOps, numMillis); + mOpUsageLiveData.observeStale(this, new Observer<Map<String, List<OpAccess>>>() { + @Override + public void onChanged(Map<String, List<OpAccess>> opUsage) { + if (mOpUsageLiveData.isStale()) { + return; + } + mOpUsage = opUsage; + mOpUsageLiveData.removeObserver(this); + + if (mPermissionUsagesLoaded) { + onPermissionUsagesLoaded(); + } + } + }); + mPermissionUsages.load(null, permissions, mStartTime, Long.MAX_VALUE, + PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), false, false, + this::onPermissionUsagesLoaded, false); + } + + private void onPermissionUsagesLoaded() { + mPermissionUsagesLoaded = true; + if (getActivity() == null || mOpUsage == null) { + return; + } + + List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages(); + + List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>(); + ArrayList<PermissionApp> permApps = new ArrayList<>(); + int numApps = appPermissionUsages.size(); + for (int appNum = 0; appNum < numApps; appNum++) { + AppPermissionUsage appUsage = appPermissionUsages.get(appNum); + + List<GroupUsage> usedGroups = new ArrayList<>(); + List<GroupUsage> appGroups = appUsage.getGroupUsages(); + int numGroups = appGroups.size(); + for (int groupNum = 0; groupNum < numGroups; groupNum++) { + GroupUsage groupUsage = appGroups.get(groupNum); + String groupName = groupUsage.getGroup().getName(); + + if (!groupUsage.isRunning()) { + if (groupUsage.getLastAccessDuration() == -1) { + if (groupUsage.getLastAccessTime() < mStartTime) { + continue; + } + } else { + // TODO: Warning: Only works for groups with a single permission as it is + // not guaranteed the last access time and duration refer to same permission + // in AppPermissionUsage#lastAccessAggregate + if (groupUsage.getLastAccessTime() + groupUsage.getLastAccessDuration() + < mStartTime) { + continue; + } + } + } + + if (Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) { + usedGroups.add(appGroups.get(groupNum)); + } else if (getContext().getSystemService(LocationManager.class).isProviderPackage( + appUsage.getPackageName()) + && (groupName.equals(CAMERA) || groupName.equals(MICROPHONE))) { + mSystemUsage.add(groupName); + } + } + + if (!usedGroups.isEmpty()) { + usages.add(Pair.create(appUsage, usedGroups)); + permApps.add(appUsage.getApp()); + } + } + + if (usages.isEmpty() && mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { + getActivity().finish(); + return; + } + + new PermissionApps.AppDataLoader(getActivity(), () -> showDialog(usages)) + .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()])); + } + + private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) + .setView(createDialogView(usages)) + .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) -> + PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, + PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS)) + .setOnDismissListener((dialog) -> getActivity().finish()); + mDialog = builder.create(); + mDialog.show(); + } + + /** + * Get a list of permission labels. + * + * @param groups map<perm group name, perm group label> + * + * @return A localized string with the list of permissions + */ + private CharSequence getListOfPermissionLabels(ArrayMap<String, CharSequence> groups) { + int numGroups = groups.size(); + + if (numGroups == 1) { + return groups.valueAt(0); + } else if (numGroups == 2 && groups.containsKey(MICROPHONE) && groups.containsKey(CAMERA)) { + // Special case camera + mic permission to be localization friendly + return getContext().getString(R.string.permgroup_list_microphone_and_camera); + } else { + // TODO: Use internationalization safe concatenation + + ArrayList<CharSequence> sortedGroups = new ArrayList<>(groups.values()); + Collator collator = Collator.getInstance( + getResources().getConfiguration().getLocales().get(0)); + sortedGroups.sort(collator); + + StringBuilder listBuilder = new StringBuilder(); + + for (int i = 0; i < numGroups; i++) { + listBuilder.append(sortedGroups.get(i)); + if (i < numGroups - 2) { + listBuilder.append(getString(R.string.ongoing_usage_dialog_separator)); + } else if (i < numGroups - 1) { + listBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator)); + } + } + + return listBuilder; + } + } + + private @NonNull View createDialogView( + @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) { + Context context = getActivity(); + LayoutInflater inflater = LayoutInflater.from(context); + View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null); + ViewGroup appsList = contentView.requireViewById(R.id.items_container); + + // Compute all of the permission group labels that were used. + ArrayMap<String, CharSequence> usedGroups = new ArrayMap<>(); + int numUsages = usages.size(); + for (int usageNum = 0; usageNum < numUsages; usageNum++) { + List<GroupUsage> groups = usages.get(usageNum).second; + int numGroups = groups.size(); + for (int groupNum = 0; groupNum < numGroups; groupNum++) { + AppPermissionGroup group = groups.get(groupNum).getGroup(); + usedGroups.put(group.getName(), group.getLabel()); + } + } + + TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header); + TextView otherUseContent = contentView.requireViewById(R.id.other_use_content); + TextView systemUseContent = contentView.requireViewById(R.id.system_use_content); + View otherUseSpacer = contentView.requireViewById(R.id.other_use_inside_spacer); + + if (mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { + otherUseHeader.setVisibility(View.GONE); + otherUseContent.setVisibility(View.GONE); + } + + if (numUsages == 0) { + otherUseHeader.setVisibility(View.GONE); + appsList.setVisibility(View.GONE); + } + + if (mOpUsage.isEmpty() || mSystemUsage.isEmpty()) { + otherUseSpacer.setVisibility(View.GONE); + } + + if (mOpUsage.isEmpty()) { + otherUseContent.setVisibility(View.GONE); + } + + if (mSystemUsage.isEmpty()) { + systemUseContent.setVisibility(View.GONE); + } + + if (!mOpUsage.isEmpty()) { + if (mOpUsage.containsKey(VIDEO_CALL) && mOpUsage.containsKey( + PHONE_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera), + 0)); + } else if (mOpUsage.containsKey(VIDEO_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_camera), 0)); + } else if (mOpUsage.containsKey(PHONE_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0)); + } + + if (mOpUsage.containsKey(VIDEO_CALL)) { + usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA)); + } + + if (mOpUsage.containsKey(PHONE_CALL)) { + usedGroups.put(MICROPHONE, + KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE)); + } + } + + if (!mSystemUsage.isEmpty()) { + if (mSystemUsage.contains(MICROPHONE) && mSystemUsage.contains(CAMERA)) { + systemUseContent.setText(getString(R.string.system_uses_microphone_and_camera)); + } else if (mSystemUsage.contains(CAMERA)) { + systemUseContent.setText(getString(R.string.system_uses_camera)); + } else if (mSystemUsage.contains(MICROPHONE) ) { + systemUseContent.setText(getString(R.string.system_uses_microphone)); + } + + for (String usage : mSystemUsage) { + usedGroups.put(usage, KotlinUtils.INSTANCE.getPermGroupLabel(context, usage)); + } + } + + // Add the layout for each app. + for (int usageNum = 0; usageNum < numUsages; usageNum++) { + Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum); + PermissionApp app = usage.first.getApp(); + List<GroupUsage> groups = usage.second; + + View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false); + + ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel()); + ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon()); + + // Add the icons for the groups this app used as long as multiple groups were used by + // some app. + if (usedGroups.size() > 1) { + ArrayMap<String, CharSequence> usedGroupsThisApp = new ArrayMap<>(); + + ViewGroup iconFrame = itemView.requireViewById(R.id.icons); + int numGroups = usages.get(usageNum).second.size(); + for (int groupNum = 0; groupNum < numGroups; groupNum++) { + AppPermissionGroup group = groups.get(groupNum).getGroup(); + + ViewGroup groupView = (ViewGroup) inflater.inflate(R.layout.image_view, null); + ((ImageView) groupView.requireViewById(R.id.icon)).setImageDrawable( + Utils.applyTint(context, group.getIconResId(), + android.R.attr.colorControlNormal)); + iconFrame.addView(groupView); + + usedGroupsThisApp.put(group.getName(), group.getLabel()); + } + iconFrame.setVisibility(View.VISIBLE); + + TextView permissionsList = itemView.requireViewById(R.id.permissionsList); + permissionsList.setText(getListOfPermissionLabels(usedGroupsThisApp)); + permissionsList.setVisibility(View.VISIBLE); + } + + itemView.setOnClickListener((v) -> { + String packageName = app.getPackageName(); + PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, + PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM); + UserHandle user = UserHandle.getUserHandleForUid(app.getUid()); + Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Intent.EXTRA_USER, user); + context.startActivity(intent); + mDialog.dismiss(); + }); + + appsList.addView(itemView); + } + + ((TextView) contentView.requireViewById(R.id.title)).setText( + getString(R.string.ongoing_usage_dialog_title, + getListOfPermissionLabels(usedGroups))); + + return contentView; + } + + @Override + public void onCreatePreferences(Bundle bundle, String s) { + // empty + } +}
diff --git a/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt b/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt index d2a316a..002c7cc 100644 --- a/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt +++ b/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
@@ -21,6 +21,8 @@ import android.app.AppOpsManager.MODE_IGNORED import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED import android.Manifest +import android.app.role.RoleManager +import android.content.Context.MODE_PRIVATE import android.os.Bundle import android.os.UserHandle import android.util.Log @@ -28,6 +30,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import com.android.permissioncontroller.Constants.ASSISTANT_RECORD_AUDIO_IS_USER_SENSITIVE_KEY +import com.android.permissioncontroller.Constants.PREFERENCES_FILE import com.android.permissioncontroller.PermissionControllerApplication import com.android.permissioncontroller.PermissionControllerStatsLog import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION @@ -67,6 +71,8 @@ val LOG_TAG: String = AppPermissionGroupsViewModel::class.java.simpleName } + val app = PermissionControllerApplication.get()!! + enum class PermSubtitle(val value: Int) { NONE(0), MEDIA_ONLY(1), @@ -176,8 +182,7 @@ fun setAutoRevoke(enabled: Boolean) { GlobalScope.launch(IPC) { - val aom = PermissionControllerApplication.get() - .getSystemService(AppOpsManager::class.java)!! + val aom = app.getSystemService(AppOpsManager::class.java)!! val uid = LightPackageInfoLiveData[packageName, user].getInitializedValue()?.uid if (uid != null) {
diff --git a/src/com/android/permissioncontroller/permission/utils/UserSensitiveFlagsUtils.kt b/src/com/android/permissioncontroller/permission/utils/UserSensitiveFlagsUtils.kt index 01f26f4..3cd9891 100644 --- a/src/com/android/permissioncontroller/permission/utils/UserSensitiveFlagsUtils.kt +++ b/src/com/android/permissioncontroller/permission/utils/UserSensitiveFlagsUtils.kt
@@ -56,14 +56,15 @@ private fun updateUserSensitiveForUidsInternal( uidsUserSensitivity: Map<Int, UidSensitivityState>, user: UserHandle, - callback: Runnable + callback: Runnable? ) { - val pm = Utils.getUserContext(PermissionControllerApplication.get(), user).packageManager + val userContext = Utils.getUserContext(PermissionControllerApplication.get(), user) + val pm = userContext.packageManager - for ((uid, uidState) in uidsUserSensitivity) { + for ((uid, uidState) in uidsUserSensitivity) { for (pkg in uidState.packages) { for (perm in pkg.requestedPermissions) { - val flags = uidState.permStates[perm] ?: continue + var flags = uidState.permStates[perm] ?: continue try { val oldFlags = pm.getPermissionFlags(perm, pkg.packageName, user) and @@ -83,7 +84,7 @@ } } } - callback.run() + callback?.run() } /** @@ -92,7 +93,8 @@ * @param uid The uid to be updated * @param callback A callback which will be executed when finished */ -fun updateUserSensitiveForUid(uid: Int, callback: Runnable) { +@JvmOverloads +fun updateUserSensitiveForUid(uid: Int, callback: Runnable? = null) { GlobalScope.launch(IPC) { val uidSensitivityState = UserSensitivityLiveData[uid].getInitializedValue() if (uidSensitivityState != null) { @@ -100,7 +102,7 @@ UserHandle.getUserHandleForUid(uid), callback) } else { Log.e(LOG_TAG, "No packages associated with uid $uid, not updating flags") - callback.run() + callback?.run() } } -} \ No newline at end of file +}
diff --git a/src/com/android/permissioncontroller/role/model/AssistantRoleBehavior.java b/src/com/android/permissioncontroller/role/model/AssistantRoleBehavior.java index cb5c301..975ed8e 100644 --- a/src/com/android/permissioncontroller/role/model/AssistantRoleBehavior.java +++ b/src/com/android/permissioncontroller/role/model/AssistantRoleBehavior.java
@@ -171,6 +171,14 @@ return hasAssistantActivity; } + @Override + public void grant(@NonNull Role role, @NonNull String packageName, @NonNull Context context) { + } + + @Override + public void revoke(@NonNull Role role, @NonNull String packageName, @NonNull Context context) { + } + private boolean isAssistantVoiceInteractionService(@NonNull PackageManager pm, @NonNull ServiceInfo si) { if (!android.Manifest.permission.BIND_VOICE_INTERACTION.equals(si.permission)) {
diff --git a/tests/inprocess/AndroidTest.xml b/tests/inprocess/AndroidTest.xml index 656b060..6f3b4c4 100644 --- a/tests/inprocess/AndroidTest.xml +++ b/tests/inprocess/AndroidTest.xml
@@ -39,6 +39,8 @@ value="/data/local/tmp/permissioncontroller/tests/inprocess/AppThatRequestsLocation.apk" /> <option name="push-file" key="AppThatUsesStoragePermission.apk" value="/data/local/tmp/permissioncontroller/tests/inprocess/AppThatUsesStoragePermission.apk" /> + <option name="push-file" key="AppThatUsesCameraPermission.apk" + value="/data/local/tmp/permissioncontroller/tests/inprocess/AppThatUsesCameraPermission.apk" /> <option name="push-file" key="AppThatDefinesAdditionalPermission.apk" value="/data/local/tmp/permissioncontroller/tests/inprocess/AppThatDefinesAdditionalPermission.apk" /> <option name="push-file" key="AppThatUsesAdditionalPermission.apk"
diff --git a/tests/inprocess/AppThatUsesCameraPermission/Android.bp b/tests/inprocess/AppThatUsesCameraPermission/Android.bp new file mode 100644 index 0000000..285c4f5 --- /dev/null +++ b/tests/inprocess/AppThatUsesCameraPermission/Android.bp
@@ -0,0 +1,25 @@ +// +// Copyright (C) 2020 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. +// + +android_test_helper_app { + name: "AppThatUsesCameraPermission", + + srcs: ["src/**/*.kt"], + + sdk_version: "current", + + test_suites: [ "device-tests" ], +}
diff --git a/tests/inprocess/AppThatUsesCameraPermission/AndroidManifest.xml b/tests/inprocess/AppThatUsesCameraPermission/AndroidManifest.xml new file mode 100644 index 0000000..557c34e --- /dev/null +++ b/tests/inprocess/AppThatUsesCameraPermission/AndroidManifest.xml
@@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.permissioncontroller.tests.appthatrequestpermission"> + <uses-permission android:name="android.permission.CAMERA"/> + + <application android:label="CameraRequestApp"> + <activity android:name=".DummyActivity" + android:exported="true"/> + </application> +</manifest> +
diff --git a/tests/inprocess/AppThatUsesCameraPermission/src/com/android/permissioncontroller/tests/appthatrequestpermission/DummyActivity.kt b/tests/inprocess/AppThatUsesCameraPermission/src/com/android/permissioncontroller/tests/appthatrequestpermission/DummyActivity.kt new file mode 100644 index 0000000..2a4900c --- /dev/null +++ b/tests/inprocess/AppThatUsesCameraPermission/src/com/android/permissioncontroller/tests/appthatrequestpermission/DummyActivity.kt
@@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 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.tests.appthatrequestpermission + +import android.app.Activity + +class DummyActivity : Activity() { +} \ No newline at end of file
diff --git a/tests/inprocess/src/com/android/permissioncontroller/permission/PermissionHub2Test.kt b/tests/inprocess/src/com/android/permissioncontroller/permission/PermissionHub2Test.kt new file mode 100644 index 0000000..da43d20 --- /dev/null +++ b/tests/inprocess/src/com/android/permissioncontroller/permission/PermissionHub2Test.kt
@@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 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 + +import android.app.AppOpsManager +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.OPSTR_CAMERA +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.provider.DeviceConfig +import android.provider.DeviceConfig.NAMESPACE_PRIVACY +import androidx.test.platform.app.InstrumentationRegistry +import com.android.compatibility.common.util.SystemUtil +import com.android.compatibility.common.util.SystemUtil.eventually +import com.android.compatibility.common.util.SystemUtil.runShellCommand +import com.google.common.truth.Truth.assertThat +import org.junit.AfterClass +import org.junit.BeforeClass + +/** + * Super class with utilities for testing permission hub 2 code + */ +open class PermissionHub2Test { + private val APP = "com.android.permissioncontroller.tests.appthatrequestpermission" + + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val context = instrumentation.targetContext + + companion object { + private const val PROPERTY_PERMISSIONS_HUB_2_ENABLED = "permissions_hub_2_enabled" + + private var wasPermissionHubEnabled = false + + @JvmStatic + @BeforeClass + fun enablePermissionHub2() { + wasPermissionHubEnabled = DeviceConfig.getBoolean(NAMESPACE_PRIVACY, + PROPERTY_PERMISSIONS_HUB_2_ENABLED, false) + + if (!wasPermissionHubEnabled) { + runShellCommand( + "device_config put privacy $PROPERTY_PERMISSIONS_HUB_2_ENABLED true") + } + } + + @JvmStatic + @AfterClass + fun disablePermissionHub2() { + if (!wasPermissionHubEnabled) { + runShellCommand( + "device_config put privacy $PROPERTY_PERMISSIONS_HUB_2_ENABLED false") + } + } + } + + /** + * Make {@value #APP} access the camera + */ + protected fun accessCamera() { + // App needs to be in foreground to be able to access camera + context.startActivity( + Intent().setComponent(ComponentName.createRelative(APP, ".DummyActivity")) + .setFlags(FLAG_ACTIVITY_NEW_TASK)) + + eventually { + assertThat( + SystemUtil.callWithShellPermissionIdentity { + context.getSystemService(AppOpsManager::class.java).startOp( + OPSTR_CAMERA, context.packageManager.getPackageUid(APP, 0), + APP, null, null) + }).isEqualTo(MODE_ALLOWED) + } + } +} \ No newline at end of file
diff --git a/tests/inprocess/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragmentTest.kt b/tests/inprocess/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragmentTest.kt new file mode 100644 index 0000000..ecffcb2 --- /dev/null +++ b/tests/inprocess/src/com/android/permissioncontroller/permission/debug/PermissionUsageFragmentTest.kt
@@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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.debug + +import android.Manifest.permission.CAMERA +import android.content.Intent +import android.permission.cts.PermissionUtils.grantPermission +import android.permission.cts.PermissionUtils.install +import android.permission.cts.PermissionUtils.uninstallApp +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.ActivityTestRule +import com.android.compatibility.common.util.SystemUtil.eventually +import com.android.permissioncontroller.R +import com.android.permissioncontroller.getPreferenceSummary +import com.android.permissioncontroller.permission.PermissionHub2Test +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity +import com.android.permissioncontroller.scrollToPreference +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Simple tests for {@link PermissionUsageFragment} + */ +@RunWith(AndroidJUnit4::class) +class PermissionUsageFragmentTest : PermissionHub2Test() { + private val APK = + "/data/local/tmp/permissioncontroller/tests/inprocess/AppThatUsesCameraPermission.apk" + private val APP = "com.android.permissioncontroller.tests.appthatrequestpermission" + private val APP_LABEL = "CameraRequestApp" + + @get:Rule + val managePermissionsActivity = object : ActivityTestRule<ManagePermissionsActivity>( + ManagePermissionsActivity::class.java) { + override fun getActivityIntent() = Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE) + + override fun beforeActivityLaunched() { + install(APK) + grantPermission(APP, CAMERA) + + accessCamera() + } + } + + @Test + fun cameraAccessShouldBeShown() { + eventually { + try { + scrollToPreference(APP_LABEL) + } catch (e: Exception) { + onView(withContentDescription(R.string.permission_usage_refresh)).perform(click()) + throw e + } + } + + assertThat(getPreferenceSummary(APP_LABEL)).isEqualTo("Camera") + + // Expand usage + onView(withText(APP_LABEL)).perform(click()) + } + + @After + fun uninstallTestApp() { + uninstallApp(APP) + } +} \ No newline at end of file
diff --git a/tests/inprocess/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragmentTest.kt b/tests/inprocess/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragmentTest.kt new file mode 100644 index 0000000..bd0d1f2 --- /dev/null +++ b/tests/inprocess/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragmentTest.kt
@@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 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 + +import android.Manifest.permission.CAMERA +import android.permission.cts.PermissionUtils.grantPermission +import android.permission.cts.PermissionUtils.install +import android.permission.cts.PermissionUtils.uninstallApp +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.rule.ActivityTestRule +import com.android.permissioncontroller.permission.PermissionHub2Test +import com.android.permissioncontroller.permission.ui.ReviewOngoingUsageActivity +import org.junit.After +import org.junit.Rule +import org.junit.Test + +/** + * Simple tests for {@link ReviewOngoingUsageFragment} + */ +class ReviewOngoingUsageFragmentTest : PermissionHub2Test() { + private val APK = + "/data/local/tmp/permissioncontroller/tests/inprocess/AppThatUsesCameraPermission.apk" + private val APP = "com.android.permissioncontroller.tests.appthatrequestpermission" + private val APP_LABEL = "CameraRequestApp" + + @get:Rule + val managePermissionsActivity = object : ActivityTestRule<ReviewOngoingUsageActivity>( + ReviewOngoingUsageActivity::class.java) { + override fun beforeActivityLaunched() { + install(APK) + grantPermission(APP, CAMERA) + + accessCamera() + } + } + + @Test + fun cameraAccessShouldBeShown() { + // Click on app entry + onView(withText(APP_LABEL)) + .inRoot(isDialog()) + .perform(click()) + } + + @After + fun uninstallTestApp() { + uninstallApp(APP) + } +} \ No newline at end of file
diff --git a/tests/outofprocess/src/com/android/permissioncontroller/tests/outofprocess/DumpTest.kt b/tests/outofprocess/src/com/android/permissioncontroller/tests/outofprocess/DumpTest.kt index 0d8a6fb..4963a46 100644 --- a/tests/outofprocess/src/com/android/permissioncontroller/tests/outofprocess/DumpTest.kt +++ b/tests/outofprocess/src/com/android/permissioncontroller/tests/outofprocess/DumpTest.kt
@@ -24,6 +24,7 @@ import com.google.common.truth.Truth.assertThat import com.google.protobuf.InvalidProtocolBufferException import org.junit.Assert.fail +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import java.nio.charset.StandardCharsets.UTF_8 @@ -49,12 +50,22 @@ @Test fun autoRevokeDumpHasCurrentUser() { - assertThat(getDump().autoRevoke.usersList.map { it.userId }).contains(myUserId()) + val dump = getDump() + + // Sometimes the dump takes to long to get generated, esp. on low end devices + assumeTrue(dump.autoRevoke.usersList.isNotEmpty()) + + assertThat(dump.autoRevoke.usersList.map { it.userId }).contains(myUserId()) } @Test fun autoRevokeDumpHasAndroidPackage() { - assertThat(getDump().autoRevoke.usersList[myUserId()].packagesList.map { it.packageName }) + val dump = getDump() + + // Sometimes the dump takes to long to get generated, esp. on low end devices + assumeTrue(dump.autoRevoke.usersList.isNotEmpty()) + + assertThat(dump.autoRevoke.usersList[myUserId()].packagesList.map { it.packageName }) .contains(OS_PKG) } }