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