summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/drawable/car_ic_info.xml25
-rw-r--r--res/drawable/car_ic_settings.xml34
-rw-r--r--res/layout/car_two_target_preference.xml84
-rw-r--r--res/layout/info_preference_widget.xml23
-rw-r--r--res/layout/settings_preference_widget.xml23
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/packageinstaller/permission/ui/AppPermissionActivity.java19
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionFragment.java806
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java8
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/AutoTwoTargetPreference.java106
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);
+ }
+}