1 /*
2  * Copyright (C) 2017 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.fuelgauge;
18 
19 import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUserConsumer;
20 
21 import android.app.Activity;
22 import android.app.ActivityManager;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.view.View;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.VisibleForTesting;
35 import androidx.preference.Preference;
36 
37 import com.android.settings.R;
38 import com.android.settings.SettingsActivity;
39 import com.android.settings.Utils;
40 import com.android.settings.applications.appinfo.AppButtonsPreferenceController;
41 import com.android.settings.applications.appinfo.ButtonActionDialogFragment;
42 import com.android.settings.core.InstrumentedPreferenceFragment;
43 import com.android.settings.core.SubSettingLauncher;
44 import com.android.settings.dashboard.DashboardFragment;
45 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
46 import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
47 import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
48 import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
49 import com.android.settings.overlay.FeatureFactory;
50 import com.android.settings.widget.EntityHeaderController;
51 import com.android.settingslib.PrimarySwitchPreference;
52 import com.android.settingslib.applications.AppUtils;
53 import com.android.settingslib.applications.ApplicationsState;
54 import com.android.settingslib.core.AbstractPreferenceController;
55 import com.android.settingslib.core.instrumentation.Instrumentable;
56 import com.android.settingslib.widget.LayoutPreference;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.concurrent.ExecutorService;
61 import java.util.concurrent.Executors;
62 
63 /**
64  * Power usage detail fragment for each app, this fragment contains <br>
65  * <br>
66  * 1. Detail battery usage information for app(i.e. usage time, usage amount) <br>
67  * 2. Battery related controls for app(i.e uninstall, force stop)
68  */
69 public class AdvancedPowerUsageDetail extends DashboardFragment
70         implements ButtonActionDialogFragment.AppButtonsDialogListener,
71                 Preference.OnPreferenceClickListener,
72                 Preference.OnPreferenceChangeListener {
73     public static final String TAG = "AdvancedPowerDetail";
74     public static final String EXTRA_UID = "extra_uid";
75     public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
76     public static final String EXTRA_FOREGROUND_TIME = "extra_foreground_time";
77     public static final String EXTRA_BACKGROUND_TIME = "extra_background_time";
78     public static final String EXTRA_SCREEN_ON_TIME = "extra_screen_on_time";
79     public static final String EXTRA_ANOMALY_HINT_PREF_KEY = "extra_anomaly_hint_pref_key";
80     public static final String EXTRA_ANOMALY_HINT_TEXT = "extra_anomaly_hint_text";
81     public static final String EXTRA_SHOW_TIME_INFO = "extra_show_time_info";
82     public static final String EXTRA_SLOT_TIME = "extra_slot_time";
83     public static final String EXTRA_LABEL = "extra_label";
84     public static final String EXTRA_ICON_ID = "extra_icon_id";
85     public static final String EXTRA_POWER_USAGE_PERCENT = "extra_power_usage_percent";
86     public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
87 
88     private static final String KEY_PREF_HEADER = "header_view";
89     private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
90 
91     private static final int REQUEST_UNINSTALL = 0;
92     private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1;
93 
94     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
95 
96     private AppButtonsPreferenceController mAppButtonsPreferenceController;
97     private PowerUsageTimeController mPowerUsageTimeController;
98 
99     @VisibleForTesting LayoutPreference mHeaderPreference;
100     @VisibleForTesting ApplicationsState mState;
101     @VisibleForTesting ApplicationsState.AppEntry mAppEntry;
102     @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
103     @VisibleForTesting PrimarySwitchPreference mAllowBackgroundUsagePreference;
104 
105     @VisibleForTesting @BatteryOptimizeUtils.OptimizationMode
106     int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
107 
108     @VisibleForTesting StringBuilder mLogStringBuilder;
109 
110     // A wrapper class to carry LaunchBatteryDetailPage required arguments.
111     private static final class LaunchBatteryDetailPageArgs {
112         private String mUsagePercent;
113         private String mPackageName;
114         private String mAppLabel;
115         private String mSlotInformation;
116         private String mAnomalyHintText;
117         private String mAnomalyHintPrefKey;
118         private int mUid;
119         private int mIconId;
120         private int mConsumedPower;
121         private long mForegroundTimeMs;
122         private long mBackgroundTimeMs;
123         private long mScreenOnTimeMs;
124         private boolean mShowTimeInformation;
125         private boolean mIsUserEntry;
126     }
127 
128     /** Launches battery details page for an individual battery consumer fragment. */
startBatteryDetailPage( Context context, int sourceMetricsCategory, BatteryDiffEntry diffEntry, String usagePercent, String slotInformation, boolean showTimeInformation, String anomalyHintPrefKey, String anomalyHintText)129     public static void startBatteryDetailPage(
130             Context context,
131             int sourceMetricsCategory,
132             BatteryDiffEntry diffEntry,
133             String usagePercent,
134             String slotInformation,
135             boolean showTimeInformation,
136             String anomalyHintPrefKey,
137             String anomalyHintText) {
138         final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
139         // configure the launch argument.
140         launchArgs.mUsagePercent = usagePercent;
141         launchArgs.mPackageName = diffEntry.getPackageName();
142         launchArgs.mAppLabel = diffEntry.getAppLabel();
143         launchArgs.mSlotInformation = slotInformation;
144         launchArgs.mUid = (int) diffEntry.mUid;
145         launchArgs.mIconId = diffEntry.getAppIconId();
146         launchArgs.mConsumedPower = (int) diffEntry.mConsumePower;
147         launchArgs.mShowTimeInformation = showTimeInformation;
148         if (launchArgs.mShowTimeInformation) {
149             launchArgs.mForegroundTimeMs = diffEntry.mForegroundUsageTimeInMs;
150             launchArgs.mBackgroundTimeMs =
151                     diffEntry.mBackgroundUsageTimeInMs + diffEntry.mForegroundServiceUsageTimeInMs;
152             launchArgs.mScreenOnTimeMs = diffEntry.mScreenOnTimeInMs;
153             launchArgs.mAnomalyHintPrefKey = anomalyHintPrefKey;
154             launchArgs.mAnomalyHintText = anomalyHintText;
155         }
156         launchArgs.mIsUserEntry = isUserConsumer(diffEntry.mConsumerType);
157         startBatteryDetailPage(context, sourceMetricsCategory, launchArgs);
158     }
159 
160     /** Launches battery details page for an individual battery consumer. */
startBatteryDetailPage( Activity caller, InstrumentedPreferenceFragment fragment, BatteryEntry entry, String usagePercent)161     public static void startBatteryDetailPage(
162             Activity caller,
163             InstrumentedPreferenceFragment fragment,
164             BatteryEntry entry,
165             String usagePercent) {
166         final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
167         // configure the launch argument.
168         launchArgs.mUsagePercent = usagePercent;
169         launchArgs.mPackageName = entry.getDefaultPackageName();
170         launchArgs.mAppLabel = entry.getLabel();
171         launchArgs.mUid = entry.getUid();
172         launchArgs.mIconId = entry.mIconId;
173         launchArgs.mConsumedPower = (int) entry.getConsumedPower();
174         launchArgs.mIsUserEntry = entry.isUserEntry();
175         launchArgs.mShowTimeInformation = false;
176         startBatteryDetailPage(caller, fragment.getMetricsCategory(), launchArgs);
177     }
178 
startBatteryDetailPage( Context context, int sourceMetricsCategory, LaunchBatteryDetailPageArgs launchArgs)179     private static void startBatteryDetailPage(
180             Context context, int sourceMetricsCategory, LaunchBatteryDetailPageArgs launchArgs) {
181         final Bundle args = new Bundle();
182         if (launchArgs.mPackageName == null) {
183             // populate data for system app
184             args.putString(EXTRA_LABEL, launchArgs.mAppLabel);
185             args.putInt(EXTRA_ICON_ID, launchArgs.mIconId);
186             args.putString(EXTRA_PACKAGE_NAME, null);
187         } else {
188             // populate data for normal app
189             args.putString(EXTRA_PACKAGE_NAME, launchArgs.mPackageName);
190         }
191 
192         args.putInt(EXTRA_UID, launchArgs.mUid);
193         args.putLong(EXTRA_BACKGROUND_TIME, launchArgs.mBackgroundTimeMs);
194         args.putLong(EXTRA_FOREGROUND_TIME, launchArgs.mForegroundTimeMs);
195         args.putLong(EXTRA_SCREEN_ON_TIME, launchArgs.mScreenOnTimeMs);
196         args.putString(EXTRA_SLOT_TIME, launchArgs.mSlotInformation);
197         args.putString(EXTRA_POWER_USAGE_PERCENT, launchArgs.mUsagePercent);
198         args.putInt(EXTRA_POWER_USAGE_AMOUNT, launchArgs.mConsumedPower);
199         args.putBoolean(EXTRA_SHOW_TIME_INFO, launchArgs.mShowTimeInformation);
200         args.putString(EXTRA_ANOMALY_HINT_PREF_KEY, launchArgs.mAnomalyHintPrefKey);
201         args.putString(EXTRA_ANOMALY_HINT_TEXT, launchArgs.mAnomalyHintText);
202         final int userId =
203                 launchArgs.mIsUserEntry
204                         ? ActivityManager.getCurrentUser()
205                         : UserHandle.getUserId(launchArgs.mUid);
206 
207         new SubSettingLauncher(context)
208                 .setDestination(AdvancedPowerUsageDetail.class.getName())
209                 .setTitleRes(R.string.battery_details_title)
210                 .setArguments(args)
211                 .setSourceMetricsCategory(sourceMetricsCategory)
212                 .setUserHandle(new UserHandle(userId))
213                 .launch();
214     }
215 
216     /** Start packageName's battery detail page. */
startBatteryDetailPage( Activity caller, Instrumentable instrumentable, String packageName, UserHandle userHandle)217     public static void startBatteryDetailPage(
218             Activity caller,
219             Instrumentable instrumentable,
220             String packageName,
221             UserHandle userHandle) {
222         final Bundle args = new Bundle(3);
223         final PackageManager packageManager = caller.getPackageManager();
224         args.putString(EXTRA_PACKAGE_NAME, packageName);
225         args.putString(EXTRA_POWER_USAGE_PERCENT, Utils.formatPercentage(0));
226         try {
227             args.putInt(EXTRA_UID, packageManager.getPackageUid(packageName, 0 /* no flag */));
228         } catch (PackageManager.NameNotFoundException e) {
229             Log.w(TAG, "Cannot find package: " + packageName, e);
230         }
231 
232         new SubSettingLauncher(caller)
233                 .setDestination(AdvancedPowerUsageDetail.class.getName())
234                 .setTitleRes(R.string.battery_details_title)
235                 .setArguments(args)
236                 .setSourceMetricsCategory(instrumentable.getMetricsCategory())
237                 .setUserHandle(userHandle)
238                 .launch();
239     }
240 
241     @Override
onAttach(Activity activity)242     public void onAttach(Activity activity) {
243         super.onAttach(activity);
244 
245         mState = ApplicationsState.getInstance(getActivity().getApplication());
246     }
247 
248     @Override
onCreate(Bundle icicle)249     public void onCreate(Bundle icicle) {
250         super.onCreate(icicle);
251 
252         final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
253         onCreateBackgroundUsageState(packageName);
254         mHeaderPreference = findPreference(KEY_PREF_HEADER);
255 
256         if (packageName != null) {
257             mAppEntry = mState.getEntry(packageName, UserHandle.myUserId());
258         }
259     }
260 
261     @Override
onResume()262     public void onResume() {
263         super.onResume();
264 
265         initHeader();
266         mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
267         initFooter();
268         mLogStringBuilder = new StringBuilder("onResume mode = ").append(mOptimizationMode);
269     }
270 
271     @Override
shouldSkipForInitialSUW()272     protected boolean shouldSkipForInitialSUW() {
273         return true;
274     }
275 
276     @Override
onPause()277     public void onPause() {
278         super.onPause();
279 
280         final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
281         mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
282         logMetricCategory(currentOptimizeMode);
283         mExecutor.execute(
284                 () -> {
285                     if (currentOptimizeMode != mOptimizationMode) {
286                         AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(
287                                 getContext(), mBatteryOptimizeUtils.getUid());
288                     }
289                     BatteryOptimizeLogUtils.writeLog(
290                             getContext().getApplicationContext(),
291                             Action.LEAVE,
292                             BatteryOptimizeLogUtils.getPackageNameWithUserId(
293                                     mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
294                             mLogStringBuilder.toString());
295                 });
296         Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
297     }
298 
299     @VisibleForTesting
initHeader()300     void initHeader() {
301         final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header);
302         final Activity context = getActivity();
303         final Bundle bundle = getArguments();
304         EntityHeaderController controller =
305                 EntityHeaderController.newInstance(context, this, appSnippet)
306                         .setButtonActions(
307                                 EntityHeaderController.ActionType.ACTION_NONE,
308                                 EntityHeaderController.ActionType.ACTION_NONE);
309 
310         if (mAppEntry == null) {
311             controller.setLabel(bundle.getString(EXTRA_LABEL));
312 
313             final int iconId = bundle.getInt(EXTRA_ICON_ID, 0);
314             if (iconId == 0) {
315                 controller.setIcon(context.getPackageManager().getDefaultActivityIcon());
316             } else {
317                 controller.setIcon(context.getDrawable(bundle.getInt(EXTRA_ICON_ID)));
318             }
319         } else {
320             mState.ensureIcon(mAppEntry);
321             controller.setLabel(mAppEntry);
322             controller.setIcon(mAppEntry);
323             controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info));
324         }
325 
326         if (mPowerUsageTimeController != null) {
327             final String slotTime = bundle.getString(EXTRA_SLOT_TIME);
328             final long screenOnTimeInMs = bundle.getLong(EXTRA_SCREEN_ON_TIME);
329             final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME);
330             final String anomalyHintPrefKey = bundle.getString(EXTRA_ANOMALY_HINT_PREF_KEY);
331             final String anomalyHintText = bundle.getString(EXTRA_ANOMALY_HINT_TEXT);
332             mPowerUsageTimeController.handleScreenTimeUpdated(
333                     slotTime,
334                     screenOnTimeInMs,
335                     backgroundTimeMs,
336                     anomalyHintPrefKey,
337                     anomalyHintText);
338         }
339         controller.done(true /* rebindActions */);
340     }
341 
342     @VisibleForTesting
initFooter()343     void initFooter() {
344         final String stateString;
345         final String detailInfoString;
346         final Context context = getContext();
347 
348         if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
349             // Present optimized only string when the package name is invalid.
350             stateString = context.getString(R.string.manager_battery_usage_optimized_only);
351             detailInfoString =
352                     context.getString(R.string.manager_battery_usage_footer_limited, stateString);
353         } else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
354             // Present unrestricted only string when the package is system or default active app.
355             stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
356             detailInfoString =
357                     context.getString(R.string.manager_battery_usage_footer_limited, stateString);
358         } else {
359             // Present default string to normal app.
360             detailInfoString =
361                     context.getString(
362                             R.string.manager_battery_usage_allow_background_usage_summary);
363         }
364         mAllowBackgroundUsagePreference.setSummary(detailInfoString);
365     }
366 
367     @Override
getMetricsCategory()368     public int getMetricsCategory() {
369         return SettingsEnums.FUELGAUGE_POWER_USAGE_DETAIL;
370     }
371 
372     @Override
getLogTag()373     protected String getLogTag() {
374         return TAG;
375     }
376 
377     @Override
getPreferenceScreenResId()378     protected int getPreferenceScreenResId() {
379         return R.xml.power_usage_detail;
380     }
381 
382     @Override
createPreferenceControllers(Context context)383     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
384         final List<AbstractPreferenceController> controllers = new ArrayList<>();
385         final Bundle bundle = getArguments();
386         final int uid = bundle.getInt(EXTRA_UID, 0);
387         final String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
388 
389         mAppButtonsPreferenceController =
390                 new AppButtonsPreferenceController(
391                         (SettingsActivity) getActivity(),
392                         this,
393                         getSettingsLifecycle(),
394                         packageName,
395                         mState,
396                         REQUEST_UNINSTALL,
397                         REQUEST_REMOVE_DEVICE_ADMIN);
398         if (bundle.getBoolean(EXTRA_SHOW_TIME_INFO, false)) {
399             mPowerUsageTimeController = new PowerUsageTimeController(getContext());
400             controllers.add(mPowerUsageTimeController);
401         }
402         controllers.add(mAppButtonsPreferenceController);
403         controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
404 
405         return controllers;
406     }
407 
408     @Override
onActivityResult(int requestCode, int resultCode, Intent data)409     public void onActivityResult(int requestCode, int resultCode, Intent data) {
410         super.onActivityResult(requestCode, resultCode, data);
411         if (mAppButtonsPreferenceController != null) {
412             mAppButtonsPreferenceController.handleActivityResult(requestCode, resultCode, data);
413         }
414     }
415 
416     @Override
handleDialogClick(int id)417     public void handleDialogClick(int id) {
418         if (mAppButtonsPreferenceController != null) {
419             mAppButtonsPreferenceController.handleDialogClick(id);
420         }
421     }
422 
423     @Override
onPreferenceClick(Preference preference)424     public boolean onPreferenceClick(Preference preference) {
425         if (!(preference instanceof PrimarySwitchPreference)
426                 || !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
427             return false;
428         }
429         PowerBackgroundUsageDetail.startPowerBackgroundUsageDetailPage(
430                 getContext(), getArguments());
431         return true;
432     }
433 
434     @Override
onPreferenceChange(@onNull Preference preference, Object newValue)435     public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
436         if (!(preference instanceof PrimarySwitchPreference)
437                 || !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
438             return false;
439         }
440         if (newValue instanceof Boolean) {
441             final boolean isAllowBackgroundUsage = (boolean) newValue;
442             mBatteryOptimizeUtils.setAppUsageState(
443                     isAllowBackgroundUsage
444                             ? BatteryOptimizeUtils.MODE_OPTIMIZED
445                             : BatteryOptimizeUtils.MODE_RESTRICTED,
446                     Action.APPLY);
447         }
448         return true;
449     }
450 
logMetricCategory(int currentOptimizeMode)451     private void logMetricCategory(int currentOptimizeMode) {
452         if (currentOptimizeMode == mOptimizationMode) {
453             return;
454         }
455         int metricCategory = 0;
456         switch (currentOptimizeMode) {
457             case BatteryOptimizeUtils.MODE_UNRESTRICTED:
458             case BatteryOptimizeUtils.MODE_OPTIMIZED:
459                 metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_ALLOW_BACKGROUND;
460                 break;
461             case BatteryOptimizeUtils.MODE_RESTRICTED:
462                 metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_DISABLE_BACKGROUND;
463                 break;
464         }
465         if (metricCategory == 0) {
466             return;
467         }
468         int finalMetricCategory = metricCategory;
469         mExecutor.execute(
470                 () -> {
471                     String packageName =
472                             BatteryUtils.getLoggingPackageName(
473                                     getContext(), mBatteryOptimizeUtils.getPackageName());
474                     FeatureFactory.getFeatureFactory()
475                             .getMetricsFeatureProvider()
476                             .action(
477                                     /* attribution */ SettingsEnums.LEAVE_APP_BATTERY_USAGE,
478                                     /* action */ finalMetricCategory,
479                                     /* pageId */ SettingsEnums.FUELGAUGE_POWER_USAGE_DETAIL,
480                                     packageName,
481                                     getArguments().getInt(EXTRA_POWER_USAGE_AMOUNT));
482                 });
483     }
484 
onCreateBackgroundUsageState(String packageName)485     private void onCreateBackgroundUsageState(String packageName) {
486         mAllowBackgroundUsagePreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
487         if (mAllowBackgroundUsagePreference != null) {
488             mAllowBackgroundUsagePreference.setOnPreferenceClickListener(this);
489             mAllowBackgroundUsagePreference.setOnPreferenceChangeListener(this);
490         }
491 
492         mBatteryOptimizeUtils =
493                 new BatteryOptimizeUtils(
494                         getContext(), getArguments().getInt(EXTRA_UID), packageName);
495     }
496 }
497