1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settings.homepage.contextualcards.slices; 17 18 import static android.provider.Settings.Global.LOW_POWER_MODE; 19 20 import static androidx.slice.builders.ListBuilder.ICON_IMAGE; 21 22 import android.annotation.ColorInt; 23 import android.app.PendingIntent; 24 import android.app.UiModeManager; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.ContentObserver; 28 import android.net.Uri; 29 import android.os.BatteryManager; 30 import android.os.Build; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.PowerManager; 34 import android.provider.Settings; 35 import android.util.Log; 36 37 import androidx.annotation.VisibleForTesting; 38 import androidx.core.graphics.drawable.IconCompat; 39 import androidx.slice.Slice; 40 import androidx.slice.builders.ListBuilder; 41 import androidx.slice.builders.SliceAction; 42 43 import com.android.settings.R; 44 import com.android.settings.Utils; 45 import com.android.settings.overlay.FeatureFactory; 46 import com.android.settings.slices.CustomSliceRegistry; 47 import com.android.settings.slices.CustomSliceable; 48 import com.android.settings.slices.SliceBackgroundWorker; 49 50 public class DarkThemeSlice implements CustomSliceable { 51 private static final String TAG = "DarkThemeSlice"; 52 private static final boolean DEBUG = Build.IS_DEBUGGABLE; 53 private static final int BATTERY_LEVEL_THRESHOLD = 50; 54 private static final int DELAY_TIME_EXECUTING_DARK_THEME = 200; 55 56 // Keep the slice even Dark theme mode changed when it is on HomePage 57 @VisibleForTesting 58 static boolean sKeepSliceShow; 59 @VisibleForTesting 60 static long sActiveUiSession = -1000; 61 @VisibleForTesting 62 static boolean sSliceClicked = false; 63 static boolean sPreChecked = false; 64 65 private final Context mContext; 66 private final UiModeManager mUiModeManager; 67 private final PowerManager mPowerManager; 68 DarkThemeSlice(Context context)69 public DarkThemeSlice(Context context) { 70 mContext = context; 71 mUiModeManager = context.getSystemService(UiModeManager.class); 72 mPowerManager = context.getSystemService(PowerManager.class); 73 } 74 75 @Override getSlice()76 public Slice getSlice() { 77 final long currentUiSession = FeatureFactory.getFeatureFactory() 78 .getSlicesFeatureProvider().getUiSessionToken(); 79 if (currentUiSession != sActiveUiSession) { 80 sActiveUiSession = currentUiSession; 81 sKeepSliceShow = false; 82 } 83 84 // 1. Dark theme slice will disappear when battery saver is ON. 85 // 2. If the slice is shown and the user doesn't toggle it directly, but instead turns on 86 // Dark theme from Quick settings or display page, the card should no longer persist. 87 // This card will persist when user clicks its toggle directly. 88 // 3. If the slice is shown and the user toggles it on (switch to Dark theme) directly, 89 // then user returns to home (launcher), no matter by the Back key or Home gesture. 90 // Next time the Settings displays on screen again this card should no longer persist. 91 if (DEBUG) { 92 Log.d(TAG, 93 "sKeepSliceShow = " + sKeepSliceShow + ", sSliceClicked = " + sSliceClicked 94 + ", isAvailable = " + isAvailable(mContext)); 95 } 96 if (mPowerManager.isPowerSaveMode() 97 || ((!sKeepSliceShow || !sSliceClicked) && !isAvailable(mContext))) { 98 return new ListBuilder(mContext, CustomSliceRegistry.DARK_THEME_SLICE_URI, 99 ListBuilder.INFINITY) 100 .setIsError(true) 101 .build(); 102 } 103 sKeepSliceShow = true; 104 final PendingIntent toggleAction = getBroadcastIntent(mContext); 105 @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); 106 final IconCompat icon = 107 IconCompat.createWithResource(mContext, R.drawable.dark_theme); 108 109 final boolean isChecked = Utils.isNightMode(mContext); 110 if (sPreChecked != isChecked) { 111 // Dark(Night) mode changed and reset the sSliceClicked. 112 resetValue(isChecked, false); 113 } 114 return new ListBuilder(mContext, CustomSliceRegistry.DARK_THEME_SLICE_URI, 115 ListBuilder.INFINITY) 116 .setAccentColor(color) 117 .addRow(new ListBuilder.RowBuilder() 118 .setTitle(mContext.getText(R.string.dark_theme_slice_title)) 119 .setTitleItem(icon, ICON_IMAGE) 120 .setSubtitle(mContext.getText(R.string.dark_theme_slice_subtitle)) 121 .setPrimaryAction( 122 SliceAction.createToggle(toggleAction, null /* actionTitle */, 123 isChecked))) 124 .build(); 125 } 126 127 @Override getUri()128 public Uri getUri() { 129 return CustomSliceRegistry.DARK_THEME_SLICE_URI; 130 } 131 132 @Override onNotifyChange(Intent intent)133 public void onNotifyChange(Intent intent) { 134 final boolean isChecked = intent.getBooleanExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, 135 false); 136 // Dark(Night) mode changed by user clicked the toggle in the Dark theme slice. 137 if (isChecked) { 138 resetValue(isChecked, true); 139 } 140 // make toggle transition more smooth before dark theme takes effect 141 new Handler(Looper.getMainLooper()).postDelayed(() -> { 142 mUiModeManager.setNightModeActivated(isChecked); 143 }, DELAY_TIME_EXECUTING_DARK_THEME); 144 } 145 146 @Override getIntent()147 public Intent getIntent() { 148 return null; 149 } 150 151 @Override getSliceHighlightMenuRes()152 public int getSliceHighlightMenuRes() { 153 return R.string.menu_key_display; 154 } 155 156 @Override getBackgroundWorkerClass()157 public Class getBackgroundWorkerClass() { 158 return DarkThemeWorker.class; 159 } 160 161 @VisibleForTesting isAvailable(Context context)162 boolean isAvailable(Context context) { 163 // check if dark theme mode is enabled or if dark theme scheduling is on. 164 if (Utils.isNightMode(context) || isNightModeScheduled()) { 165 return false; 166 } 167 // checking the current battery level 168 final BatteryManager batteryManager = context.getSystemService(BatteryManager.class); 169 final int level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); 170 Log.d(TAG, "battery level = " + level); 171 return level <= BATTERY_LEVEL_THRESHOLD; 172 } 173 resetValue(boolean preChecked, boolean clicked)174 private void resetValue(boolean preChecked, boolean clicked) { 175 sPreChecked = preChecked; 176 sSliceClicked = clicked; 177 } 178 isNightModeScheduled()179 private boolean isNightModeScheduled() { 180 final int mode = mUiModeManager.getNightMode(); 181 if (DEBUG) { 182 Log.d(TAG, "night mode = " + mode); 183 } 184 // Turn on from sunset to sunrise or turn on at custom time 185 if (mode == UiModeManager.MODE_NIGHT_AUTO || mode == UiModeManager.MODE_NIGHT_CUSTOM) { 186 return true; 187 } 188 return false; 189 } 190 191 public static class DarkThemeWorker extends SliceBackgroundWorker<Void> { 192 private final Context mContext; 193 private final ContentObserver mContentObserver = 194 new ContentObserver(new Handler(Looper.getMainLooper())) { 195 @Override 196 public void onChange(boolean bChanged) { 197 if (mContext.getSystemService(PowerManager.class).isPowerSaveMode()) { 198 notifySliceChange(); 199 } 200 } 201 }; 202 DarkThemeWorker(Context context, Uri uri)203 public DarkThemeWorker(Context context, Uri uri) { 204 super(context, uri); 205 mContext = context; 206 } 207 208 @Override onSlicePinned()209 protected void onSlicePinned() { 210 mContext.getContentResolver().registerContentObserver( 211 Settings.Global.getUriFor(LOW_POWER_MODE), false /* notifyForDescendants */, 212 mContentObserver); 213 } 214 215 @Override onSliceUnpinned()216 protected void onSliceUnpinned() { 217 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 218 } 219 220 @Override close()221 public void close() { 222 } 223 } 224 } 225