Adds contextual cards for screen attention in Settings Homepage

Bug: 128527964
Test: atest ContextualAdaptiveSleepSliceTest, maually verified.
Change-Id: Ifaea7d8d4391e91cf6cbde38a2506728f55913d8
This commit is contained in:
Yi Jiang
2019-04-16 14:51:11 -07:00
parent 0fbbad92f9
commit 7605494cc5
8 changed files with 355 additions and 14 deletions

View File

@@ -0,0 +1,31 @@
<!--
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="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10.67,21H7v-1h3.13c-0.27-0.63-0.46-1.3-0.56-2H7V6h10v3.5c0.69,0,1.36,0.1,2,0.28V3 c0-1.1-0.9-1.99-2-1.99L7,1C5.9,1,5,1.9,5,3v18c0,1.1,0.9,2,2,2h5.52C11.79,22.45,11.16,21.77,10.67,21z M7,3h10v1H7V3z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M17,12.5c2.48,0,4.5,2.02,4.5,4.5s-2.02,4.5-4.5,4.5s-4.5-2.02-4.5-4.5S14.52,12.5,17,12.5 M17,11 c-3.31,0-6,2.69-6,6s2.69,6,6,6c3.31,0,6-2.69,6-6S20.31,11,17,11L17,11z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M17,14v3l-2.12,2.12C15.42,19.66,16.17,20,17,20c1.66,0,3-1.34,3-3S18.66,14,17,14z" />
</vector>

View File

@@ -2833,7 +2833,7 @@
<!-- Setting option summary when adaptive sleep is off [CHAR LIMIT=NONE] -->
<string name="adaptive_sleep_summary_off">Off</string>
<!-- Description about the feature adaptive sleep [CHAR LIMIT=NONE]-->
<string name="adaptive_sleep_description">Prevents your screen from turning off if youre looking at it.</string>
<string name="adaptive_sleep_description">Keep screen on when viewing it</string>
<!-- Description feature's privacy sensitive details to make sure users understand what feature users, what it saves/sends etc [CHAR LIMIT=NONE]-->
<string name="adaptive_sleep_privacy">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.</string>

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<? extends CustomSliceable> getSliceClassByUri(Uri uri) {

View File

@@ -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();
}
}