diff options
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; }); |