From 01df2b4ee29db1e13ef8371b6961e561b84f6da8 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 9 Nov 2022 11:47:50 -0800 Subject: [PATCH] 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 --- AndroidManifest.xml | 1 + res/values/strings.xml | 8 + res/xml/security_lockscreen_settings.xml | 5 + .../ControlsPrivacyPreferenceController.java | 5 + ...olsTrivialPrivacyPreferenceController.java | 4 + ...nQuickAffordancesPreferenceController.java | 71 ++++++++ .../display/CustomizableLockScreenUtils.java | 152 +++++++++++++++++ .../QRCodeScannerPreferenceController.java | 4 + .../WalletPrivacyPreferenceController.java | 4 + ...ckAffordancesPreferenceControllerTest.java | 153 ++++++++++++++++++ 10 files changed, 407 insertions(+) create mode 100644 src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceController.java create mode 100644 src/com/android/settings/display/CustomizableLockScreenUtils.java create mode 100644 tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 564d5c23c3d..c87835c5f72 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -118,6 +118,7 @@ + Show double-line clock when available Double-line clock + + Buttons + + + None + %1$s + %1$s, %2$s + diff --git a/res/xml/security_lockscreen_settings.xml b/res/xml/security_lockscreen_settings.xml index 3bd84f81ae3..80e8fe69d2a 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"/> + + { + // 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 00000000000..14601a3655b --- /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. + * + *

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. + * + *

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 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 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 16e594a62d5..cb022a74fa8 100644 --- a/src/com/android/settings/display/QRCodeScannerPreferenceController.java +++ b/src/com/android/settings/display/QRCodeScannerPreferenceController.java @@ -87,6 +87,10 @@ public class QRCodeScannerPreferenceController extends TogglePreferenceControlle @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 92580f3d569..fe14a40a01a 100644 --- a/src/com/android/settings/display/WalletPrivacyPreferenceController.java +++ b/src/com/android/settings/display/WalletPrivacyPreferenceController.java @@ -62,6 +62,10 @@ public class WalletPrivacyPreferenceController extends TogglePreferenceControlle @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 00000000000..8597d6490af --- /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 clickCaptor = + ArgumentCaptor.forClass(Preference.OnPreferenceClickListener.class); + verify(preference).setOnPreferenceClickListener(clickCaptor.capture()); + + clickCaptor.getValue().onPreferenceClick(preference); + + final ArgumentCaptor 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); + } +}