summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/drawable/ic_info.xml25
-rw-r--r--res/drawable/ic_settings.xml34
-rw-r--r--res/drawable/list_divider_dark.xml24
-rw-r--r--res/layout/app_permission.xml145
-rw-r--r--res/values/strings.xml24
-rw-r--r--src/com/android/packageinstaller/permission/model/AppPermissionUsage.java20
-rw-r--r--src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java17
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java666
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/AppPermissionUsageFragment.java2
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/PermissionUsagePreference.java3
10 files changed, 958 insertions, 2 deletions
diff --git a/res/drawable/ic_info.xml b/res/drawable/ic_info.xml
new file mode 100644
index 00000000..365bd338
--- /dev/null
+++ b/res/drawable/ic_info.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2018 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_settings.xml b/res/drawable/ic_settings.xml
new file mode 100644
index 00000000..28fb10df
--- /dev/null
+++ b/res/drawable/ic_settings.xml
@@ -0,0 +1,34 @@
+<!--
+ Copyright (C) 2018 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21.4 14.2l-1.94-1.45c.03-.25 .04 -.5 .04 -.76s-.01-.51-.04-.76L21.4 9.8c.42-.31
+.52 -.94 .24 -1.41l-1.6-2.76c-.28-.48-.88-.7-1.36-.5l-2.14 .91
+c-.48-.37-1.01-.68-1.57-.92l-.27-2.2c-.06-.52-.56-.92-1.11-.92h-3.18c-.55 0-1.05
+.4 -1.11 .92 l-.26 2.19c-.57 .24 -1.1 .55 -1.58 .92 l-2.14-.91c-.48-.2-1.08 .02
+-1.36 .5 l-1.6 2.76c-.28 .48 -.18 1.1 .24 1.42l1.94 1.45c-.03 .24 -.04 .49 -.04
+.75 s.01 .51 .04 .76 L2.6 14.2c-.42 .31 -.52 .94 -.24 1.41l1.6 2.76c.28 .48 .88
+.7 1.36 .5 l2.14-.91c.48 .37 1.01 .68 1.57 .92 l.27 2.19c.06 .53 .56 .93 1.11
+.93 h3.18c.55 0 1.04-.4 1.11-.92l.27-2.19c.56-.24 1.09-.55 1.57-.92l2.14 .91
+c.48 .2 1.08-.02 1.36-.5l1.6-2.76c.28-.48 .18 -1.1-.24-1.42zM12 15.5c-1.93
+0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
+</vector> \ No newline at end of file
diff --git a/res/drawable/list_divider_dark.xml b/res/drawable/list_divider_dark.xml
new file mode 100644
index 00000000..c5af982b
--- /dev/null
+++ b/res/drawable/list_divider_dark.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#64000000" />
+ <size
+ android:height="1dp"
+ android:width="1dp" />
+</shape> \ No newline at end of file
diff --git a/res/layout/app_permission.xml b/res/layout/app_permission.xml
new file mode 100644
index 00000000..bfe0f781
--- /dev/null
+++ b/res/layout/app_permission.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <include layout="@layout/header" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:theme="@*android:style/Theme.DeviceDefault.PermissionGrant">
+
+ <TextView
+ android:id="@+id/permission_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAllCaps="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <RadioGroup
+ android:id="@+id/radiogroup"
+ android:animateLayoutChanges="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/PermissionGrantRadioGroup" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/allow_radio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_weight="1"
+ android:text="@string/app_permission_button_allow_always"
+ style="@android:attr/radioButtonStyle" />
+
+ <LinearLayout
+ android:id="@+id/two_target_divider"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/list_divider_dark" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:minWidth="64dp"
+ android:orientation="vertical" />
+
+ </LinearLayout>
+
+ <RadioButton
+ android:id="@+id/foreground_only_radio_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/app_permission_button_allow_foreground"
+ style="@android:attr/radioButtonStyle" />
+
+ <RadioButton
+ android:id="@+id/deny_radio_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/app_permission_button_deny"
+ style="@android:attr/radioButtonStyle" />
+
+ </RadioGroup>
+
+ <TextView
+ android:id="@+id/permission_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp" />
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height=".75dp"
+ android:layout_marginTop="20dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/dividerHorizontal"/>
+
+ <TextView
+ android:id="@+id/usage_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:paddingTop="16dp"
+ android:paddingStart="32dp"
+ android:paddingEnd="32dp"
+ android:textColor="?android:attr/textColorSecondary"/>
+
+ <TextView
+ android:id="@+id/usage_link"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:paddingTop="16dp"
+ android:paddingStart="32dp"
+ android:paddingEnd="32dp"
+ android:textColor="?android:attr/colorAccent"
+ android:clickable="true" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 611ccd67..cf7dd5fb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -270,6 +270,30 @@
<!-- Summary for showing a single permission access [CHAR LIMIT=60] -->
<string name="app_permission_usage_summary"><xliff:g id="time" example="2 hours">%1$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>
+
+ <!-- Title for the dialog button to allow a permission grant when you can also only allow in the foreground. [CHAR LIMIT=60] -->
+ <string name="app_permission_button_allow_always">Allow all the time</string>
+
+ <!-- Title for the dialog button to allow a permission grant only when the app is in the foreground. [CHAR LIMIT=60] -->
+ <string name="app_permission_button_allow_foreground">Allow only while the app is in use</string>
+
+ <!-- Title for the dialog button to deny a permission grant. [CHAR LIMIT=60] -->
+ <string name="app_permission_button_deny">Deny</string>
+
+ <!-- Title for app permission [CHAR LIMIT=30] -->
+ <string name="app_permission_title"><xliff:g id="perm" example="location">%1$s</xliff:g> permission</string>
+
+ <!-- Description for showing an app's permission [CHAR LIMIT=60] -->
+ <string name="app_permission_header"><xliff:g id="perm" example="location">%1$s</xliff:g> access for <xliff:g id="app" example="Maps">%2$s</xliff:g></string>
+
+ <!-- Summary for showing a single permission access [CHAR LIMIT=60] -->
+ <string name="app_permission_footer_usage_summary"><xliff:g id="app" example="Maps">%1$s</xliff:g> accessed your <xliff:g id="perm" example="location">%2$s</xliff:g> <xliff:g id="time" example="2 hours">%3$s</xliff:g> ago.</string>
+
+ <!-- Summary for linking to the page that shows an app's use of permissions [CHAR LIMIT=none] -->
+ <string name="app_permission_footer_usage_link">View detailed permissions usage</string>
+
<!-- Time in days -->
<plurals name="days">
<item quantity="one">1 day</item>
diff --git a/src/com/android/packageinstaller/permission/model/AppPermissionUsage.java b/src/com/android/packageinstaller/permission/model/AppPermissionUsage.java
index 95c90669..66d032c9 100644
--- a/src/com/android/packageinstaller/permission/model/AppPermissionUsage.java
+++ b/src/com/android/packageinstaller/permission/model/AppPermissionUsage.java
@@ -20,6 +20,9 @@ import android.app.AppOpsManager;
import androidx.annotation.NonNull;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
/**
* A single instance of an app accessing a permission.
*/
@@ -56,4 +59,21 @@ public final class AppPermissionUsage {
public @NonNull CharSequence getPermissionGroupLabel() {
return mPermissionGroupLabel;
}
+
+ /**
+ * Get the name of the permission (not the group) this represents.
+ *
+ * @return the name of the permission this represents.
+ */
+ public String getPermissionName() {
+ // TODO: Replace reflection with a proper API (probably in AppOpsManager).
+ try {
+ Method getOpMethod = AppOpsManager.OpEntry.class.getMethod("getOp");
+ Method opToPermissionMethod = AppOpsManager.class.getMethod("opToPermission",
+ int.class);
+ return (String) opToPermissionMethod.invoke(null, (int) getOpMethod.invoke(mOp));
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ return null;
+ }
+ }
}
diff --git a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
index 20b668e2..0dc680ec 100644
--- a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
+++ b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
@@ -131,6 +131,23 @@ public final class ManagePermissionsActivity extends FragmentActivity {
.AppPermissionUsageFragment.newInstance(packageName);
} break;
+ case Intent.ACTION_MANAGE_APP_PERMISSION: {
+ String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+ if (packageName == null) {
+ Log.i(LOG_TAG, "Missing mandatory argument EXTRA_PACKAGE_NAME");
+ finish();
+ return;
+ }
+ permissionName = getIntent().getStringExtra(Intent.EXTRA_PERMISSION_NAME);
+ if (permissionName == null) {
+ Log.i(LOG_TAG, "Missing mandatory argument EXTRA_PERMISSION_NAME");
+ finish();
+ return;
+ }
+ androidXFragment = com.android.packageinstaller.permission.ui.handheld
+ .AppPermissionFragment.newInstance(packageName, permissionName);
+ } break;
+
default: {
Log.w(LOG_TAG, "Unrecognized action " + action);
finish();
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java
new file mode 100644
index 00000000..a903f3be
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.permission.ui.handheld;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissionUsage;
+import com.android.packageinstaller.permission.utils.IconDrawableFactory;
+import com.android.packageinstaller.permission.utils.LocationUtils;
+import com.android.packageinstaller.permission.utils.Utils;
+import com.android.permissioncontroller.R;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+
+/**
+ * Show and manage a single permission group for an app.
+ *
+ * <p>Allows the user to control whether the app is granted the permission.
+ */
+public class AppPermissionFragment extends PermissionsFrameFragment {
+ private static final String LOG_TAG = "AppPermissionFragment";
+
+ @Retention(SOURCE)
+ @IntDef(value = {CHANGE_FOREGROUND, CHANGE_BACKGROUND}, flag = true)
+ @interface ChangeTarget {}
+ static final int CHANGE_FOREGROUND = 1;
+ static final int CHANGE_BACKGROUND = 2;
+ static final int CHANGE_BOTH = CHANGE_FOREGROUND | CHANGE_BACKGROUND;
+
+ private @NonNull AppPermissionGroup mGroup;
+
+ private @NonNull RadioGroup mRadioGroup;
+ private @NonNull RadioButton mAlwaysButton;
+ private @NonNull RadioButton mForegroundOnlyButton;
+ private @NonNull RadioButton mDenyButton;
+ private @NonNull View mDivider;
+ private @NonNull ViewGroup mWidgetFrame;
+ private @NonNull TextView mPermissionDetails;
+
+ private boolean mHasConfirmedRevoke;
+
+ /**
+ * @return A new fragment
+ */
+ public static @NonNull AppPermissionFragment newInstance(@NonNull String packageName,
+ @NonNull String permissionName) {
+ AppPermissionFragment fragment = new AppPermissionFragment();
+ Bundle arguments = new Bundle();
+ arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
+ arguments.putString(Intent.EXTRA_PERMISSION_NAME, permissionName);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ ActionBar ab = getActivity().getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ }
+
+ String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
+ Activity activity = getActivity();
+ Context context = getPreferenceManager().getContext();
+ mGroup = AppPermissionGroup.create(context, getPackageInfo(activity, packageName),
+ getArguments().getString(Intent.EXTRA_PERMISSION_NAME), false);
+
+ if (mGroup == null || !Utils.shouldShowPermission(context, mGroup)) {
+ Log.i(LOG_TAG, "Illegal group: " + (mGroup == null ? "null" : mGroup.getName()));
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ return;
+ }
+
+ mHasConfirmedRevoke = false;
+
+ activity.setTitle(context.getString(R.string.app_permission_title, mGroup.getLabel()));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Context context = getPreferenceManager().getContext();
+ ViewGroup root = (ViewGroup) inflater.inflate(R.layout.app_permission, container, false);
+
+ if (mGroup == null) {
+ return root;
+ }
+
+ String appLabel = Utils.getAppLabel(mGroup.getApp().applicationInfo, context);
+
+ ((ImageView) root.requireViewById(R.id.icon)).setImageDrawable(getAppIcon());
+ ((TextView) root.requireViewById(R.id.name)).setText(appLabel);
+ root.requireViewById(R.id.info).setVisibility(View.GONE);
+
+ ((TextView) root.requireViewById(R.id.permission_message)).setText(
+ context.getString(R.string.app_permission_header, mGroup.getLabel(), appLabel));
+
+ ((TextView) root.requireViewById(R.id.usage_summary)).setText(
+ context.getString(
+ R.string.app_permission_footer_usage_summary,
+ appLabel,
+ mGroup.getLabel().toString().toLowerCase(), getUsageTimeDiffString()));
+
+ TextView usageLink = root.requireViewById(R.id.usage_link);
+ usageLink.setText(context.getString(R.string.app_permission_footer_usage_link));
+ usageLink.setOnClickListener((v) -> {
+ Intent intent = new Intent(Intent.ACTION_REVIEW_APP_PERMISSION_USAGE);
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mGroup.getApp().packageName);
+ context.startActivity(intent);
+ });
+
+ mRadioGroup = root.requireViewById(R.id.radiogroup);
+ mAlwaysButton = root.requireViewById(R.id.allow_radio_button);
+ mForegroundOnlyButton = root.requireViewById(R.id.foreground_only_radio_button);
+ mDenyButton = root.requireViewById(R.id.deny_radio_button);
+ mDivider = root.requireViewById(R.id.two_target_divider);
+ mWidgetFrame = root.requireViewById(R.id.widget_frame);
+ mPermissionDetails = root.requireViewById(R.id.permission_details);
+
+ updateButtons();
+
+ return root;
+ }
+
+ /**
+ * Build a string representing the amount of time passed since the most recent permission usage
+ * by this AppPermissionGroup.
+ *
+ * @return a string representing the amount of time since this app's most recent permission
+ * usage.
+ */
+ private @NonNull String getUsageTimeDiffString() {
+ long mostRecentTime = 0;
+ List<AppPermissionUsage> groupUsages = mGroup.getAppPermissionUsage();
+ int numUsages = groupUsages.size();
+ for (int usageNum = 0; usageNum < numUsages; usageNum++) {
+ AppPermissionUsage usage = groupUsages.get(usageNum);
+ mostRecentTime = Math.max(mostRecentTime, usage.getTime());
+ }
+ if (mostRecentTime == 0) {
+ Log.e(LOG_TAG, "Unexpected usage time of 0.");
+ }
+ return Utils.getTimeDiffStr(getContext(), System.currentTimeMillis() - mostRecentTime);
+ }
+
+ private void updateButtons() {
+ // Reset everything to the "default" state: tri-state buttons are shown with exactly one
+ // selected and no special messages.
+ mDivider.setVisibility(View.GONE);
+ mWidgetFrame.setVisibility(View.GONE);
+ mPermissionDetails.setVisibility(View.GONE);
+
+ if (mGroup.areRuntimePermissionsGranted()) {
+ if (!mGroup.hasPermissionWithBackgroundMode()
+ || (mGroup.getBackgroundPermissions() != null
+ && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted())) {
+ setCheckedButton(mAlwaysButton);
+ } else {
+ setCheckedButton(mForegroundOnlyButton);
+ }
+ } else {
+ setCheckedButton(mDenyButton);
+ }
+
+ mAlwaysButton.setOnClickListener((v) -> requestChange(true, CHANGE_BOTH));
+ mForegroundOnlyButton.setOnClickListener((v) -> {
+ requestChange(false, CHANGE_BACKGROUND);
+ requestChange(true, CHANGE_FOREGROUND);
+ });
+ mDenyButton.setOnClickListener((v) -> requestChange(false, CHANGE_BOTH));
+
+ // Handle the UI for various special cases.
+ Context context = getContext();
+ if (isPolicyFullyFixed() || isForegroundDisabledByPolicy()) {
+ // Disable changing permissions and potentially show administrator message.
+ EnforcedAdmin admin = getAdmin();
+ if (admin != null) {
+ mAlwaysButton.setEnabled(false);
+ mForegroundOnlyButton.setEnabled(false);
+ mDenyButton.setEnabled(false);
+
+ showRightIcon(R.drawable.ic_info);
+ mWidgetFrame.setOnClickListener(v ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin)
+ );
+ } else {
+ mAlwaysButton.setEnabled(false);
+ mForegroundOnlyButton.setEnabled(false);
+ mDenyButton.setEnabled(false);
+ }
+ updateDetailForFixedByPolicyPermissionGroup();
+ } else if (Utils.areGroupPermissionsIndividuallyControlled(context, mGroup.getName())) {
+ // If the permissions are individually controlled, also show a link to the page that
+ // lets you control them.
+ mDivider.setVisibility(View.VISIBLE);
+ showRightIcon(R.drawable.ic_settings);
+ mWidgetFrame.setOnClickListener(v -> showAllPermissions(mGroup.getName()));
+ } else {
+ if (mGroup.hasPermissionWithBackgroundMode()) {
+ if (mGroup.getBackgroundPermissions() == null) {
+ // The group has background permissions but the app did not request any. I.e.
+ // The app can only switch between 'never" and "only in foreground".
+ mAlwaysButton.setEnabled(false);
+
+ mDenyButton.setOnClickListener((v) -> requestChange(false, CHANGE_FOREGROUND));
+ } else {
+ if (isBackgroundPolicyFixed()) {
+ // If background policy is fixed, we only allow switching the foreground.
+ // Note that this assumes that the background policy is fixed to deny,
+ // since if it is fixed to grant, so is the foreground.
+ mAlwaysButton.setEnabled(false);
+ setCheckedButton(mForegroundOnlyButton);
+
+ mDenyButton.setOnClickListener(
+ (v) -> requestChange(false, CHANGE_FOREGROUND));
+
+ updateDetailForFixedByPolicyPermissionGroup();
+ } else if (isForegroundPolicyFixed()) {
+ // Foreground permissions are fixed to allow (the first case above handles
+ // fixing to deny), so we only allow toggling background permissions.
+ mDenyButton.setEnabled(false);
+
+ mAlwaysButton.setOnClickListener(
+ (v) -> requestChange(true, CHANGE_BACKGROUND));
+ mForegroundOnlyButton.setOnClickListener(
+ (v) -> requestChange(false, CHANGE_BACKGROUND));
+
+ updateDetailForFixedByPolicyPermissionGroup();
+ } else {
+ // The default tri-state case is handled by default.
+ }
+ }
+
+ } else {
+ // The default bi-state.
+ mForegroundOnlyButton.setVisibility(View.GONE);
+ mAlwaysButton.setText(context.getString(R.string.app_permission_button_allow));
+ }
+ }
+ }
+
+ /**
+ * Set the given button as the only checked button in the radio group.
+ *
+ * @param button the button that should be checked.
+ */
+ private void setCheckedButton(@NonNull RadioButton button) {
+ mRadioGroup.clearCheck();
+ button.setChecked(true);
+ if (button != mAlwaysButton) {
+ mAlwaysButton.setChecked(false);
+ }
+ if (button != mForegroundOnlyButton) {
+ mForegroundOnlyButton.setChecked(false);
+ }
+ if (button != mDenyButton) {
+ mDenyButton.setChecked(false);
+ }
+ }
+
+ /**
+ * Show the given icon on the right of the first radio button.
+ *
+ * @param iconId the resourceId of the drawable to use.
+ */
+ private void showRightIcon(int iconId) {
+ mWidgetFrame.removeAllViews();
+ ImageView imageView = new ImageView(getPreferenceManager().getContext());
+ imageView.setImageResource(iconId);
+ mWidgetFrame.addView(imageView);
+ mWidgetFrame.setVisibility(View.VISIBLE);
+ }
+
+ private static @Nullable PackageInfo getPackageInfo(@NonNull Activity activity,
+ @NonNull String packageName) {
+ try {
+ return activity.getPackageManager().getPackageInfo(
+ packageName, PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.i(LOG_TAG, "No package: " + activity.getCallingPackage(), e);
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ return null;
+ }
+ }
+
+ /**
+ * Is any foreground permissions of this group fixed by the policy, i.e. not changeable by the
+ * user.
+ *
+ * @return {@code true} iff any foreground permission is fixed
+ */
+ private boolean isForegroundPolicyFixed() {
+ return mGroup.isPolicyFixed();
+ }
+
+ /**
+ * Is any background permissions of this group fixed by the policy, i.e. not changeable by the
+ * user.
+ *
+ * @return {@code true} iff any background permission is fixed
+ */
+ private boolean isBackgroundPolicyFixed() {
+ return mGroup.getBackgroundPermissions() != null
+ && mGroup.getBackgroundPermissions().isPolicyFixed();
+ }
+
+ /**
+ * Are there permissions fixed, so that the user cannot change the preference at all?
+ *
+ * @return {@code true} iff the permissions of this group are fixed
+ */
+ private boolean isPolicyFullyFixed() {
+ return isForegroundPolicyFixed() && (mGroup.getBackgroundPermissions() == null
+ || isBackgroundPolicyFixed());
+ }
+
+ /**
+ * Is the foreground part of this group disabled. If the foreground is disabled, there is no
+ * need to possible grant background access.
+ *
+ * @return {@code true} iff the permissions of this group are fixed
+ */
+ private boolean isForegroundDisabledByPolicy() {
+ return isForegroundPolicyFixed() && !mGroup.areRuntimePermissionsGranted();
+ }
+
+ /**
+ * Get the app that acts as admin for this profile.
+ *
+ * @return The admin or {@code null} if there is no admin.
+ */
+ private @Nullable EnforcedAdmin getAdmin() {
+ return RestrictedLockUtils.getProfileOrDeviceOwner(getContext(), mGroup.getUser());
+ }
+
+ /**
+ * Update the detail of a permission group that is at least partially fixed by policy.
+ */
+ private void updateDetailForFixedByPolicyPermissionGroup() {
+ EnforcedAdmin admin = getAdmin();
+ AppPermissionGroup backgroundGroup = mGroup.getBackgroundPermissions();
+
+ boolean hasAdmin = admin != null;
+
+ if (isForegroundDisabledByPolicy()) {
+ // Permission is fully controlled by policy and cannot be switched
+
+ if (hasAdmin) {
+ setDetail(R.string.disabled_by_admin);
+ } else {
+ // Disabled state will be displayed by switch, so no need to add text for that
+ setDetail(R.string.permission_summary_enforced_by_policy);
+ }
+ } else if (isPolicyFullyFixed()) {
+ // Permission is fully controlled by policy and cannot be switched
+
+ if (backgroundGroup == null) {
+ if (hasAdmin) {
+ setDetail(R.string.enabled_by_admin);
+ } else {
+ // Enabled state will be displayed by switch, so no need to add text for
+ // that
+ setDetail(R.string.permission_summary_enforced_by_policy);
+ }
+ } else {
+ if (backgroundGroup.areRuntimePermissionsGranted()) {
+ if (hasAdmin) {
+ setDetail(R.string.enabled_by_admin);
+ } else {
+ // Enabled state will be displayed by switch, so no need to add text for
+ // that
+ setDetail(R.string.permission_summary_enforced_by_policy);
+ }
+ } else {
+ if (hasAdmin) {
+ setDetail(
+ R.string.permission_summary_enabled_by_admin_foreground_only);
+ } else {
+ setDetail(
+ R.string.permission_summary_enabled_by_policy_foreground_only);
+ }
+ }
+ }
+ } else {
+ // Part of the permission group can still be switched
+
+ if (isBackgroundPolicyFixed()) {
+ if (backgroundGroup.areRuntimePermissionsGranted()) {
+ if (hasAdmin) {
+ setDetail(R.string.permission_summary_enabled_by_admin_background_only);
+ } else {
+ setDetail(R.string.permission_summary_enabled_by_policy_background_only);
+ }
+ } else {
+ if (hasAdmin) {
+ setDetail(R.string.permission_summary_disabled_by_admin_background_only);
+ } else {
+ setDetail(R.string.permission_summary_disabled_by_policy_background_only);
+ }
+ }
+ } else if (isForegroundPolicyFixed()) {
+ if (hasAdmin) {
+ setDetail(R.string.permission_summary_enabled_by_admin_foreground_only);
+ } else {
+ setDetail(R.string.permission_summary_enabled_by_policy_foreground_only);
+ }
+ }
+ }
+ }
+
+ /**
+ * Show the given string as informative text below the radio buttons.
+ * @param strId the resourceId of the string to display.
+ */
+ private void setDetail(int strId) {
+ mPermissionDetails.setText(getPreferenceManager().getContext().getString(strId));
+ mPermissionDetails.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Show all individual permissions in this group in a new fragment.
+ */
+ private void showAllPermissions(@NonNull String filterGroup) {
+ Fragment frag = AllAppPermissionsFragment.newInstance(mGroup.getApp().packageName,
+ filterGroup);
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, frag)
+ .addToBackStack("AllPerms")
+ .commit();
+ }
+
+ /**
+ * Get the icon of this app.
+ *
+ * @return the app's icon.
+ */
+ private @NonNull Drawable getAppIcon() {
+ ApplicationInfo appInfo = mGroup.getApp().applicationInfo;
+ return IconDrawableFactory.getBadgedIcon(getActivity(), appInfo,
+ UserHandle.getUserHandleForUid(appInfo.uid));
+ }
+
+ /**
+ * Request to grant/revoke permissions group.
+ *
+ * <p>Does <u>not</u> handle:
+ * <ul>
+ * <li>Individually granted permissions</li>
+ * <li>Permission groups with background permissions</li>
+ * </ul>
+ * <p><u>Does</u> handle:
+ * <ul>
+ * <li>Default grant permissions</li>
+ * </ul>
+ *
+ * @param requestGrant If this group should be granted
+ * @param changeTarget Which permission group (foreground/background/both) should be changed
+ *
+ * @return If the request was processed.
+ */
+ private boolean requestChange(boolean requestGrant, @ChangeTarget int changeTarget) {
+ if (LocationUtils.isLocationGroupAndProvider(getContext(), mGroup.getName(),
+ mGroup.getApp().packageName)) {
+ LocationUtils.showLocationDialog(getContext(),
+ Utils.getAppLabel(mGroup.getApp().applicationInfo, getContext()));
+
+ // The request was denied, so update the buttons.
+ updateButtons();
+ return false;
+ }
+
+ if (requestGrant) {
+ if ((changeTarget & CHANGE_FOREGROUND) != 0) {
+ mGroup.grantRuntimePermissions(false);
+ }
+ if ((changeTarget & CHANGE_BACKGROUND) != 0) {
+ if (mGroup.getBackgroundPermissions() != null) {
+ mGroup.getBackgroundPermissions().grantRuntimePermissions(false);
+ }
+ }
+ } else {
+ boolean requestToRevokeGrantedByDefault = false;
+
+ if ((changeTarget & CHANGE_FOREGROUND) != 0
+ && mGroup.areRuntimePermissionsGranted()) {
+ requestToRevokeGrantedByDefault = mGroup.hasGrantedByDefaultPermission();
+ }
+ if ((changeTarget & CHANGE_BACKGROUND) != 0) {
+ if (mGroup.getBackgroundPermissions() != null
+ && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) {
+ requestToRevokeGrantedByDefault |=
+ mGroup.getBackgroundPermissions().hasGrantedByDefaultPermission();
+ }
+ }
+
+ if ((requestToRevokeGrantedByDefault || !mGroup.doesSupportRuntimePermissions())
+ && !mHasConfirmedRevoke) {
+ showDefaultDenyDialog(changeTarget);
+ updateButtons();
+ return false;
+ } else {
+ if ((changeTarget & CHANGE_FOREGROUND) != 0
+ && mGroup.areRuntimePermissionsGranted()) {
+ mGroup.revokeRuntimePermissions(false);
+ }
+ if ((changeTarget & CHANGE_BACKGROUND) != 0) {
+ if (mGroup.getBackgroundPermissions() != null
+ && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) {
+ mGroup.getBackgroundPermissions().revokeRuntimePermissions(false);
+ }
+ }
+ }
+ }
+
+ updateButtons();
+
+ return true;
+ }
+
+ /**
+ * Show a dialog that warns the user that she/he is about to revoke permissions that were
+ * granted by default.
+ *
+ * <p>The order of operation to revoke a permission granted by default is:
+ * <ol>
+ * <li>{@code showDefaultDenyDialog}</li>
+ * <li>{@link DefaultDenyDialog#onCreateDialog}</li>
+ * <li>{@link AppPermissionFragment#onDenyAnyWay}</li>
+ * </ol>
+ *
+ * @param changeTarget Whether background or foreground should be changed
+ */
+ private void showDefaultDenyDialog(@ChangeTarget int changeTarget) {
+ Bundle args = new Bundle();
+
+ boolean showGrantedByDefaultWarning = false;
+ if ((changeTarget & CHANGE_FOREGROUND) != 0) {
+ showGrantedByDefaultWarning = mGroup.hasGrantedByDefaultPermission();
+ }
+ if ((changeTarget & CHANGE_BACKGROUND) != 0) {
+ if (mGroup.getBackgroundPermissions() != null) {
+ showGrantedByDefaultWarning |=
+ mGroup.getBackgroundPermissions().hasGrantedByDefaultPermission();
+ }
+ }
+
+ args.putInt(DefaultDenyDialog.MSG, showGrantedByDefaultWarning ? R.string.system_warning
+ : R.string.old_sdk_deny_warning);
+ args.putInt(DefaultDenyDialog.CHANGE_TARGET, changeTarget);
+
+ DefaultDenyDialog defaultDenyDialog = new DefaultDenyDialog(this);
+ defaultDenyDialog.setArguments(args);
+ defaultDenyDialog.show(getFragmentManager().beginTransaction(),
+ "denyDefault");
+ }
+
+ /**
+ * Once we user has confirmed that he/she wants to revoke a permission that was granted by
+ * default, actually revoke the permissions.
+ *
+ * @param changeTarget whether to change foreground, background, or both.
+ *
+ * @see #showDefaultDenyDialog(int)
+ */
+ void onDenyAnyWay(@ChangeTarget int changeTarget) {
+ boolean hasDefaultPermissions = false;
+ if ((changeTarget & CHANGE_FOREGROUND) != 0) {
+ mGroup.revokeRuntimePermissions(false);
+ hasDefaultPermissions = mGroup.hasGrantedByDefaultPermission();
+ }
+ if ((changeTarget & CHANGE_BACKGROUND) != 0) {
+ if (mGroup.getBackgroundPermissions() != null) {
+ mGroup.getBackgroundPermissions().revokeRuntimePermissions(false);
+ hasDefaultPermissions |=
+ mGroup.getBackgroundPermissions().hasGrantedByDefaultPermission();
+ }
+ }
+
+ if (hasDefaultPermissions || !mGroup.doesSupportRuntimePermissions()) {
+ mHasConfirmedRevoke = true;
+ }
+ updateButtons();
+ }
+
+ /**
+ * A dialog warning the user that she/he is about to deny a permission that was granted by
+ * default.
+ *
+ * @see #showDefaultDenyDialog(int)
+ */
+ public static class DefaultDenyDialog extends DialogFragment {
+ private static final String MSG = DefaultDenyDialog.class.getName() + ".arg.msg";
+ private static final String CHANGE_TARGET = DefaultDenyDialog.class.getName()
+ + ".arg.changeTarget";
+ private static final String KEY = DefaultDenyDialog.class.getName() + ".arg.key";
+
+ private @NonNull AppPermissionFragment mFragment;
+
+ public DefaultDenyDialog(@NonNull AppPermissionFragment fragment) {
+ mFragment = fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder b = new AlertDialog.Builder(getContext())
+ .setMessage(getArguments().getInt(MSG))
+ .setNegativeButton(R.string.cancel,
+ (DialogInterface dialog, int which) -> mFragment.updateButtons())
+ .setPositiveButton(R.string.grant_dialog_button_deny_anyway,
+ (DialogInterface dialog, int which) ->
+ mFragment.onDenyAnyWay(getArguments().getInt(CHANGE_TARGET)));
+
+ return b.create();
+ }
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionUsageFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionUsageFragment.java
index a9d2b88b..5f1e7c40 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionUsageFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionUsageFragment.java
@@ -156,7 +156,7 @@ public class AppPermissionUsageFragment extends SettingsWithHeader {
if (group.getLabel().equals("Storage")) {
continue;
}
- if (!Utils.shouldShowPermission(getContext(), group)) {
+ if (!Utils.shouldShowPermission(context, group)) {
continue;
}
List<AppPermissionUsage> groupUsages = group.getAppPermissionUsage();
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsagePreference.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsagePreference.java
index e2fe9121..fbfb42d2 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsagePreference.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsagePreference.java
@@ -49,8 +49,9 @@ public class PermissionUsagePreference extends Preference {
setTitle(title);
setSummary(summary);
setOnPreferenceClickListener(preference -> {
- Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
+ Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, usage.getPackageName());
+ intent.putExtra(Intent.EXTRA_PERMISSION_NAME, usage.getPermissionName());
context.startActivity(intent);
return true;
});