1 /*
2  * Copyright (C) 2023 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.BatteryOptimizeHistoricalLogEntry.Action;
20 
21 import android.app.Activity;
22 import android.app.settings.SettingsEnums;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Bundle;
26 import android.os.UserHandle;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.view.View;
30 import android.widget.CompoundButton;
31 import android.widget.CompoundButton.OnCheckedChangeListener;
32 
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.settings.R;
36 import com.android.settings.core.SubSettingLauncher;
37 import com.android.settings.dashboard.DashboardFragment;
38 import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
39 import com.android.settings.overlay.FeatureFactory;
40 import com.android.settings.widget.EntityHeaderController;
41 import com.android.settingslib.HelpUtils;
42 import com.android.settingslib.applications.AppUtils;
43 import com.android.settingslib.applications.ApplicationsState;
44 import com.android.settingslib.core.AbstractPreferenceController;
45 import com.android.settingslib.widget.FooterPreference;
46 import com.android.settingslib.widget.LayoutPreference;
47 import com.android.settingslib.widget.MainSwitchPreference;
48 import com.android.settingslib.widget.SelectorWithWidgetPreference;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.concurrent.ExecutorService;
53 import java.util.concurrent.Executors;
54 
55 /** Allow background usage fragment for each app */
56 public class PowerBackgroundUsageDetail extends DashboardFragment
57         implements SelectorWithWidgetPreference.OnClickListener, OnCheckedChangeListener {
58     private static final String TAG = "PowerBackgroundUsageDetail";
59 
60     public static final String EXTRA_UID = "extra_uid";
61     public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
62     public static final String EXTRA_LABEL = "extra_label";
63     public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
64     public static final String EXTRA_ICON_ID = "extra_icon_id";
65     private static final String KEY_PREF_HEADER = "header_view";
66     private static final String KEY_PREF_UNRESTRICTED = "unrestricted_preference";
67     private static final String KEY_PREF_OPTIMIZED = "optimized_preference";
68     private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
69     private static final String KEY_FOOTER_PREFERENCE = "app_usage_footer_preference";
70 
71     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
72 
73     @VisibleForTesting LayoutPreference mHeaderPreference;
74     @VisibleForTesting ApplicationsState mState;
75     @VisibleForTesting ApplicationsState.AppEntry mAppEntry;
76     @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
77     @VisibleForTesting SelectorWithWidgetPreference mOptimizePreference;
78     @VisibleForTesting SelectorWithWidgetPreference mUnrestrictedPreference;
79     @VisibleForTesting MainSwitchPreference mMainSwitchPreference;
80     @VisibleForTesting FooterPreference mFooterPreference;
81     @VisibleForTesting StringBuilder mLogStringBuilder;
82 
83     @VisibleForTesting @BatteryOptimizeUtils.OptimizationMode
84     int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
85 
86     @Override
onAttach(Activity activity)87     public void onAttach(Activity activity) {
88         super.onAttach(activity);
89 
90         mState = ApplicationsState.getInstance(getActivity().getApplication());
91     }
92 
93     @Override
onCreate(Bundle icicle)94     public void onCreate(Bundle icicle) {
95         super.onCreate(icicle);
96 
97         final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
98         onCreateBackgroundUsageState(packageName);
99         mHeaderPreference = findPreference(KEY_PREF_HEADER);
100 
101         if (packageName != null) {
102             mAppEntry = mState.getEntry(packageName, UserHandle.myUserId());
103         }
104     }
105 
106     @Override
onResume()107     public void onResume() {
108         super.onResume();
109         initHeader();
110         mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
111         initFooter();
112         mLogStringBuilder = new StringBuilder("onResume mode = ").append(mOptimizationMode);
113     }
114 
115     @Override
onPause()116     public void onPause() {
117         super.onPause();
118 
119         final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
120         mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
121         logMetricCategory(currentOptimizeMode);
122 
123         mExecutor.execute(
124                 () -> {
125                     if (currentOptimizeMode != mOptimizationMode) {
126                         AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(
127                                 getContext(), mBatteryOptimizeUtils.getUid());
128                     }
129                     BatteryOptimizeLogUtils.writeLog(
130                             getContext().getApplicationContext(),
131                             Action.LEAVE,
132                             BatteryOptimizeLogUtils.getPackageNameWithUserId(
133                                     mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
134                             mLogStringBuilder.toString());
135                 });
136         Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
137     }
138 
139     @Override
onRadioButtonClicked(SelectorWithWidgetPreference selected)140     public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
141         final String selectedKey = selected == null ? null : selected.getKey();
142         updateSelectorPreferenceState(mUnrestrictedPreference, selectedKey);
143         updateSelectorPreferenceState(mOptimizePreference, selectedKey);
144         mBatteryOptimizeUtils.setAppUsageState(getSelectedPreference(), Action.APPLY);
145     }
146 
147     @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)148     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
149         mMainSwitchPreference.setChecked(isChecked);
150         updateSelectorPreference(isChecked);
151     }
152 
153     @Override
getMetricsCategory()154     public int getMetricsCategory() {
155         return SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND;
156     }
157 
158     @Override
createPreferenceControllers(Context context)159     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
160         final List<AbstractPreferenceController> controllers = new ArrayList<>();
161         final Bundle bundle = getArguments();
162         final int uid = bundle.getInt(EXTRA_UID, 0);
163         final String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
164 
165         controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
166         controllers.add(new OptimizedPreferenceController(context, uid, packageName));
167         controllers.add(new UnrestrictedPreferenceController(context, uid, packageName));
168 
169         return controllers;
170     }
171 
172     @Override
getPreferenceScreenResId()173     protected int getPreferenceScreenResId() {
174         return R.xml.power_background_usage_detail;
175     }
176 
177     @Override
getLogTag()178     protected String getLogTag() {
179         return TAG;
180     }
181 
182     @VisibleForTesting
updateSelectorPreference(boolean isEnabled)183     void updateSelectorPreference(boolean isEnabled) {
184         mOptimizePreference.setEnabled(isEnabled);
185         mUnrestrictedPreference.setEnabled(isEnabled);
186         onRadioButtonClicked(isEnabled ? mOptimizePreference : null);
187     }
188 
189     @VisibleForTesting
getSelectedPreference()190     int getSelectedPreference() {
191         if (!mMainSwitchPreference.isChecked()) {
192             return BatteryOptimizeUtils.MODE_RESTRICTED;
193         } else if (mUnrestrictedPreference.isChecked()) {
194             return BatteryOptimizeUtils.MODE_UNRESTRICTED;
195         } else if (mOptimizePreference.isChecked()) {
196             return BatteryOptimizeUtils.MODE_OPTIMIZED;
197         } else {
198             return BatteryOptimizeUtils.MODE_UNKNOWN;
199         }
200     }
201 
startPowerBackgroundUsageDetailPage(Context context, Bundle args)202     static void startPowerBackgroundUsageDetailPage(Context context, Bundle args) {
203         new SubSettingLauncher(context)
204                 .setDestination(PowerBackgroundUsageDetail.class.getName())
205                 .setArguments(args)
206                 .setSourceMetricsCategory(SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND)
207                 .launch();
208     }
209 
210     @VisibleForTesting
initHeader()211     void initHeader() {
212         final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header);
213         final Activity context = getActivity();
214         final Bundle bundle = getArguments();
215         EntityHeaderController controller =
216                 EntityHeaderController.newInstance(context, this, appSnippet)
217                         .setButtonActions(
218                                 EntityHeaderController.ActionType.ACTION_NONE,
219                                 EntityHeaderController.ActionType.ACTION_NONE);
220 
221         if (mAppEntry == null) {
222             controller.setLabel(bundle.getString(EXTRA_LABEL));
223 
224             final int iconId = bundle.getInt(EXTRA_ICON_ID, 0);
225             if (iconId == 0) {
226                 controller.setIcon(context.getPackageManager().getDefaultActivityIcon());
227             } else {
228                 controller.setIcon(context.getDrawable(bundle.getInt(EXTRA_ICON_ID)));
229             }
230         } else {
231             mState.ensureIcon(mAppEntry);
232             controller.setLabel(mAppEntry);
233             controller.setIcon(mAppEntry);
234             controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info));
235         }
236 
237         controller.done(true /* rebindActions */);
238     }
239 
240     @VisibleForTesting
initFooter()241     void initFooter() {
242         final String stateString;
243         final String footerString;
244         final Context context = getContext();
245 
246         if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
247             // Present optimized only string when the package name is invalid.
248             stateString = context.getString(R.string.manager_battery_usage_optimized_only);
249             footerString =
250                     context.getString(R.string.manager_battery_usage_footer_limited, stateString);
251         } else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
252             // Present unrestricted only string when the package is system or default active app.
253             stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
254             footerString =
255                     context.getString(R.string.manager_battery_usage_footer_limited, stateString);
256         } else {
257             // Present default string to normal app.
258             footerString = context.getString(R.string.manager_battery_usage_footer);
259         }
260         mFooterPreference.setTitle(footerString);
261         final Intent helpIntent =
262                 HelpUtils.getHelpIntent(
263                         context,
264                         context.getString(R.string.help_url_app_usage_settings),
265                         /* backupContext= */ "");
266         if (helpIntent != null) {
267             mFooterPreference.setLearnMoreAction(
268                     v -> startActivityForResult(helpIntent, /* requestCode= */ 0));
269             mFooterPreference.setLearnMoreText(
270                     context.getString(R.string.manager_battery_usage_link_a11y));
271         }
272     }
273 
onCreateBackgroundUsageState(String packageName)274     private void onCreateBackgroundUsageState(String packageName) {
275         mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED);
276         mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED);
277         mMainSwitchPreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
278         mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE);
279 
280         mOptimizePreference.setOnClickListener(this);
281         mUnrestrictedPreference.setOnClickListener(this);
282         mMainSwitchPreference.addOnSwitchChangeListener(this);
283 
284         mBatteryOptimizeUtils =
285                 new BatteryOptimizeUtils(
286                         getContext(), getArguments().getInt(EXTRA_UID), packageName);
287     }
288 
updateSelectorPreferenceState( SelectorWithWidgetPreference preference, String selectedKey)289     private void updateSelectorPreferenceState(
290             SelectorWithWidgetPreference preference, String selectedKey) {
291         preference.setChecked(TextUtils.equals(selectedKey, preference.getKey()));
292     }
293 
logMetricCategory(int currentOptimizeMode)294     private void logMetricCategory(int currentOptimizeMode) {
295         if (currentOptimizeMode == mOptimizationMode) {
296             return;
297         }
298         int metricCategory = 0;
299         switch (currentOptimizeMode) {
300             case BatteryOptimizeUtils.MODE_UNRESTRICTED:
301                 metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_UNRESTRICTED;
302                 break;
303             case BatteryOptimizeUtils.MODE_OPTIMIZED:
304                 metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_OPTIMIZED;
305                 break;
306             case BatteryOptimizeUtils.MODE_RESTRICTED:
307                 metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_RESTRICTED;
308                 break;
309         }
310         if (metricCategory == 0) {
311             return;
312         }
313         int finalMetricCategory = metricCategory;
314         mExecutor.execute(
315                 () -> {
316                     String packageName =
317                             BatteryUtils.getLoggingPackageName(
318                                     getContext(), mBatteryOptimizeUtils.getPackageName());
319                     FeatureFactory.getFeatureFactory()
320                             .getMetricsFeatureProvider()
321                             .action(
322                                     /* attribution */ SettingsEnums
323                                             .LEAVE_POWER_USAGE_MANAGE_BACKGROUND,
324                                     /* action */ finalMetricCategory,
325                                     /* pageId */ SettingsEnums
326                                             .FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND,
327                                     packageName,
328                                     getArguments().getInt(EXTRA_POWER_USAGE_AMOUNT));
329                 });
330     }
331 }
332