1 /* 2 * Copyright (C) 2018 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 17 package com.android.settings.homepage.contextualcards.slices; 18 19 import static android.content.Context.MODE_PRIVATE; 20 21 import static com.android.settings.slices.CustomSliceRegistry.BATTERY_FIX_SLICE_URI; 22 23 import android.app.PendingIntent; 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.graphics.PorterDuff; 29 import android.graphics.PorterDuffColorFilter; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.BatteryUsageStats; 33 import android.util.ArrayMap; 34 import android.view.View; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.annotation.WorkerThread; 38 import androidx.core.graphics.drawable.IconCompat; 39 import androidx.slice.Slice; 40 import androidx.slice.builders.ListBuilder; 41 import androidx.slice.builders.ListBuilder.RowBuilder; 42 import androidx.slice.builders.SliceAction; 43 44 import com.android.settings.R; 45 import com.android.settings.SubSettings; 46 import com.android.settings.Utils; 47 import com.android.settings.fuelgauge.BatteryUsageStatsLoader; 48 import com.android.settings.fuelgauge.PowerUsageSummary; 49 import com.android.settings.fuelgauge.batterytip.BatteryTipLoader; 50 import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; 51 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; 52 import com.android.settings.slices.CustomSliceable; 53 import com.android.settings.slices.SliceBackgroundWorker; 54 import com.android.settings.slices.SliceBuilderUtils; 55 import com.android.settingslib.utils.ThreadUtils; 56 57 import java.util.Arrays; 58 import java.util.List; 59 import java.util.Map; 60 61 public class BatteryFixSlice implements CustomSliceable { 62 63 @VisibleForTesting 64 static final String PREFS = "battery_fix_prefs"; 65 @VisibleForTesting 66 static final String KEY_CURRENT_TIPS_TYPE = "current_tip_type"; 67 static final String KEY_CURRENT_TIPS_STATE = "current_tip_state"; 68 69 // A map tracking which BatteryTip and which state of that tip is not important. 70 private static final Map<Integer, List<Integer>> UNIMPORTANT_BATTERY_TIPS; 71 72 static { 73 UNIMPORTANT_BATTERY_TIPS = new ArrayMap<>(); UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.SUMMARY, Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED))74 UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.SUMMARY, 75 Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED)); UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.HIGH_DEVICE_USAGE, Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED))76 UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.HIGH_DEVICE_USAGE, 77 Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED)); UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.BATTERY_SAVER, Arrays.asList(BatteryTip.StateType.HANDLED))78 UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.BATTERY_SAVER, 79 Arrays.asList(BatteryTip.StateType.HANDLED)); 80 } 81 82 private static final String TAG = "BatteryFixSlice"; 83 84 private final Context mContext; 85 BatteryFixSlice(Context context)86 public BatteryFixSlice(Context context) { 87 mContext = context; 88 } 89 90 @Override getUri()91 public Uri getUri() { 92 return BATTERY_FIX_SLICE_URI; 93 } 94 95 @Override getSlice()96 public Slice getSlice() { 97 final ListBuilder sliceBuilder = 98 new ListBuilder(mContext, BATTERY_FIX_SLICE_URI, ListBuilder.INFINITY) 99 .setAccentColor(COLOR_NOT_TINTED); 100 101 if (!isBatteryTipAvailableFromCache(mContext)) { 102 return buildBatteryGoodSlice(sliceBuilder, true /* isError */); 103 } 104 105 final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance(getUri()); 106 final List<BatteryTip> batteryTips = worker != null ? worker.getResults() : null; 107 108 if (batteryTips == null) { 109 // Because we need wait slice background worker return data 110 return buildBatteryGoodSlice(sliceBuilder, false /* isError */); 111 } 112 113 for (BatteryTip batteryTip : batteryTips) { 114 if (batteryTip.getState() == BatteryTip.StateType.INVISIBLE) { 115 continue; 116 } 117 final Drawable drawable = mContext.getDrawable(batteryTip.getIconId()); 118 final int iconTintColorId = batteryTip.getIconTintColorId(); 119 if (iconTintColorId != View.NO_ID) { 120 drawable.setColorFilter(new PorterDuffColorFilter( 121 mContext.getResources().getColor(iconTintColorId), 122 PorterDuff.Mode.SRC_IN)); 123 } 124 125 final IconCompat icon = Utils.createIconWithDrawable(drawable); 126 final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), 127 icon, 128 ListBuilder.ICON_IMAGE, 129 batteryTip.getTitle(mContext)); 130 sliceBuilder.addRow(new RowBuilder() 131 .setTitleItem(icon, ListBuilder.ICON_IMAGE) 132 .setTitle(batteryTip.getTitle(mContext)) 133 .setSubtitle(batteryTip.getSummary(mContext)) 134 .setPrimaryAction(primaryAction)); 135 break; 136 } 137 return sliceBuilder.build(); 138 } 139 140 @Override getIntent()141 public Intent getIntent() { 142 final String screenTitle = mContext.getText(R.string.power_usage_summary_title) 143 .toString(); 144 final Uri contentUri = new Uri.Builder() 145 .appendPath(BatteryTipPreferenceController.PREF_NAME).build(); 146 147 return SliceBuilderUtils.buildSearchResultPageIntent(mContext, 148 PowerUsageSummary.class.getName(), BatteryTipPreferenceController.PREF_NAME, 149 screenTitle, 150 SettingsEnums.SLICE) 151 .setClassName(mContext.getPackageName(), SubSettings.class.getName()) 152 .setData(contentUri); 153 } 154 155 @Override onNotifyChange(Intent intent)156 public void onNotifyChange(Intent intent) { 157 } 158 159 @Override getBackgroundWorkerClass()160 public Class getBackgroundWorkerClass() { 161 return BatteryTipWorker.class; 162 } 163 getPrimaryAction()164 private PendingIntent getPrimaryAction() { 165 final Intent intent = getIntent(); 166 return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 167 PendingIntent.FLAG_IMMUTABLE); 168 } 169 buildBatteryGoodSlice(ListBuilder sliceBuilder, boolean isError)170 private Slice buildBatteryGoodSlice(ListBuilder sliceBuilder, boolean isError) { 171 final IconCompat icon = IconCompat.createWithResource(mContext, 172 R.drawable.ic_battery_status_good_24dp); 173 final String title = mContext.getString(R.string.power_usage_summary_title); 174 final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon, 175 ListBuilder.ICON_IMAGE, title); 176 sliceBuilder.addRow(new RowBuilder() 177 .setTitleItem(icon, ListBuilder.ICON_IMAGE) 178 .setTitle(title) 179 .setPrimaryAction(primaryAction)) 180 .setIsError(isError); 181 return sliceBuilder.build(); 182 } 183 184 // TODO(b/114807643): we should find a better way to get current battery tip type quickly 185 // Now we save battery tip type to shared preference when battery level changes updateBatteryTipAvailabilityCache(Context context)186 public static void updateBatteryTipAvailabilityCache(Context context) { 187 ThreadUtils.postOnBackgroundThread(() -> refreshBatteryTips(context)); 188 } 189 190 191 @VisibleForTesting isBatteryTipAvailableFromCache(Context context)192 static boolean isBatteryTipAvailableFromCache(Context context) { 193 final SharedPreferences prefs = context.getSharedPreferences(PREFS, MODE_PRIVATE); 194 195 final int type = prefs.getInt(KEY_CURRENT_TIPS_TYPE, BatteryTip.TipType.SUMMARY); 196 final int state = prefs.getInt(KEY_CURRENT_TIPS_STATE, BatteryTip.StateType.INVISIBLE); 197 if (state == BatteryTip.StateType.INVISIBLE) { 198 // State is INVISIBLE, We should not show anything. 199 return false; 200 } 201 final boolean unimportant = UNIMPORTANT_BATTERY_TIPS.containsKey(type) 202 && UNIMPORTANT_BATTERY_TIPS.get(type).contains(state); 203 return !unimportant; 204 } 205 206 @WorkerThread 207 @VisibleForTesting refreshBatteryTips(Context context)208 static List<BatteryTip> refreshBatteryTips(Context context) { 209 final BatteryUsageStatsLoader statsLoader = new BatteryUsageStatsLoader(context, 210 /* includeBatteryHistory */ false); 211 final BatteryUsageStats batteryUsageStats = statsLoader.loadInBackground(); 212 final BatteryTipLoader loader = new BatteryTipLoader(context, batteryUsageStats); 213 final List<BatteryTip> batteryTips = loader.loadInBackground(); 214 for (BatteryTip batteryTip : batteryTips) { 215 if (batteryTip.getState() != BatteryTip.StateType.INVISIBLE) { 216 context.getSharedPreferences(PREFS, MODE_PRIVATE) 217 .edit() 218 .putInt(KEY_CURRENT_TIPS_TYPE, batteryTip.getType()) 219 .putInt(KEY_CURRENT_TIPS_STATE, batteryTip.getState()) 220 .apply(); 221 break; 222 } 223 } 224 return batteryTips; 225 } 226 227 public static class BatteryTipWorker extends SliceBackgroundWorker<BatteryTip> { 228 229 private final Context mContext; 230 BatteryTipWorker(Context context, Uri uri)231 public BatteryTipWorker(Context context, Uri uri) { 232 super(context, uri); 233 mContext = context; 234 } 235 236 @Override onSlicePinned()237 protected void onSlicePinned() { 238 ThreadUtils.postOnBackgroundThread(() -> { 239 final List<BatteryTip> batteryTips = refreshBatteryTips(mContext); 240 updateResults(batteryTips); 241 }); 242 } 243 244 @Override onSliceUnpinned()245 protected void onSliceUnpinned() { 246 } 247 248 @Override close()249 public void close() { 250 } 251 } 252 } 253