1/*
2 * Copyright (C) 2009 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
17package com.android.settings.fuelgauge;
18
19import android.app.Activity;
20import android.graphics.drawable.Drawable;
21import android.os.BatteryStats;
22import android.os.Build;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.Message;
26import android.os.Process;
27import android.os.UserHandle;
28import android.preference.Preference;
29import android.preference.PreferenceGroup;
30import android.preference.PreferenceScreen;
31import android.text.TextUtils;
32import android.util.SparseArray;
33import android.util.TypedValue;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37
38import com.android.internal.logging.MetricsLogger;
39import com.android.internal.os.BatterySipper;
40import com.android.internal.os.BatterySipper.DrainType;
41import com.android.internal.os.PowerProfile;
42import com.android.settings.HelpUtils;
43import com.android.settings.R;
44import com.android.settings.Settings.HighPowerApplicationsActivity;
45import com.android.settings.SettingsActivity;
46import com.android.settings.applications.ManageApplications;
47
48import java.util.ArrayList;
49import java.util.Collections;
50import java.util.Comparator;
51import java.util.List;
52
53/**
54 * Displays a list of apps and subsystems that consume power, ordered by how much power was
55 * consumed since the last time it was unplugged.
56 */
57public class PowerUsageSummary extends PowerUsageBase {
58
59    private static final boolean DEBUG = false;
60
61    private static final boolean USE_FAKE_DATA = false;
62
63    static final String TAG = "PowerUsageSummary";
64
65    private static final String KEY_APP_LIST = "app_list";
66    private static final String KEY_BATTERY_HISTORY = "battery_history";
67
68    private static final int MENU_STATS_TYPE = Menu.FIRST;
69    private static final int MENU_BATTERY_SAVER = Menu.FIRST + 2;
70    private static final int MENU_HIGH_POWER_APPS = Menu.FIRST + 3;
71    private static final int MENU_HELP = Menu.FIRST + 4;
72
73    private BatteryHistoryPreference mHistPref;
74    private PreferenceGroup mAppListGroup;
75
76    private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
77
78    private static final int MIN_POWER_THRESHOLD_MILLI_AMP = 5;
79    private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10;
80    private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
81    private static final int SECONDS_IN_HOUR = 60 * 60;
82
83    @Override
84    public void onCreate(Bundle icicle) {
85        super.onCreate(icicle);
86
87        addPreferencesFromResource(R.xml.power_usage_summary);
88        mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_HISTORY);
89        mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST);
90    }
91
92    @Override
93    protected int getMetricsCategory() {
94        return MetricsLogger.FUELGAUGE_POWER_USAGE_SUMMARY;
95    }
96
97    @Override
98    public void onResume() {
99        super.onResume();
100        refreshStats();
101    }
102
103    @Override
104    public void onPause() {
105        BatteryEntry.stopRequestQueue();
106        mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
107        super.onPause();
108    }
109
110    @Override
111    public void onDestroy() {
112        super.onDestroy();
113        if (getActivity().isChangingConfigurations()) {
114            BatteryEntry.clearUidCache();
115        }
116    }
117
118    @Override
119    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
120        if (!(preference instanceof PowerGaugePreference)) {
121            return false;
122        }
123        PowerGaugePreference pgp = (PowerGaugePreference) preference;
124        BatteryEntry entry = pgp.getInfo();
125        PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), mStatsHelper,
126                mStatsType, entry, true);
127        return super.onPreferenceTreeClick(preferenceScreen, preference);
128    }
129
130    @Override
131    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
132        if (DEBUG) {
133            menu.add(0, MENU_STATS_TYPE, 0, R.string.menu_stats_total)
134                    .setIcon(com.android.internal.R.drawable.ic_menu_info_details)
135                    .setAlphabeticShortcut('t');
136        }
137
138        MenuItem batterySaver = menu.add(0, MENU_BATTERY_SAVER, 0, R.string.battery_saver);
139        batterySaver.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
140
141        menu.add(0, MENU_HIGH_POWER_APPS, 0, R.string.high_power_apps);
142        super.onCreateOptionsMenu(menu, inflater);
143    }
144
145    @Override
146    protected int getHelpResource() {
147        return R.string.help_url_battery;
148    }
149
150    @Override
151    public boolean onOptionsItemSelected(MenuItem item) {
152        final SettingsActivity sa = (SettingsActivity) getActivity();
153        switch (item.getItemId()) {
154            case MENU_STATS_TYPE:
155                if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
156                    mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
157                } else {
158                    mStatsType = BatteryStats.STATS_SINCE_CHARGED;
159                }
160                refreshStats();
161                return true;
162            case MENU_BATTERY_SAVER:
163                sa.startPreferencePanel(BatterySaverSettings.class.getName(), null,
164                        R.string.battery_saver, null, null, 0);
165                return true;
166            case MENU_HIGH_POWER_APPS:
167                Bundle args = new Bundle();
168                args.putString(ManageApplications.EXTRA_CLASSNAME,
169                        HighPowerApplicationsActivity.class.getName());
170                sa.startPreferencePanel(ManageApplications.class.getName(), args,
171                        R.string.high_power_apps, null, null, 0);
172                return true;
173            default:
174                return super.onOptionsItemSelected(item);
175        }
176    }
177
178    private void addNotAvailableMessage() {
179        Preference notAvailable = new Preference(getActivity());
180        notAvailable.setTitle(R.string.power_usage_not_available);
181        mAppListGroup.addPreference(notAvailable);
182    }
183
184    private static boolean isSharedGid(int uid) {
185        return UserHandle.getAppIdFromSharedAppGid(uid) > 0;
186    }
187
188    private static boolean isSystemUid(int uid) {
189        return uid >= Process.SYSTEM_UID && uid < Process.FIRST_APPLICATION_UID;
190    }
191
192    /**
193     * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
194     * exists for all users of the same app. We detect this case and merge the power use
195     * for dex2oat to the device OWNER's use of the app.
196     * @return A sorted list of apps using power.
197     */
198    private static List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) {
199        final SparseArray<BatterySipper> uidList = new SparseArray<>();
200
201        final ArrayList<BatterySipper> results = new ArrayList<>();
202        final int numSippers = sippers.size();
203        for (int i = 0; i < numSippers; i++) {
204            BatterySipper sipper = sippers.get(i);
205            if (sipper.getUid() > 0) {
206                int realUid = sipper.getUid();
207
208                // Check if this UID is a shared GID. If so, we combine it with the OWNER's
209                // actual app UID.
210                if (isSharedGid(sipper.getUid())) {
211                    realUid = UserHandle.getUid(UserHandle.USER_OWNER,
212                            UserHandle.getAppIdFromSharedAppGid(sipper.getUid()));
213                }
214
215                // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc).
216                if (isSystemUid(realUid)
217                        && !"mediaserver".equals(sipper.packageWithHighestDrain)) {
218                    // Use the system UID for all UIDs running in their own sandbox that
219                    // are not apps. We exclude mediaserver because we already are expected to
220                    // report that as a separate item.
221                    realUid = Process.SYSTEM_UID;
222                }
223
224                if (realUid != sipper.getUid()) {
225                    // Replace the BatterySipper with a new one with the real UID set.
226                    BatterySipper newSipper = new BatterySipper(sipper.drainType,
227                            new FakeUid(realUid), 0.0);
228                    newSipper.add(sipper);
229                    newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
230                    newSipper.mPackages = sipper.mPackages;
231                    sipper = newSipper;
232                }
233
234                int index = uidList.indexOfKey(realUid);
235                if (index < 0) {
236                    // New entry.
237                    uidList.put(realUid, sipper);
238                } else {
239                    // Combine BatterySippers if we already have one with this UID.
240                    final BatterySipper existingSipper = uidList.valueAt(index);
241                    existingSipper.add(sipper);
242                    if (existingSipper.packageWithHighestDrain == null
243                            && sipper.packageWithHighestDrain != null) {
244                        existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
245                    }
246
247                    final int existingPackageLen = existingSipper.mPackages != null ?
248                            existingSipper.mPackages.length : 0;
249                    final int newPackageLen = sipper.mPackages != null ?
250                            sipper.mPackages.length : 0;
251                    if (newPackageLen > 0) {
252                        String[] newPackages = new String[existingPackageLen + newPackageLen];
253                        if (existingPackageLen > 0) {
254                            System.arraycopy(existingSipper.mPackages, 0, newPackages, 0,
255                                    existingPackageLen);
256                        }
257                        System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen,
258                                newPackageLen);
259                        existingSipper.mPackages = newPackages;
260                    }
261                }
262            } else {
263                results.add(sipper);
264            }
265        }
266
267        final int numUidSippers = uidList.size();
268        for (int i = 0; i < numUidSippers; i++) {
269            results.add(uidList.valueAt(i));
270        }
271
272        // The sort order must have changed, so re-sort based on total power use.
273        Collections.sort(results, new Comparator<BatterySipper>() {
274            @Override
275            public int compare(BatterySipper a, BatterySipper b) {
276                return Double.compare(b.totalPowerMah, a.totalPowerMah);
277            }
278        });
279        return results;
280    }
281
282    protected void refreshStats() {
283        super.refreshStats();
284        updatePreference(mHistPref);
285        mAppListGroup.removeAll();
286        mAppListGroup.setOrderingAsAdded(false);
287        boolean addedSome = false;
288
289        final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
290        final BatteryStats stats = mStatsHelper.getStats();
291        final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
292
293        TypedValue value = new TypedValue();
294        getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
295        int colorControl = getContext().getColor(value.resourceId);
296
297        if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
298            final List<BatterySipper> usageList = getCoalescedUsageList(
299                    USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());
300
301            final int dischargeAmount = USE_FAKE_DATA ? 5000
302                    : stats != null ? stats.getDischargeAmount(mStatsType) : 0;
303            final int numSippers = usageList.size();
304            for (int i = 0; i < numSippers; i++) {
305                final BatterySipper sipper = usageList.get(i);
306                if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
307                    continue;
308                }
309                double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
310                final double percentOfTotal =
311                        ((sipper.totalPowerMah / totalPower) * dischargeAmount);
312                if (((int) (percentOfTotal + .5)) < 1) {
313                    continue;
314                }
315                if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
316                    // Don't show over-counted unless it is at least 2/3 the size of
317                    // the largest real entry, and its percent of total is more significant
318                    if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
319                        continue;
320                    }
321                    if (percentOfTotal < 10) {
322                        continue;
323                    }
324                    if ("user".equals(Build.TYPE)) {
325                        continue;
326                    }
327                }
328                if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
329                    // Don't show over-counted unless it is at least 1/2 the size of
330                    // the largest real entry, and its percent of total is more significant
331                    if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
332                        continue;
333                    }
334                    if (percentOfTotal < 5) {
335                        continue;
336                    }
337                    if ("user".equals(Build.TYPE)) {
338                        continue;
339                    }
340                }
341                final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
342                final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper);
343                final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(),
344                        userHandle);
345                final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(),
346                        userHandle);
347                final PowerGaugePreference pref = new PowerGaugePreference(getActivity(),
348                        badgedIcon, contentDescription, entry);
349
350                final double percentOfMax = (sipper.totalPowerMah * 100)
351                        / mStatsHelper.getMaxPower();
352                sipper.percent = percentOfTotal;
353                pref.setTitle(entry.getLabel());
354                pref.setOrder(i + 1);
355                pref.setPercent(percentOfMax, percentOfTotal);
356                if (sipper.uidObj != null) {
357                    pref.setKey(Integer.toString(sipper.uidObj.getUid()));
358                }
359                if ((sipper.drainType != DrainType.APP || sipper.uidObj.getUid() == 0)
360                         && sipper.drainType != DrainType.USER) {
361                    pref.setTint(colorControl);
362                }
363                addedSome = true;
364                mAppListGroup.addPreference(pref);
365                if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
366                    break;
367                }
368            }
369        }
370        if (!addedSome) {
371            addNotAvailableMessage();
372        }
373
374        BatteryEntry.startRequestQueue();
375    }
376
377    private static List<BatterySipper> getFakeStats() {
378        ArrayList<BatterySipper> stats = new ArrayList<>();
379        float use = 5;
380        for (DrainType type : DrainType.values()) {
381            if (type == DrainType.APP) {
382                continue;
383            }
384            stats.add(new BatterySipper(type, null, use));
385            use += 5;
386        }
387        stats.add(new BatterySipper(DrainType.APP,
388                new FakeUid(Process.FIRST_APPLICATION_UID), use));
389        stats.add(new BatterySipper(DrainType.APP,
390                new FakeUid(0), use));
391
392        // Simulate dex2oat process.
393        BatterySipper sipper = new BatterySipper(DrainType.APP,
394                new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
395        sipper.packageWithHighestDrain = "dex2oat";
396        stats.add(sipper);
397
398        sipper = new BatterySipper(DrainType.APP,
399                new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f);
400        sipper.packageWithHighestDrain = "dex2oat";
401        stats.add(sipper);
402
403        sipper = new BatterySipper(DrainType.APP,
404                new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f);
405        stats.add(sipper);
406
407        return stats;
408    }
409
410    Handler mHandler = new Handler() {
411
412        @Override
413        public void handleMessage(Message msg) {
414            switch (msg.what) {
415                case BatteryEntry.MSG_UPDATE_NAME_ICON:
416                    BatteryEntry entry = (BatteryEntry) msg.obj;
417                    PowerGaugePreference pgp =
418                            (PowerGaugePreference) findPreference(
419                                    Integer.toString(entry.sipper.uidObj.getUid()));
420                    if (pgp != null) {
421                        final int userId = UserHandle.getUserId(entry.sipper.getUid());
422                        final UserHandle userHandle = new UserHandle(userId);
423                        pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle));
424                        pgp.setTitle(entry.name);
425                    }
426                    break;
427                case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
428                    Activity activity = getActivity();
429                    if (activity != null) {
430                        activity.reportFullyDrawn();
431                    }
432                    break;
433            }
434            super.handleMessage(msg);
435        }
436    };
437}
438