From 7605494cc5d3c8a68eb995945e3b4adbe0b7f285 Mon Sep 17 00:00:00 2001 From: Yi Jiang Date: Tue, 16 Apr 2019 14:51:11 -0700 Subject: [PATCH] Adds contextual cards for screen attention in Settings Homepage Bug: 128527964 Test: atest ContextualAdaptiveSleepSliceTest, maually verified. Change-Id: Ifaea7d8d4391e91cf6cbde38a2506728f55913d8 --- res/drawable/ic_settings_adaptive_sleep.xml | 31 ++++ res/values/strings.xml | 2 +- .../AdaptiveSleepPreferenceController.java | 29 ++-- .../display/AdaptiveSleepSettings.java | 12 ++ .../SettingsContextualCardProvider.java | 9 + .../slices/ContextualAdaptiveSleepSlice.java | 154 ++++++++++++++++++ .../settings/slices/CustomSliceRegistry.java | 17 +- .../ContextualAdaptiveSleepSliceTest.java | 115 +++++++++++++ 8 files changed, 355 insertions(+), 14 deletions(-) create mode 100644 res/drawable/ic_settings_adaptive_sleep.xml create mode 100644 src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSlice.java create mode 100644 tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSliceTest.java diff --git a/res/drawable/ic_settings_adaptive_sleep.xml b/res/drawable/ic_settings_adaptive_sleep.xml new file mode 100644 index 00000000000..765ed949a0e --- /dev/null +++ b/res/drawable/ic_settings_adaptive_sleep.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 4f962991f4b..8bcadc0f85e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2833,7 +2833,7 @@ Off - Prevents your screen from turning off if you’re looking at it. + Keep screen on when viewing it Screen attention uses the front camera to see if someone is looking at the screen. It works on device, and images are never stored or sent to Google. diff --git a/src/com/android/settings/display/AdaptiveSleepPreferenceController.java b/src/com/android/settings/display/AdaptiveSleepPreferenceController.java index 6b91792800e..e83410d3d7a 100644 --- a/src/com/android/settings/display/AdaptiveSleepPreferenceController.java +++ b/src/com/android/settings/display/AdaptiveSleepPreferenceController.java @@ -25,9 +25,9 @@ import com.android.settings.core.TogglePreferenceController; public class AdaptiveSleepPreferenceController extends TogglePreferenceController { - - private final String SYSTEM_KEY = ADAPTIVE_SLEEP; - private final int DEFAULT_VALUE = 0; + public static final String PREF_NAME = "adaptive_sleep"; + private static final String SYSTEM_KEY = ADAPTIVE_SLEEP; + private static final int DEFAULT_VALUE = 0; final boolean hasSufficientPermissions; @@ -35,9 +35,7 @@ public class AdaptiveSleepPreferenceController extends TogglePreferenceControlle super(context, key); final PackageManager packageManager = mContext.getPackageManager(); - final String attentionPackage = packageManager.getAttentionServicePackageName(); - hasSufficientPermissions = attentionPackage != null && packageManager.checkPermission( - Manifest.permission.CAMERA, attentionPackage) == PackageManager.PERMISSION_GRANTED; + hasSufficientPermissions = hasSufficientPermission(packageManager); } @Override @@ -46,7 +44,6 @@ public class AdaptiveSleepPreferenceController extends TogglePreferenceControlle SYSTEM_KEY, DEFAULT_VALUE) != DEFAULT_VALUE; } - @Override public boolean setChecked(boolean isChecked) { Settings.System.putInt(mContext.getContentResolver(), SYSTEM_KEY, @@ -57,10 +54,7 @@ public class AdaptiveSleepPreferenceController extends TogglePreferenceControlle @Override @AvailabilityStatus public int getAvailabilityStatus() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_adaptive_sleep_available) - ? AVAILABLE_UNSEARCHABLE - : UNSUPPORTED_ON_DEVICE; + return isControllerAvailable(mContext); } @Override @@ -69,4 +63,17 @@ public class AdaptiveSleepPreferenceController extends TogglePreferenceControlle ? R.string.adaptive_sleep_summary_on : R.string.adaptive_sleep_summary_off); } + + public static int isControllerAvailable(Context context) { + return context.getResources().getBoolean( + com.android.internal.R.bool.config_adaptive_sleep_available) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + private static boolean hasSufficientPermission(PackageManager packageManager) { + final String attentionPackage = packageManager.getAttentionServicePackageName(); + return attentionPackage != null && packageManager.checkPermission( + Manifest.permission.CAMERA, attentionPackage) == PackageManager.PERMISSION_GRANTED; + } } diff --git a/src/com/android/settings/display/AdaptiveSleepSettings.java b/src/com/android/settings/display/AdaptiveSleepSettings.java index 4c17a67b717..d0f2c9aa0b1 100644 --- a/src/com/android/settings/display/AdaptiveSleepSettings.java +++ b/src/com/android/settings/display/AdaptiveSleepSettings.java @@ -16,10 +16,15 @@ package com.android.settings.display; +import static com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice.PREF; +import static com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice.PREF_KEY_INTERACTED; + import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.provider.SearchIndexableResource; +import android.util.Log; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -40,8 +45,15 @@ public class AdaptiveSleepSettings extends DashboardFragment { super.onCreate(icicle); final FooterPreference footerPreference = mFooterPreferenceMixin.createFooterPreference(); + final Context context = getContext(); + footerPreference.setIcon(R.drawable.ic_privacy_shield_24dp); footerPreference.setTitle(R.string.adaptive_sleep_privacy); + + context.getSharedPreferences(PREF, Context.MODE_PRIVATE) + .edit() + .putBoolean(PREF_KEY_INTERACTED, true) + .apply(); } @Override diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java index 86fee03e556..aaae076e620 100644 --- a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java @@ -64,12 +64,21 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setCardName(contextualNotificationChannelSliceUri) .setCardCategory(ContextualCard.Category.POSSIBLE) .build(); + final String contextualAdaptiveSleepSliceUri = + CustomSliceRegistry.CONTEXTUAL_ADAPTIVE_SLEEP_URI.toString(); + final ContextualCard contextualAdaptiveSleepCard = + ContextualCard.newBuilder() + .setSliceUri(contextualAdaptiveSleepSliceUri) + .setCardName(contextualAdaptiveSleepSliceUri) + .setCardCategory(ContextualCard.Category.DEFAULT) + .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(connectedDeviceCard) .addCard(lowStorageCard) .addCard(batteryFixCard) .addCard(notificationChannelCard) + .addCard(contextualAdaptiveSleepCard) .build(); return cards; diff --git a/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSlice.java b/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSlice.java new file mode 100644 index 00000000000..2c091fa716c --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSlice.java @@ -0,0 +1,154 @@ +/* + * 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.settings.homepage.contextualcards.slices; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.display.AdaptiveSleepPreferenceController.PREF_NAME; +import static com.android.settings.display.AdaptiveSleepPreferenceController.isControllerAvailable; +import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_ADAPTIVE_SLEEP_URI; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.display.AdaptiveSleepSettings; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.concurrent.TimeUnit; + +public class ContextualAdaptiveSleepSlice implements CustomSliceable { + private static final String TAG = "ContextualAdaptiveSleepSlice"; + private static final long DEFAULT_SETUP_TIME = 0; + private Context mContext; + + @VisibleForTesting + static final long DEFERRED_TIME_DAYS = TimeUnit.DAYS.toMillis(14); + @VisibleForTesting + static final String PREF_KEY_SETUP_TIME = "adaptive_sleep_setup_time"; + + public static final String PREF_KEY_INTERACTED = "adaptive_sleep_interacted"; + public static final String PREF = "adaptive_sleep_slice"; + + public ContextualAdaptiveSleepSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + final long setupTime = mContext.getSharedPreferences(PREF, Context.MODE_PRIVATE).getLong( + PREF_KEY_SETUP_TIME, DEFAULT_SETUP_TIME); + if (setupTime == DEFAULT_SETUP_TIME) { + // Set the first setup time. + mContext.getSharedPreferences(PREF, Context.MODE_PRIVATE) + .edit() + .putLong(PREF_KEY_SETUP_TIME, System.currentTimeMillis()) + .apply(); + return null; + } + + // Display the contextual card only if all the following 3 conditions hold: + // 1. The Screen Attention is enabled in Settings. + // 2. The device is not recently set up. + // 3. Current user hasn't opened Screen Attention's settings page before. + if (isSettingsAvailable() && !isUserInteracted() && !isRecentlySetup()) { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_settings_adaptive_sleep); + final CharSequence title = mContext.getText(R.string.adaptive_sleep_title); + final CharSequence subtitle = mContext.getText(R.string.adaptive_sleep_description); + + final SliceAction pAction = SliceAction.createDeeplink(getPrimaryAction(), + icon, + ListBuilder.ICON_IMAGE, + title); + final ListBuilder listBuilder = new ListBuilder(mContext, + CONTEXTUAL_ADAPTIVE_SLEEP_URI, + ListBuilder.INFINITY) + .addRow(new ListBuilder.RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(subtitle) + .setPrimaryAction(pAction)); + return listBuilder.build(); + } else { + return null; + } + } + + @Override + public Uri getUri() { + return CONTEXTUAL_ADAPTIVE_SLEEP_URI; + } + + @Override + public Intent getIntent() { + final CharSequence screenTitle = mContext.getText(R.string.adaptive_sleep_title); + final Uri contentUri = new Uri.Builder().appendPath(PREF_NAME).build(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + AdaptiveSleepSettings.class.getName(), PREF_NAME, screenTitle.toString(), + SettingsEnums.SLICE).setClassName(mContext.getPackageName(), + SubSettings.class.getName()).setData(contentUri); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + /** + * @return {@code true} if the current user has opened the Screen Attention settings page + * before, otherwise {@code false}. + */ + private boolean isUserInteracted() { + return mContext.getSharedPreferences(PREF, Context.MODE_PRIVATE).getBoolean( + PREF_KEY_INTERACTED, false); + } + + /** + * The device is recently set up means its first settings-open time is within 2 weeks ago. + * + * @return {@code true} if the device is recently set up, otherwise {@code false}. + */ + private boolean isRecentlySetup() { + final long endTime = System.currentTimeMillis() - DEFERRED_TIME_DAYS; + final long firstSetupTime = mContext.getSharedPreferences(PREF, + Context.MODE_PRIVATE).getLong(PREF_KEY_SETUP_TIME, DEFAULT_SETUP_TIME); + return firstSetupTime > endTime; + } + + /** + * Check whether the screen attention settings is enabled. Contextual card will only appear + * when the screen attention settings is available. + * + * @return {@code true} if screen attention settings is enabled, otherwise {@code false} + */ + @VisibleForTesting + boolean isSettingsAvailable() { + return isControllerAvailable(mContext) == AVAILABLE; + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index dc3324b3d92..ebfd7b34e7e 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -26,6 +26,7 @@ import android.util.ArrayMap; import androidx.annotation.VisibleForTesting; +import com.android.settings.display.AdaptiveSleepPreferenceController; import com.android.settings.flashlight.FlashlightSlice; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; @@ -34,6 +35,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.EmergencyInfoSli import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.BatteryFixSlice; import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlice; +import com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice; import com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice; import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice; @@ -64,6 +66,16 @@ public class CustomSliceRegistry { .appendPath(SettingsSlicesContract.KEY_AIRPLANE_MODE) .build(); + /** + * Uri for Contextual Adaptive Sleep Slice + */ + public static final Uri CONTEXTUAL_ADAPTIVE_SLEEP_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath(AdaptiveSleepPreferenceController.PREF_NAME) + .build(); + /** * Uri for Battery Fix Slice. */ @@ -328,6 +340,7 @@ public class CustomSliceRegistry { sUriToSlice.put(BATTERY_FIX_SLICE_URI, BatteryFixSlice.class); sUriToSlice.put(BLUETOOTH_DEVICES_SLICE_URI, BluetoothDevicesSlice.class); + sUriToSlice.put(CONTEXTUAL_ADAPTIVE_SLEEP_URI, ContextualAdaptiveSleepSlice.class); sUriToSlice.put(CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI, ContextualNotificationChannelSlice.class); sUriToSlice.put(CONTEXTUAL_WIFI_SLICE_URI, ContextualWifiSlice.class); @@ -337,12 +350,12 @@ public class CustomSliceRegistry { sUriToSlice.put(FLASHLIGHT_SLICE_URI, FlashlightSlice.class); sUriToSlice.put(LOCATION_SLICE_URI, LocationSlice.class); sUriToSlice.put(LOW_STORAGE_SLICE_URI, LowStorageSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); sUriToSlice.put(MOBILE_DATA_SLICE_URI, MobileDataSlice.class); sUriToSlice.put(NOTIFICATION_CHANNEL_SLICE_URI, NotificationChannelSlice.class); sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class); sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class); - sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); - sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class); } public static Class getSliceClassByUri(Uri uri) { diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSliceTest.java new file mode 100644 index 00000000000..54fb2c3281a --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ContextualAdaptiveSleepSliceTest.java @@ -0,0 +1,115 @@ +/* + * 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.settings.homepage.contextualcards.slices; + +import static com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice.DEFERRED_TIME_DAYS; +import static com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice.PREF; +import static com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice.PREF_KEY_SETUP_TIME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; + +import androidx.slice.Slice; +import androidx.slice.SliceProvider; +import androidx.slice.widget.SliceLiveData; + +import com.android.settings.slices.CustomSliceRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ContextualAdaptiveSleepSliceTest { + + private static final String pkgName = "adaptive_sleep"; + private Context mContext; + private ContextualAdaptiveSleepSlice mContextualAdaptiveSleepSlice; + @Mock + private PackageManager mPackageManager; + @Mock + private SharedPreferences mSharedPreferences; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + mContext = spy(RuntimeEnvironment.application); + mContextualAdaptiveSleepSlice = spy(new ContextualAdaptiveSleepSlice(mContext)); + + doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(mSharedPreferences).when(mContext).getSharedPreferences(eq(PREF), anyInt()); + doReturn(true).when(mContextualAdaptiveSleepSlice).isSettingsAvailable(); + doReturn(pkgName).when(mPackageManager).getAttentionServicePackageName(); + doReturn(-DEFERRED_TIME_DAYS).when(mSharedPreferences).getLong(eq(PREF_KEY_SETUP_TIME), + anyLong()); + } + + @Test + public void getUri_shouldReturnContextualAdaptiveSleepSliceUri() { + final Uri uri = mContextualAdaptiveSleepSlice.getUri(); + + assertThat(uri).isEqualTo(CustomSliceRegistry.CONTEXTUAL_ADAPTIVE_SLEEP_URI); + } + + @Test + public void getSlice_ShowIfFeatureIsAvailable() { + final Slice slice = mContextualAdaptiveSleepSlice.getSlice(); + + assertThat(slice).isNotNull(); + } + + @Test + public void getSlice_DoNotShowIfFeatureIsUnavailable() { + doReturn(false).when(mContextualAdaptiveSleepSlice).isSettingsAvailable(); + + final Slice slice = mContextualAdaptiveSleepSlice.getSlice(); + + assertThat(slice).isNull(); + } + + @Test + public void getSlice_ShowIfNotRecentlySetup() { + final Slice slice = mContextualAdaptiveSleepSlice.getSlice(); + + assertThat(slice).isNotNull(); + } + + @Test + public void getSlice_DoNotShowIfRecentlySetup() { + doReturn(System.currentTimeMillis()).when(mSharedPreferences).getLong( + eq(PREF_KEY_SETUP_TIME), anyLong()); + + final Slice slice = mContextualAdaptiveSleepSlice.getSlice(); + + assertThat(slice).isNull(); + } +}