Adds settings item for quick affordances.
This is in Display > Lock screen. It reads "Buttons" and the summary
text below it is a comma delimited list of the names of the
currently-selected quick affordances.
Fix: 256662519
Test: Manual verification that the lock screen and wallet
items are gone and the new item is visible and clicking it opens the
Wallpaper & style settings screen
Change-Id: If3746b5d0eb8c61edb9378cdb217ca248b999944
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 564d5c2..c87835c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -118,6 +118,7 @@
<uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" />
<uses-permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
<uses-permission android:name="android.permission.START_VIEW_APP_FEATURES" />
+ <uses-permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES" />
<application
android:name=".SettingsApplication"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5fafdbf..a23df3d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -13743,6 +13743,14 @@
<string name="lockscreen_double_line_clock_summary">Show double-line clock when available</string>
<!-- Lockscreen double-line clock toggle [CHAR LIMIT=60] -->
<string name="lockscreen_double_line_clock_setting_toggle">Double-line clock</string>
+ <!-- Lock screen buttons preference [CHAR LIMIT=60] -->
+ <string name="lockscreen_quick_affordances_title">Buttons</string>
+ <!-- Summary for the lock screen button preference [CHAR LIMIT=60] -->
+ <plurals name="lockscreen_quick_affordances_summary">
+ <item quantity="zero">None</item>
+ <item quantity="one"><xliff:g id="first">%1$s</xliff:g></item>
+ <item quantity="other"><xliff:g id="first">%1$s</xliff:g>, <xliff:g id="second">%2$s</xliff:g></item>
+ </plurals>
<!-- Title for RTT setting. [CHAR LIMIT=NONE] -->
<string name="rtt_settings_title"></string>
diff --git a/res/xml/security_lockscreen_settings.xml b/res/xml/security_lockscreen_settings.xml
index 3bd84f8..80e8fe6 100644
--- a/res/xml/security_lockscreen_settings.xml
+++ b/res/xml/security_lockscreen_settings.xml
@@ -69,6 +69,11 @@
android:summary="@string/lockscreen_trivial_controls_summary"
settings:controller="com.android.settings.display.ControlsTrivialPrivacyPreferenceController"/>
+ <Preference
+ android:key="customizable_lock_screen_quick_affordances"
+ android:title="@string/lockscreen_quick_affordances_title"
+ settings:controller="com.android.settings.display.CustomizableLockScreenQuickAffordancesPreferenceController" />
+
<SwitchPreference
android:key="lockscreen_double_line_clock_switch"
android:title="@string/lockscreen_double_line_clock_setting_toggle"
diff --git a/src/com/android/settings/display/ControlsPrivacyPreferenceController.java b/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
index 6f146d5..5b5b900 100644
--- a/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/ControlsPrivacyPreferenceController.java
@@ -62,6 +62,11 @@
@Override
public int getAvailabilityStatus() {
+ // hide if we should use customizable lock screen quick affordances
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
// hide if lockscreen isn't secure for this user
return isEnabled() && isSecure() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
diff --git a/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java b/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
index 57f717b..7239ac7 100644
--- a/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/ControlsTrivialPrivacyPreferenceController.java
@@ -70,6 +70,10 @@
@Override
public int getAvailabilityStatus() {
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
return showDeviceControlsSettingsEnabled() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
diff --git a/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java b/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java
new file mode 100644
index 0000000..94f1519
--- /dev/null
+++ b/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 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.settings.display;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.PreferenceControllerMixin;
+
+/**
+ * Preference for accessing an experience to customize lock screen quick affordances.
+ */
+public class CustomizableLockScreenQuickAffordancesPreferenceController extends
+ BasePreferenceController implements PreferenceControllerMixin {
+
+ public CustomizableLockScreenQuickAffordancesPreferenceController(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return CustomizableLockScreenUtils.isFeatureEnabled(mContext)
+ ? AVAILABLE
+ : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ final Preference preference = screen.findPreference(getPreferenceKey());
+ if (preference != null) {
+ preference.setOnPreferenceClickListener(preference1 -> {
+ // TODO(b/258471384): open the buttons destination within wallpaper picker.
+ final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
+ final String packageName =
+ mContext.getString(R.string.config_wallpaper_picker_package);
+ if (!TextUtils.isEmpty(packageName)) {
+ intent.setPackage(packageName);
+ }
+ mContext.startActivity(intent);
+ return true;
+ });
+ refreshSummary(preference);
+ }
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return CustomizableLockScreenUtils.getQuickAffordanceSummary(mContext);
+ }
+}
diff --git a/src/com/android/settings/display/CustomizableLockScreenUtils.java b/src/com/android/settings/display/CustomizableLockScreenUtils.java
new file mode 100644
index 0000000..14601a3
--- /dev/null
+++ b/src/com/android/settings/display/CustomizableLockScreenUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 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.settings.display;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Utilities for display settings related to customizable lock screen features. */
+public final class CustomizableLockScreenUtils {
+
+ private static final String TAG = "CustomizableLockScreenUtils";
+ private static final Uri BASE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("com.android.systemui.keyguard.quickaffordance")
+ .build();
+ @VisibleForTesting
+ static final Uri FLAGS_URI = BASE_URI.buildUpon()
+ .path("flags")
+ .build();
+ @VisibleForTesting
+ static final Uri SELECTIONS_URI = BASE_URI.buildUpon()
+ .path("selections")
+ .build();
+ @VisibleForTesting
+ static final String NAME = "name";
+ @VisibleForTesting
+ static final String VALUE = "value";
+ @VisibleForTesting
+ static final String ENABLED_FLAG = "is_feature_enabled";
+ @VisibleForTesting
+ static final String AFFORDANCE_NAME = "affordance_name";
+
+ private CustomizableLockScreenUtils() {}
+
+ /**
+ * Queries and returns whether the customizable lock screen quick affordances feature is enabled
+ * on the device.
+ *
+ * <p>This is a slow, blocking call that shouldn't be made on the main thread.
+ */
+ public static boolean isFeatureEnabled(Context context) {
+ try (Cursor cursor = context.getContentResolver().query(
+ FLAGS_URI,
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ Log.w(TAG, "Cursor was null!");
+ return false;
+ }
+
+ final int indexOfNameColumn = cursor.getColumnIndex(NAME);
+ final int indexOfValueColumn = cursor.getColumnIndex(VALUE);
+ if (indexOfNameColumn == -1 || indexOfValueColumn == -1) {
+ Log.w(TAG, "Cursor doesn't contain " + NAME + " or " + VALUE + "!");
+ return false;
+ }
+
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(indexOfNameColumn);
+ final int value = cursor.getInt(indexOfValueColumn);
+ if (TextUtils.equals(ENABLED_FLAG, name)) {
+ Log.d(TAG, ENABLED_FLAG + "=" + value);
+ return value == 1;
+ }
+ }
+
+ Log.w(TAG, "Flag with name \"" + ENABLED_FLAG + "\" not found!");
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while querying quick affordance content provider", e);
+ return false;
+ }
+ }
+
+ /**
+ * Queries and returns a summary text for the currently-selected lock screen quick affordances.
+ *
+ * <p>This is a slow, blocking call that shouldn't be made on the main thread.
+ */
+ @Nullable
+ public static CharSequence getQuickAffordanceSummary(Context context) {
+ try (Cursor cursor = context.getContentResolver().query(
+ SELECTIONS_URI,
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ Log.w(TAG, "Cursor was null!");
+ return null;
+ }
+
+ final int columnIndex = cursor.getColumnIndex(AFFORDANCE_NAME);
+ if (columnIndex == -1) {
+ Log.w(TAG, "Cursor doesn't contain \"" + AFFORDANCE_NAME + "\" column!");
+ return null;
+ }
+
+ final List<String> affordanceNames = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ final String affordanceName = cursor.getString(columnIndex);
+ if (!TextUtils.isEmpty(affordanceName)) {
+ affordanceNames.add(affordanceName);
+ }
+ }
+
+ // We don't display more than the first two items.
+ final int usableAffordanceNameCount = Math.min(2, affordanceNames.size());
+ final List<String> arguments = new ArrayList<>(usableAffordanceNameCount);
+ if (!affordanceNames.isEmpty()) {
+ arguments.add(affordanceNames.get(0));
+ }
+ if (affordanceNames.size() > 1) {
+ arguments.add(affordanceNames.get(1));
+ }
+
+ return context.getResources().getQuantityString(
+ R.plurals.lockscreen_quick_affordances_summary,
+ usableAffordanceNameCount,
+ arguments.toArray());
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while querying quick affordance content provider", e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/settings/display/QRCodeScannerPreferenceController.java b/src/com/android/settings/display/QRCodeScannerPreferenceController.java
index 16e594a..cb022a7 100644
--- a/src/com/android/settings/display/QRCodeScannerPreferenceController.java
+++ b/src/com/android/settings/display/QRCodeScannerPreferenceController.java
@@ -87,6 +87,10 @@
@Override
public int getAvailabilityStatus() {
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
return isScannerActivityAvailable() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
diff --git a/src/com/android/settings/display/WalletPrivacyPreferenceController.java b/src/com/android/settings/display/WalletPrivacyPreferenceController.java
index 92580f3..fe14a40 100644
--- a/src/com/android/settings/display/WalletPrivacyPreferenceController.java
+++ b/src/com/android/settings/display/WalletPrivacyPreferenceController.java
@@ -62,6 +62,10 @@
@Override
public int getAvailabilityStatus() {
+ if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
return isEnabled() && isSecure() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
diff --git a/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java
new file mode 100644
index 0000000..8597d64
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2022 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.settings.display;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.MatrixCursor;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class CustomizableLockScreenQuickAffordancesPreferenceControllerTest {
+
+ private static final String KEY = "key";
+
+ @Mock private Context mContext;
+ @Mock private ContentResolver mContentResolver;
+
+ private CustomizableLockScreenQuickAffordancesPreferenceController mUnderTest;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
+ when(mContext.getResources())
+ .thenReturn(ApplicationProvider.getApplicationContext().getResources());
+
+ mUnderTest = new CustomizableLockScreenQuickAffordancesPreferenceController(mContext, KEY);
+ }
+
+ @Test
+ public void getAvailabilityStatus_whenEnabled() {
+ setEnabled(true);
+
+ assertThat(mUnderTest.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_whenNotEnabled() {
+ setEnabled(false);
+
+ assertThat(mUnderTest.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+ }
+
+ @Test
+ public void displayPreference_click() {
+ setSelectedAffordanceNames("one", "two");
+ final PreferenceScreen screen = mock(PreferenceScreen.class);
+ final Preference preference = mock(Preference.class);
+ when(screen.findPreference(KEY)).thenReturn(preference);
+
+ mUnderTest.displayPreference(screen);
+
+ final ArgumentCaptor<Preference.OnPreferenceClickListener> clickCaptor =
+ ArgumentCaptor.forClass(Preference.OnPreferenceClickListener.class);
+ verify(preference).setOnPreferenceClickListener(clickCaptor.capture());
+
+ clickCaptor.getValue().onPreferenceClick(preference);
+
+ final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(preference).setOnPreferenceClickListener(clickCaptor.capture());
+ verify(mContext).startActivity(intentCaptor.capture());
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(
+ mContext.getString(R.string.config_wallpaper_picker_package));
+ assertThat(intentCaptor.getValue().getAction()).isEqualTo(Intent.ACTION_SET_WALLPAPER);
+ }
+
+ @Test
+ public void getSummary_whenNoneAreSelected() {
+ setSelectedAffordanceNames();
+
+ assertThat(mUnderTest.getSummary()).isNull();
+ }
+
+ @Test
+ public void getSummary_whenOneIsSelected() {
+ setSelectedAffordanceNames("one");
+
+ assertThat(TextUtils.equals(mUnderTest.getSummary(), "one")).isTrue();
+ }
+
+ @Test
+ public void getSummary_whenTwoAreSelected() {
+ setSelectedAffordanceNames("one", "two");
+
+ assertThat(TextUtils.equals(mUnderTest.getSummary(), "one, two")).isTrue();
+ }
+
+ private void setEnabled(boolean isEnabled) {
+ final MatrixCursor cursor = new MatrixCursor(
+ new String[] {
+ CustomizableLockScreenUtils.NAME,
+ CustomizableLockScreenUtils.VALUE
+ });
+ cursor.addRow(new Object[] { CustomizableLockScreenUtils.ENABLED_FLAG, isEnabled ? 1 : 0 });
+ when(
+ mContentResolver.query(
+ CustomizableLockScreenUtils.FLAGS_URI, null, null, null))
+ .thenReturn(cursor);
+ }
+
+ private void setSelectedAffordanceNames(String... affordanceNames) {
+ final MatrixCursor cursor = new MatrixCursor(
+ new String[] { CustomizableLockScreenUtils.AFFORDANCE_NAME });
+ for (final String name : affordanceNames) {
+ cursor.addRow(new Object[] { name });
+ }
+
+ when(
+ mContentResolver.query(
+ CustomizableLockScreenUtils.SELECTIONS_URI, null, null, null))
+ .thenReturn(cursor);
+ }
+}