From 51c94041820cce810b38248888a8725232b3f988 Mon Sep 17 00:00:00 2001 From: Stanley Wang Date: Wed, 17 Oct 2018 12:07:55 +0800 Subject: [PATCH] Architecture review of Copyable Slice Design doc: https://drive.google.com/open?id=1NJPd_282H4195HUGJH5cGJO_Jrcz1Vl6AAw_VQOtGq0 Fixes: 118398321 Test: manual Test: make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.slice Change-Id: Ic6762e58698a994d16a5de1778b4035ae430a256 --- res/drawable/ic_content_copy_grey600_24dp.xml | 9 ++++ .../settings/slices/CopyableSlice.java | 28 ++++++++++++ .../slices/SettingsSliceProvider.java | 8 +++- .../slices/SliceBroadcastReceiver.java | 27 ++++++++++++ .../settings/slices/SliceBuilderUtils.java | 34 ++++++++++++++ .../slices/SliceBuilderUtilsTest.java | 20 ++++++++- .../testutils/FakeCopyableController.java | 44 +++++++++++++++++++ .../settings/testutils/SliceTester.java | 39 +++++++++++++++- 8 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 res/drawable/ic_content_copy_grey600_24dp.xml create mode 100644 src/com/android/settings/slices/CopyableSlice.java create mode 100644 tests/robotests/src/com/android/settings/testutils/FakeCopyableController.java diff --git a/res/drawable/ic_content_copy_grey600_24dp.xml b/res/drawable/ic_content_copy_grey600_24dp.xml new file mode 100644 index 00000000000..827c66ebab0 --- /dev/null +++ b/res/drawable/ic_content_copy_grey600_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/com/android/settings/slices/CopyableSlice.java b/src/com/android/settings/slices/CopyableSlice.java new file mode 100644 index 00000000000..31fc151d6ba --- /dev/null +++ b/src/com/android/settings/slices/CopyableSlice.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.slices; + +/** + * Provide the copy ability for preference controller to copy the data to the clipboard. + */ +public interface CopyableSlice { + /** + * Copy the key slice information to the clipboard. + * It is highly recommended to show the toast to notify users when implemented this function. + */ + void copy(); +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 08ed0d8bf15..cc698661159 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -106,6 +106,12 @@ public class SettingsSliceProvider extends SliceProvider { public static final String ACTION_SLIDER_CHANGED = "com.android.settings.slice.action.SLIDER_CHANGED"; + /** + * Action passed for copy data for the Copyable Slices. + */ + public static final String ACTION_COPY = + "com.android.settings.slice.action.COPY"; + /** * Intent Extra passed for the key identifying the Setting Slice. */ @@ -509,4 +515,4 @@ public class SettingsSliceProvider extends SliceProvider { } return new String[0]; } -} \ No newline at end of file +} diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index e9e9d2cdb4b..4b86f336332 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -24,6 +24,7 @@ import static com.android.settings.network.telephony.Enhanced4gLteSliceHelper import static com.android.settings.notification.ZenModeSliceBuilder.ACTION_ZEN_MODE_SLICE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_SLIDER_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED; +import static com.android.settings.slices.SettingsSliceProvider.ACTION_COPY; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED; import static com.android.settings.wifi.calling.WifiCallingSliceHelper.ACTION_WIFI_CALLING_CHANGED; @@ -115,6 +116,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { case ACTION_FLASHLIGHT_SLICE_CHANGED: FlashlightSliceBuilder.handleUriChange(context, intent); break; + case ACTION_COPY: + handleCopyAction(context, key, isPlatformSlice); + break; } } @@ -184,6 +188,29 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { updateUri(context, key, isPlatformSlice); } + private void handleCopyAction(Context context, String key, boolean isPlatformSlice) { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("No key passed to Intent for controller"); + } + + final BasePreferenceController controller = getPreferenceController(context, key); + + if (!(controller instanceof CopyableSlice)) { + throw new IllegalArgumentException( + "Copyable action passed for a non-copyable key:" + key); + } + + if (!controller.isAvailable()) { + Log.w(TAG, "Can't update " + key + " since the setting is unavailable"); + if (!controller.hasAsyncUpdate()) { + updateUri(context, key, isPlatformSlice); + } + return; + } + + ((CopyableSlice) controller).copy(); + } + /** * Log Slice value update events into MetricsFeatureProvider. The logging schema generally * follows the pattern in SharedPreferenceLogger. diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java index a947fb2d25e..c70b92e0f07 100644 --- a/src/com/android/settings/slices/SliceBuilderUtils.java +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -93,6 +93,10 @@ public class SliceBuilderUtils { return buildUnavailableSlice(context, sliceData); } + if (controller instanceof CopyableSlice) { + return buildCopyableSlice(context, sliceData, controller); + } + switch (sliceData.getSliceType()) { case SliceData.SliceType.INTENT: return buildIntentSlice(context, sliceData, controller); @@ -324,6 +328,28 @@ public class SliceBuilderUtils { .build(); } + private static Slice buildCopyableSlice(Context context, SliceData sliceData, + BasePreferenceController controller) { + final SliceAction copyableAction = getCopyableAction(context, sliceData); + final PendingIntent contentIntent = getContentPendingIntent(context, sliceData); + final IconCompat icon = getSafeIcon(context, sliceData); + final SliceAction primaryAction = new SliceAction(contentIntent, icon, + sliceData.getTitle()); + final CharSequence subtitleText = getSubtitleText(context, controller, sliceData); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); + final Set keywords = buildSliceKeywords(sliceData); + + return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new RowBuilder() + .setTitle(sliceData.getTitle()) + .setSubtitle(subtitleText) + .setPrimaryAction(primaryAction) + .addEndItem(copyableAction)) + .setKeywords(keywords) + .build(); + } + private static BasePreferenceController getPreferenceController(Context context, String controllerClassName, String controllerKey) { try { @@ -346,6 +372,14 @@ public class SliceBuilderUtils { return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData); } + private static SliceAction getCopyableAction(Context context, SliceData sliceData) { + final PendingIntent intent = getActionIntent(context, + SettingsSliceProvider.ACTION_COPY, sliceData); + final IconCompat icon = IconCompat.createWithResource(context, + R.drawable.ic_content_copy_grey600_24dp); + return new SliceAction(intent, icon, sliceData.getTitle()); + } + private static boolean isValidSummary(Context context, CharSequence summary) { if (summary == null || TextUtils.isEmpty(summary.toString().trim())) { return false; diff --git a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java index f9cc427e6c8..5770e25d544 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java @@ -41,6 +41,7 @@ import androidx.slice.widget.SliceLiveData; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.FakeCopyableController; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeSliderController; import com.android.settings.testutils.FakeToggleController; @@ -67,6 +68,7 @@ public class SliceBuilderUtilsTest { private final Uri URI = Uri.parse("content://com.android.settings.slices/test"); private final Class TOGGLE_CONTROLLER = FakeToggleController.class; private final Class SLIDER_CONTROLLER = FakeSliderController.class; + private final Class COPYABLE_CONTROLLER = FakeCopyableController.class; private final Class CONTEXT_CONTROLLER = FakeContextOnlyPreferenceController.class; private final boolean IS_DYNAMIC_SUMMARY_ALLOWED = false; @@ -116,7 +118,6 @@ public class SliceBuilderUtilsTest { public void buildSliderSlice_returnsMatchingSlice() { final SliceData data = getDummyData(SLIDER_CONTROLLER, SliceData.SliceType.SLIDER); - final Slice slice = SliceBuilderUtils.buildSlice(mContext, data); verify(mFeatureFactory.metricsFeatureProvider) .action(eq(mContext), eq(MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED), @@ -130,6 +131,23 @@ public class SliceBuilderUtilsTest { SliceTester.testSettingsSliderSlice(mContext, slice, data); } + @Test + public void buildCopyableSlice_returnsMatchingSlice() { + final SliceData dummyData = getDummyData(COPYABLE_CONTROLLER, -1); + + final Slice slice = SliceBuilderUtils.buildSlice(mContext, dummyData); + verify(mFeatureFactory.metricsFeatureProvider) + .action(eq(mContext), eq(MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED), + mLoggingArgumentCatpor.capture()); + final Pair capturedLoggingPair = mLoggingArgumentCatpor.getValue(); + + assertThat(capturedLoggingPair.first) + .isEqualTo(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME); + assertThat(capturedLoggingPair.second) + .isEqualTo(dummyData.getKey()); + SliceTester.testSettingsCopyableSlice(mContext, slice, dummyData); + } + @Test public void testUriBuilder_oemAuthority_intentPath_returnsValidSliceUri() { final Uri expectedUri = new Uri.Builder() diff --git a/tests/robotests/src/com/android/settings/testutils/FakeCopyableController.java b/tests/robotests/src/com/android/settings/testutils/FakeCopyableController.java new file mode 100644 index 00000000000..a02377cf268 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/FakeCopyableController.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.testutils; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.CopyableSlice; + +public class FakeCopyableController extends BasePreferenceController implements + CopyableSlice { + + public FakeCopyableController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public void copy() { + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/SliceTester.java b/tests/robotests/src/com/android/settings/testutils/SliceTester.java index 892f948fe19..be4199da2d5 100644 --- a/tests/robotests/src/com/android/settings/testutils/SliceTester.java +++ b/tests/robotests/src/com/android/settings/testutils/SliceTester.java @@ -168,6 +168,43 @@ public class SliceTester { assertKeywords(metadata, sliceData); } + /** + * Test the copyable slice, including: + * - No intent + * - Correct title + * - Correct intent + * - Correct keywords + * - TTL + * - Color + */ + public static void testSettingsCopyableSlice(Context context, Slice slice, + SliceData sliceData) { + final SliceMetadata metadata = SliceMetadata.from(context, slice); + + final SliceItem colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR); + final int color = colorItem.getInt(); + assertThat(color).isEqualTo(Utils.getColorAccentDefaultColor(context)); + + final SliceAction primaryAction = metadata.getPrimaryAction(); + + final IconCompat expectedIcon = IconCompat.createWithResource(context, + sliceData.getIconResource()); + assertThat(expectedIcon.toString()).isEqualTo(primaryAction.getIcon().toString()); + + final long sliceTTL = metadata.getExpiry(); + assertThat(sliceTTL).isEqualTo(ListBuilder.INFINITY); + + // Check primary intent + final PendingIntent primaryPendingIntent = primaryAction.getAction(); + assertThat(primaryPendingIntent).isEqualTo( + SliceBuilderUtils.getContentPendingIntent(context, sliceData)); + + final List sliceItems = slice.getItems(); + assertTitle(sliceItems, sliceData.getTitle()); + + assertKeywords(metadata, sliceData); + } + /** * Test the contents of an unavailable slice, including: * - No toggles @@ -229,4 +266,4 @@ public class SliceTester { expectedKeywords.add(data.getScreenTitle().toString()); assertThat(keywords).containsExactlyElementsIn(expectedKeywords); } -} \ No newline at end of file +}