diff options
10 files changed, 1121 insertions, 10 deletions
diff --git a/res/drawable/car_ic_info.xml b/res/drawable/car_ic_info.xml new file mode 100644 index 00000000..bc334fb0 --- /dev/null +++ b/res/drawable/car_ic_info.xml @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@*android:dimen/car_preference_icon_size" + android:height="@*android:dimen/car_preference_icon_size" + android:tint="@*android:color/car_tint" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <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> diff --git a/res/drawable/car_ic_settings.xml b/res/drawable/car_ic_settings.xml new file mode 100644 index 00000000..f278af91 --- /dev/null +++ b/res/drawable/car_ic_settings.xml @@ -0,0 +1,34 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@*android:dimen/car_preference_icon_size" + android:height="@*android:dimen/car_preference_icon_size" + android:tint="@*android:color/car_tint" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <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> diff --git a/res/layout/car_two_target_preference.xml b/res/layout/car_two_target_preference.xml new file mode 100644 index 00000000..66cf936a --- /dev/null +++ b/res/layout/car_two_target_preference.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@android:color/transparent" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall"> + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="?android:attr/selectableItemBackground" + android:clipToPadding="false" + android:gravity="start|center_vertical" + android:paddingBottom="@*android:dimen/car_preference_row_vertical_margin" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingTop="@*android:dimen/car_preference_row_vertical_margin"> + <androidx.preference.internal.PreferenceImageView + android:id="@android:id/icon" + android:layout_width="@*android:dimen/car_preference_icon_size" + android:layout_height="@*android:dimen/car_preference_icon_size" + android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:orientation="vertical"> + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:hyphenationFrequency="none" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceListItem"/> + <TextView + android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:hyphenationFrequency="none" + android:textAppearance="?android:attr/textAppearanceListItemSecondary"/> + </LinearLayout> + </LinearLayout> + <LinearLayout + android:id="@+id/action_widget_container" + android:layout_width="wrap_content" + android:layout_height="match_parent"> + <View + android:id="@+id/two_target_divider" + android:layout_width="1dp" + android:layout_height="match_parent" + android:layout_marginBottom="@*android:dimen/car_preference_row_vertical_margin" + android:layout_marginTop="@*android:dimen/car_preference_row_vertical_margin" + android:background="?attr/carDividerColor"/> + <!-- Preference should place its actual preference widget here. --> + <FrameLayout + android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:background="?android:attr/selectableItemBackground" + android:gravity="center" + android:minWidth="?android:attr/listPreferredItemHeightSmall" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"/> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/info_preference_widget.xml b/res/layout/info_preference_widget.xml new file mode 100644 index 00000000..3efa9835 --- /dev/null +++ b/res/layout/info_preference_widget.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> + +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/car_ic_info"/> diff --git a/res/layout/settings_preference_widget.xml b/res/layout/settings_preference_widget.xml new file mode 100644 index 00000000..70f8cd65 --- /dev/null +++ b/res/layout/settings_preference_widget.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> + +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/car_ic_settings"/> diff --git a/res/values/strings.xml b/res/values/strings.xml index c03934da..eaeaa4ea 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -604,6 +604,9 @@ <!-- Label when no apps have been denied a given permission [CHAR LIMIT=none] --> <string name="no_apps_denied">No apps denied</string> + <!-- Label for the selected permission state for a given permission and application [CHAR LIMIT=30] --> + <string name="car_permission_selected">Selected</string> + <!-- Label for button that opens up the Settings [CHAR LIMIT=20] --> <string name="settings">Settings</string> diff --git a/src/com/android/packageinstaller/permission/ui/AppPermissionActivity.java b/src/com/android/packageinstaller/permission/ui/AppPermissionActivity.java index 87702d3f..b425fd8e 100644 --- a/src/com/android/packageinstaller/permission/ui/AppPermissionActivity.java +++ b/src/com/android/packageinstaller/permission/ui/AppPermissionActivity.java @@ -28,9 +28,11 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.permission.ui.auto.AutoAppPermissionFragment; import com.android.packageinstaller.permission.ui.handheld.AppPermissionFragment; import com.android.packageinstaller.permission.utils.LocationUtils; import com.android.packageinstaller.permission.utils.Utils; +import com.android.permissioncontroller.R; /** * Manage a single permission of a single app @@ -43,6 +45,11 @@ public final class AppPermissionActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { + if (DeviceUtils.isAuto(this)) { + // Automotive relies on a different theme. Apply before calling super so that + // fragments are restored properly on configuration changes. + setTheme(R.style.CarSettings); + } super.onCreate(savedInstanceState); getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); @@ -88,11 +95,17 @@ public final class AppPermissionActivity extends FragmentActivity { String caller = getIntent().getStringExtra(EXTRA_CALLER_NAME); - Fragment androidXFragment = AppPermissionFragment.newInstance(packageName, permissionName, - groupName, userHandle, caller); + Fragment androidXFragment; + if (DeviceUtils.isAuto(this)) { + androidXFragment = AutoAppPermissionFragment.newInstance(packageName, permissionName, + groupName, userHandle); + } else { + androidXFragment = AppPermissionFragment.newInstance(packageName, permissionName, + groupName, userHandle, caller); + } getSupportFragmentManager().beginTransaction().replace(android.R.id.content, - androidXFragment).commit(); + androidXFragment).commit(); } @Override diff --git a/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionFragment.java b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionFragment.java new file mode 100644 index 00000000..ecbde6c5 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionFragment.java @@ -0,0 +1,806 @@ +/* + * 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. + */ + +package com.android.packageinstaller.permission.ui.auto; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.res.TypedArrayUtils; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.packageinstaller.auto.AutoSettingsFrameFragment; +import com.android.packageinstaller.permission.model.AppPermissionGroup; +import com.android.packageinstaller.permission.model.Permission; +import com.android.packageinstaller.permission.utils.LocationUtils; +import com.android.packageinstaller.permission.utils.PackageRemovalMonitor; +import com.android.packageinstaller.permission.utils.SafetyNetLogger; +import com.android.packageinstaller.permission.utils.Utils; +import com.android.permissioncontroller.R; +import com.android.settingslib.RestrictedLockUtils; + +import java.lang.annotation.Retention; +import java.util.List; + +/** Settings related to a particular permission for the given app. */ +public class AutoAppPermissionFragment extends AutoSettingsFrameFragment { + + 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; + + @NonNull + private AppPermissionGroup mGroup; + + @NonNull + private TwoStatePreference mAlwaysPermissionPreference; + @NonNull + private TwoStatePreference mForegroundOnlyPermissionPreference; + @NonNull + private TwoStatePreference mDenyPermissionPreference; + @NonNull + private AutoTwoTargetPreference mDetailsPreference; + + private boolean mHasConfirmedRevoke; + + /** + * Listens for changes to the permission of the app the permission is currently getting + * granted to. {@code null} when unregistered. + */ + @Nullable + private PackageManager.OnPermissionsChangedListener mPermissionChangeListener; + + /** + * Listens for changes to the app the permission is currently getting granted to. {@code null} + * when unregistered. + */ + @Nullable + private PackageRemovalMonitor mPackageRemovalMonitor; + + /** + * Returns a new {@link AutoAppPermissionFragment}. + * + * @param packageName the package name for which the permission is being changed + * @param permName the name of the permission being changed + * @param groupName the name of the permission group being changed + * @param userHandle the user for which the permission is being changed + */ + @NonNull + public static AutoAppPermissionFragment newInstance(@NonNull String packageName, + @NonNull String permName, @Nullable String groupName, @NonNull UserHandle userHandle) { + AutoAppPermissionFragment fragment = new AutoAppPermissionFragment(); + Bundle arguments = new Bundle(); + arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); + if (groupName == null) { + arguments.putString(Intent.EXTRA_PERMISSION_NAME, permName); + } else { + arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName); + } + arguments.putParcelable(Intent.EXTRA_USER, userHandle); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mHasConfirmedRevoke = false; + + mGroup = getAppPermissionGroup(); + if (mGroup == null) { + requireActivity().setResult(Activity.RESULT_CANCELED); + requireActivity().finish(); + return; + } + + setHeaderLabel( + getContext().getString(R.string.app_permission_title, mGroup.getFullLabel())); + } + + private AppPermissionGroup getAppPermissionGroup() { + Activity activity = getActivity(); + Context context = getPreferenceManager().getContext(); + + String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); + String groupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); + if (groupName == null) { + groupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); + } + PackageItemInfo groupInfo = Utils.getGroupInfo(groupName, context); + List<PermissionInfo> groupPermInfos = Utils.getGroupPermissionInfos(groupName, context); + if (groupInfo == null || groupPermInfos == null) { + Log.i(LOG_TAG, "Illegal group: " + groupName); + return null; + } + UserHandle userHandle = getArguments().getParcelable(Intent.EXTRA_USER); + PackageInfo packageInfo = AutoPermissionsUtils.getPackageInfo(activity, packageName, + userHandle); + if (packageInfo == null) { + Log.i(LOG_TAG, "PackageInfo is null"); + return null; + } + AppPermissionGroup group = AppPermissionGroup.create(context, packageInfo, groupInfo, + groupPermInfos, false); + + if (group == null || !Utils.shouldShowPermission(context, group)) { + Log.i(LOG_TAG, "Illegal group: " + (group == null ? "null" : group.getName())); + return null; + } + + return group; + } + + @Override + public void onCreatePreferences(Bundle bundle, String s) { + setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + PreferenceScreen screen = getPreferenceScreen(); + screen.addPreference( + AutoPermissionsUtils.createHeaderPreference(getContext(), + mGroup.getApp().applicationInfo)); + + // Add permissions selector preferences. + PreferenceGroup permissionSelector = new PreferenceCategory(getContext()); + permissionSelector.setTitle( + getContext().getString(R.string.app_permission_header, mGroup.getFullLabel())); + screen.addPreference(permissionSelector); + + mAlwaysPermissionPreference = new SelectedPermissionPreference(getContext()); + mAlwaysPermissionPreference.setTitle(R.string.app_permission_button_allow_always); + permissionSelector.addPreference(mAlwaysPermissionPreference); + + mForegroundOnlyPermissionPreference = new SelectedPermissionPreference(getContext()); + mForegroundOnlyPermissionPreference.setTitle( + R.string.app_permission_button_allow_foreground); + permissionSelector.addPreference(mForegroundOnlyPermissionPreference); + + mDenyPermissionPreference = new SelectedPermissionPreference(getContext()); + mDenyPermissionPreference.setTitle(R.string.app_permission_button_deny); + permissionSelector.addPreference(mDenyPermissionPreference); + + mDetailsPreference = new AutoTwoTargetPreference(getContext()); + screen.addPreference(mDetailsPreference); + } + + @Override + public void onStart() { + super.onStart(); + Activity activity = requireActivity(); + + mPermissionChangeListener = new PermissionChangeListener( + mGroup.getApp().applicationInfo.uid); + PackageManager pm = activity.getPackageManager(); + pm.addOnPermissionsChangeListener(mPermissionChangeListener); + + // Get notified when the package is removed. + String packageName = mGroup.getApp().packageName; + mPackageRemovalMonitor = new PackageRemovalMonitor(getContext(), packageName) { + @Override + public void onPackageRemoved() { + Log.w(LOG_TAG, packageName + " was uninstalled"); + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + } + }; + mPackageRemovalMonitor.register(); + + // Check if the package was removed while this activity was not started. + try { + activity.createPackageContextAsUser(packageName, /* flags= */ 0, + mGroup.getUser()).getPackageManager().getPackageInfo(packageName, + /* flags= */ 0); + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, packageName + " was uninstalled while this activity was stopped", e); + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + } + + // Re-create the permission group in case permissions have changed and update the UI. + mGroup = getAppPermissionGroup(); + updateUi(); + } + + @Override + public void onStop() { + super.onStop(); + + if (mPackageRemovalMonitor != null) { + mPackageRemovalMonitor.unregister(); + mPackageRemovalMonitor = null; + } + + if (mPermissionChangeListener != null) { + getActivity().getPackageManager().removeOnPermissionsChangeListener( + mPermissionChangeListener); + mPermissionChangeListener = null; + } + } + + private void updateUi() { + mDetailsPreference.setOnSecondTargetClickListener(null); + mDetailsPreference.setVisible(false); + + if (mGroup.areRuntimePermissionsGranted()) { + if (!mGroup.hasPermissionWithBackgroundMode() + || (mGroup.getBackgroundPermissions() != null + && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted())) { + setSelectedPermissionState(mAlwaysPermissionPreference); + } else { + setSelectedPermissionState(mForegroundOnlyPermissionPreference); + } + } else { + setSelectedPermissionState(mDenyPermissionPreference); + } + + mAlwaysPermissionPreference.setOnPreferenceClickListener( + v -> requestChange(/* requestGrant= */true, CHANGE_BOTH)); + mForegroundOnlyPermissionPreference.setOnPreferenceClickListener(v -> { + requestChange(/* requestGrant= */false, CHANGE_BACKGROUND); + requestChange(/* requestGrant= */true, CHANGE_FOREGROUND); + return true; + }); + mDenyPermissionPreference.setOnPreferenceClickListener( + v -> requestChange(/* requestGrant= */ false, CHANGE_BOTH)); + + // Set the allow and foreground-only button states appropriately. + if (mGroup.hasPermissionWithBackgroundMode()) { + if (mGroup.getBackgroundPermissions() == null) { + mAlwaysPermissionPreference.setVisible(false); + } else { + mForegroundOnlyPermissionPreference.setVisible(true); + mAlwaysPermissionPreference.setTitle(R.string.app_permission_button_allow_always); + } + } else { + mForegroundOnlyPermissionPreference.setVisible(false); + mAlwaysPermissionPreference.setTitle(R.string.app_permission_button_allow); + } + + // Handle the UI for various special cases. + if (isSystemFixed() || isPolicyFullyFixed() || isForegroundDisabledByPolicy()) { + // Disable changing permissions and potentially show administrator message. + mAlwaysPermissionPreference.setEnabled(false); + mForegroundOnlyPermissionPreference.setEnabled(false); + mDenyPermissionPreference.setEnabled(false); + + RestrictedLockUtils.EnforcedAdmin admin = getAdmin(); + if (admin != null) { + mDetailsPreference.setWidgetLayoutResource(R.layout.info_preference_widget); + mDetailsPreference.setOnSecondTargetClickListener( + preference -> RestrictedLockUtils.sendShowAdminSupportDetailsIntent( + getContext(), admin)); + } + + updateDetailForFixedByPolicyPermissionGroup(); + } else if (Utils.areGroupPermissionsIndividuallyControlled(getContext(), + mGroup.getName())) { + // If the permissions are individually controlled, also show a link to the page that + // lets you control them. + mDetailsPreference.setWidgetLayoutResource(R.layout.settings_preference_widget); + mDetailsPreference.setOnSecondTargetClickListener( + preference -> showAllPermissions(mGroup.getName())); + + updateDetailForIndividuallyControlledPermissionGroup(); + } 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". + mAlwaysPermissionPreference.setEnabled(false); + + mDenyPermissionPreference.setOnPreferenceClickListener(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. + mAlwaysPermissionPreference.setEnabled(false); + setSelectedPermissionState(mForegroundOnlyPermissionPreference); + + mDenyPermissionPreference.setOnPreferenceClickListener( + 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. + mDenyPermissionPreference.setEnabled(false); + + mAlwaysPermissionPreference.setOnPreferenceClickListener( + v -> requestChange(true, CHANGE_BACKGROUND)); + mForegroundOnlyPermissionPreference.setOnPreferenceClickListener( + v -> requestChange(false, CHANGE_BACKGROUND)); + + updateDetailForFixedByPolicyPermissionGroup(); + } else { + // The default tri-state case is handled by default. + } + } + + } else { + // The default bi-state case is handled by default. + } + } + } + + /** + * Set the given permission state as the only checked permission state. + */ + private void setSelectedPermissionState(@NonNull TwoStatePreference permissionState) { + permissionState.setChecked(true); + if (permissionState != mAlwaysPermissionPreference) { + mAlwaysPermissionPreference.setChecked(false); + } + if (permissionState != mForegroundOnlyPermissionPreference) { + mForegroundOnlyPermissionPreference.setChecked(false); + } + if (permissionState != mDenyPermissionPreference) { + mDenyPermissionPreference.setChecked(false); + } + } + + /** + * Are any permissions of this group fixed by the system, i.e. not changeable by the user. + * + * @return {@code true} iff any permission is fixed + */ + private boolean isSystemFixed() { + return mGroup.isSystemFixed(); + } + + /** + * 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. + */ + @Nullable + private RestrictedLockUtils.EnforcedAdmin getAdmin() { + return RestrictedLockUtils.getProfileOrDeviceOwner(getContext(), mGroup.getUser()); + } + + /** + * Update the detail in the case the permission group has individually controlled permissions. + */ + private void updateDetailForIndividuallyControlledPermissionGroup() { + int revokedCount = 0; + List<Permission> permissions = mGroup.getPermissions(); + int permissionCount = permissions.size(); + for (int i = 0; i < permissionCount; i++) { + Permission permission = permissions.get(i); + if (!permission.isGrantedIncludingAppOp()) { + revokedCount++; + } + } + + int resId; + if (revokedCount == 0) { + resId = R.string.permission_revoked_none; + } else if (revokedCount == permissionCount) { + resId = R.string.permission_revoked_all; + } else { + resId = R.string.permission_revoked_count; + } + + mDetailsPreference.setSummary(getContext().getString(resId, revokedCount)); + mDetailsPreference.setVisible(true); + } + + /** + * Update the detail of a permission group that is at least partially fixed by policy. + */ + private void updateDetailForFixedByPolicyPermissionGroup() { + RestrictedLockUtils.EnforcedAdmin admin = getAdmin(); + AppPermissionGroup backgroundGroup = mGroup.getBackgroundPermissions(); + + boolean hasAdmin = admin != null; + + if (isSystemFixed()) { + // Permission is fully controlled by the system and cannot be switched + + setDetail(R.string.permission_summary_enabled_system_fixed); + } else 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 permission picker preferences. + * + * @param strId the resourceId of the string to display. + */ + private void setDetail(int strId) { + mDetailsPreference.setSummary(strId); + mDetailsPreference.setVisible(true); + } + + /** + * Show all individual permissions in this group in a new fragment. + */ + private void showAllPermissions(@NonNull String filterGroup) { + Fragment frag = AutoAllAppPermissionsFragment.newInstance(mGroup.getApp().packageName, + filterGroup, UserHandle.getUserHandleForUid(mGroup.getApp().applicationInfo.uid)); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, frag) + .addToBackStack("AllPerms") + .commit(); + } + + /** + * 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. + updateUi(); + return false; + } + + if (requestGrant) { + if ((changeTarget & CHANGE_FOREGROUND) != 0) { + if (!mGroup.areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup); + } + + mGroup.grantRuntimePermissions(false); + } + if ((changeTarget & CHANGE_BACKGROUND) != 0) { + if (mGroup.getBackgroundPermissions() != null) { + if (!mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup.getBackgroundPermissions()); + } + + mGroup.getBackgroundPermissions().grantRuntimePermissions(false); + } + } + } else { + boolean showDefaultDenyDialog = false; + + if ((changeTarget & CHANGE_FOREGROUND) != 0 + && mGroup.areRuntimePermissionsGranted()) { + showDefaultDenyDialog = mGroup.hasGrantedByDefaultPermission() + || !mGroup.doesSupportRuntimePermissions() + || mGroup.hasInstallToRuntimeSplit(); + } + if ((changeTarget & CHANGE_BACKGROUND) != 0) { + if (mGroup.getBackgroundPermissions() != null + && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) { + AppPermissionGroup bgPerm = mGroup.getBackgroundPermissions(); + showDefaultDenyDialog |= bgPerm.hasGrantedByDefaultPermission() + || !bgPerm.doesSupportRuntimePermissions() + || bgPerm.hasInstallToRuntimeSplit(); + } + } + + if (showDefaultDenyDialog && !mHasConfirmedRevoke) { + showDefaultDenyDialog(changeTarget); + updateUi(); + return false; + } else { + if ((changeTarget & CHANGE_FOREGROUND) != 0 + && mGroup.areRuntimePermissionsGranted()) { + if (mGroup.areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup); + } + + mGroup.revokeRuntimePermissions(false); + } + if ((changeTarget & CHANGE_BACKGROUND) != 0) { + if (mGroup.getBackgroundPermissions() != null + && mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) { + if (mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup.getBackgroundPermissions()); + } + + mGroup.getBackgroundPermissions().revokeRuntimePermissions(false); + } + } + } + } + + updateUi(); + + 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 AutoAppPermissionFragment#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(); + defaultDenyDialog.setArguments(args); + defaultDenyDialog.setTargetFragment(this, 0); + defaultDenyDialog.show(getFragmentManager().beginTransaction(), + DefaultDenyDialog.class.getName()); + } + + /** + * 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) { + if (mGroup.areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup); + } + + mGroup.revokeRuntimePermissions(false); + hasDefaultPermissions = mGroup.hasGrantedByDefaultPermission(); + } + if ((changeTarget & CHANGE_BACKGROUND) != 0) { + if (mGroup.getBackgroundPermissions() != null) { + if (mGroup.getBackgroundPermissions().areRuntimePermissionsGranted()) { + SafetyNetLogger.logPermissionToggled(mGroup.getBackgroundPermissions()); + } + + mGroup.getBackgroundPermissions().revokeRuntimePermissions(false); + hasDefaultPermissions |= + mGroup.getBackgroundPermissions().hasGrantedByDefaultPermission(); + } + } + + if (hasDefaultPermissions || !mGroup.doesSupportRuntimePermissions()) { + mHasConfirmedRevoke = true; + } + updateUi(); + } + + /** Preference used to represent apps that can be picked as a default app. */ + private static class SelectedPermissionPreference extends TwoStatePreference { + + SelectedPermissionPreference(Context context) { + super(context, null, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + setPersistent(false); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + setSummary(checked ? getContext().getString(R.string.car_permission_selected) : null); + } + } + + /** + * A dialog warning the user that they are 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"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AutoAppPermissionFragment fragment = (AutoAppPermissionFragment) getTargetFragment(); + AlertDialog.Builder b = new AlertDialog.Builder(getContext()) + .setMessage(getArguments().getInt(MSG)) + .setNegativeButton(R.string.cancel, + (dialog, which) -> fragment.updateUi()) + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, + (dialog, which) -> + fragment.onDenyAnyWay(getArguments().getInt(CHANGE_TARGET))); + + return b.create(); + } + } + + /** + * A listener for permission changes. + */ + private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener { + private final int mUid; + + PermissionChangeListener(int uid) { + mUid = uid; + } + + @Override + public void onPermissionsChanged(int uid) { + if (uid == mUid) { + Log.w(LOG_TAG, "Permissions changed."); + mGroup = getAppPermissionGroup(); + updateUi(); + } + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java index a9e82788..aac7faf6 100644 --- a/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java @@ -24,7 +24,6 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.text.TextUtils; -import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; @@ -107,15 +106,10 @@ public class AutoAppPermissionsFragment extends AutoSettingsFrameFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - bindUi(mAppPermissions.getPackageInfo()); - } - - @Override public void onStart() { super.onStart(); mAppPermissions.refresh(); + bindUi(mAppPermissions.getPackageInfo()); updatePreferences(); } diff --git a/src/com/android/packageinstaller/permission/ui/auto/AutoTwoTargetPreference.java b/src/com/android/packageinstaller/permission/ui/auto/AutoTwoTargetPreference.java new file mode 100644 index 00000000..dd2686f9 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/auto/AutoTwoTargetPreference.java @@ -0,0 +1,106 @@ +/* + * 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. + */ + +package com.android.packageinstaller.permission.ui.auto; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.permissioncontroller.R; + +/** {@link Preference} with the widget layout as a separate target. */ +public class AutoTwoTargetPreference extends Preference { + + private OnSecondTargetClickListener mListener; + private boolean mIsDividerVisible = true; + + public AutoTwoTargetPreference(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public AutoTwoTargetPreference(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public AutoTwoTargetPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AutoTwoTargetPreference(@NonNull Context context) { + super(context); + init(); + } + + private void init() { + setLayoutResource(R.layout.car_two_target_preference); + } + + /** Set the listener for second target click. */ + public void setOnSecondTargetClickListener(@Nullable OnSecondTargetClickListener listener) { + mListener = listener; + notifyChanged(); + } + + /** Sets the visibility of the divider. */ + public void setDividerVisible(boolean visible) { + mIsDividerVisible = visible; + notifyChanged(); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + View actionContainer = holder.findViewById(R.id.action_widget_container); + View divider = holder.findViewById(R.id.two_target_divider); + FrameLayout widgetFrame = (FrameLayout) holder.findViewById(android.R.id.widget_frame); + if (mListener != null) { + actionContainer.setVisibility(View.VISIBLE); + divider.setVisibility(mIsDividerVisible ? View.VISIBLE : View.GONE); + widgetFrame.setVisibility(View.VISIBLE); + widgetFrame.setOnClickListener(v -> mListener.onSecondTargetClick(this)); + } else { + actionContainer.setVisibility(View.GONE); + } + } + + /** + * Listener for second target click. + */ + public interface OnSecondTargetClickListener { + + /** + * Callback when the second target is clicked. + * + * @param preference the {@link AutoTwoTargetPreference} that was clicked + */ + void onSecondTargetClick(@NonNull AutoTwoTargetPreference preference); + } +} |