/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.fuelgauge.batteryusage; import android.app.usage.UsageEvents; import android.content.Context; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.Utils; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Manages the async tasks to process battery and app usage data. * * For now, there exist 4 async tasks in this manager: * * * If there is battery level data, the first 3 async tasks will be started at the same time. * * * If there is no battery level data, the 4th async task will be started only and the usage map * callback function will be applied directly to show the app list on the UI. */ public class DataProcessManager { private static final String TAG = "DataProcessManager"; private static final List POWER_CONNECTION_EVENTS = List.of(BatteryEventType.POWER_CONNECTED, BatteryEventType.POWER_DISCONNECTED); private static final List BATTERY_LEVEL_RECORD_EVENTS = List.of(BatteryEventType.FULL_CHARGED, BatteryEventType.EVEN_HOUR); // For testing only. @VisibleForTesting static Map> sFakeBatteryHistoryMap; // Raw start timestamp with round to the nearest hour. private final long mRawStartTimestamp; private final long mLastFullChargeTimestamp; private final Context mContext; private final Handler mHandler; private final UserManager mUserManager; private final OnBatteryDiffDataMapLoadedListener mCallbackFunction; private final List mAppUsageEventList = new ArrayList<>(); private final List mBatteryEventList = new ArrayList<>(); private final List mBatteryUsageSlotList = new ArrayList<>(); private final List mHourlyBatteryLevelsPerDay; private final Map> mBatteryHistoryMap; private boolean mIsCurrentBatteryHistoryLoaded = false; private boolean mIsCurrentAppUsageLoaded = false; private boolean mIsDatabaseAppUsageLoaded = false; private boolean mIsBatteryEventLoaded = false; private boolean mIsBatteryUsageSlotLoaded = false; // Used to identify whether screen-on time data should be shown in the UI. private boolean mShowScreenOnTime = true; private Set mSystemAppsPackageNames = null; private Set mSystemAppsUids = null; /** * The indexed {@link AppUsagePeriod} list data for each corresponding time slot. *

{@code Long} stands for the userId.

*

{@code String} stands for the packageName.

*/ private Map>>>> mAppUsagePeriodMap; /** * A callback listener when all the data is processed. * This happens when all the async tasks complete and generate the final callback. */ public interface OnBatteryDiffDataMapLoadedListener { /** The callback function when all the data is processed. */ void onBatteryDiffDataMapLoaded(Map batteryDiffDataMap); } /** * Constructor when there exists battery level data. */ DataProcessManager( Context context, Handler handler, final long rawStartTimestamp, final long lastFullChargeTimestamp, @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction, @NonNull final List hourlyBatteryLevelsPerDay, @NonNull final Map> batteryHistoryMap) { mContext = context.getApplicationContext(); mHandler = handler; mUserManager = mContext.getSystemService(UserManager.class); mRawStartTimestamp = rawStartTimestamp; mLastFullChargeTimestamp = lastFullChargeTimestamp; mCallbackFunction = callbackFunction; mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; mBatteryHistoryMap = batteryHistoryMap; } /** * Constructor when there is no battery level data. */ DataProcessManager( Context context, Handler handler, @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction) { mContext = context.getApplicationContext(); mHandler = handler; mUserManager = mContext.getSystemService(UserManager.class); mCallbackFunction = callbackFunction; mRawStartTimestamp = 0L; mLastFullChargeTimestamp = 0L; mHourlyBatteryLevelsPerDay = null; mBatteryHistoryMap = null; // When there is no battery level data, don't show screen-on time and battery level chart on // the UI. mShowScreenOnTime = false; } /** * Starts the async tasks to load battery history data and app usage data. */ public void start() { start(/*isFromPeriodJob=*/ false); } /** * Starts the async tasks to load battery history data and app usage data. */ public void start(boolean isFromPeriodJob) { // If we have battery level data, load the battery history map and app usage simultaneously. if (mHourlyBatteryLevelsPerDay != null) { if (isFromPeriodJob) { mIsCurrentBatteryHistoryLoaded = true; mIsCurrentAppUsageLoaded = true; mIsBatteryUsageSlotLoaded = true; } else { // Loads the latest battery history data from the service. loadCurrentBatteryHistoryMap(); // Loads the latest app usage list from the service. loadCurrentAppUsageList(); // Loads existing battery usage slots from database. loadBatteryUsageSlotList(); } // Loads app usage list from database. loadDatabaseAppUsageList(); // Loads the battery event list from database. loadPowerConnectionBatteryEventList(); } else { // If there is no battery level data, only load the battery history data from service // and show it as the app list directly. loadAndApplyBatteryMapFromServiceOnly(); } } @VisibleForTesting List getAppUsageEventList() { return mAppUsageEventList; } @VisibleForTesting Map>>>> getAppUsagePeriodMap() { return mAppUsagePeriodMap; } @VisibleForTesting boolean getIsCurrentAppUsageLoaded() { return mIsCurrentAppUsageLoaded; } @VisibleForTesting boolean getIsDatabaseAppUsageLoaded() { return mIsDatabaseAppUsageLoaded; } @VisibleForTesting boolean getIsBatteryEventLoaded() { return mIsBatteryEventLoaded; } @VisibleForTesting boolean getIsCurrentBatteryHistoryLoaded() { return mIsCurrentBatteryHistoryLoaded; } @VisibleForTesting boolean getShowScreenOnTime() { return mShowScreenOnTime; } private void loadCurrentBatteryHistoryMap() { new AsyncTask>() { @Override protected Map doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); // Loads the current battery usage data from the battery stats service. final Map currentBatteryHistoryMap = DataProcessor.getCurrentBatteryHistoryMapFromStatsService( mContext); Log.d(TAG, String.format("execute loadCurrentBatteryHistoryMap size=%d in %d/ms", currentBatteryHistoryMap.size(), (System.currentTimeMillis() - startTime))); return currentBatteryHistoryMap; } @Override protected void onPostExecute( final Map currentBatteryHistoryMap) { if (mBatteryHistoryMap != null) { // Replaces the placeholder in mBatteryHistoryMap. for (Map.Entry> mapEntry : mBatteryHistoryMap.entrySet()) { if (mapEntry.getValue().containsKey( DataProcessor.CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { mapEntry.setValue(currentBatteryHistoryMap); } } } mIsCurrentBatteryHistoryLoaded = true; tryToGenerateFinalDataAndApplyCallback(); } }.execute(); } private void loadCurrentAppUsageList() { new AsyncTask>() { @Override protected List doInBackground(Void... voids) { if (!shouldLoadAppUsageData()) { Log.d(TAG, "not loadCurrentAppUsageList"); return null; } final long startTime = System.currentTimeMillis(); // Loads the current battery usage data from the battery stats service. final int currentUserId = getCurrentUserId(); final int workProfileUserId = getWorkProfileUserId(); final UsageEvents usageEventsForCurrentUser = DataProcessor.getAppUsageEventsForUser( mContext, currentUserId, mRawStartTimestamp); // If fail to load usage events for current user, return null directly and screen-on // time will not be shown in the UI. if (usageEventsForCurrentUser == null) { Log.w(TAG, "usageEventsForCurrentUser is null"); return null; } UsageEvents usageEventsForWorkProfile = null; if (workProfileUserId != Integer.MIN_VALUE) { usageEventsForWorkProfile = DataProcessor.getAppUsageEventsForUser( mContext, workProfileUserId, mRawStartTimestamp); } else { Log.d(TAG, "there is no work profile"); } final Map usageEventsMap = new HashMap<>(); usageEventsMap.put(Long.valueOf(currentUserId), usageEventsForCurrentUser); if (usageEventsForWorkProfile != null) { Log.d(TAG, "usageEventsForWorkProfile is null"); usageEventsMap.put(Long.valueOf(workProfileUserId), usageEventsForWorkProfile); } final List appUsageEventList = DataProcessor.generateAppUsageEventListFromUsageEvents( mContext, usageEventsMap); Log.d(TAG, String.format("execute loadCurrentAppUsageList size=%d in %d/ms", appUsageEventList.size(), (System.currentTimeMillis() - startTime))); return appUsageEventList; } @Override protected void onPostExecute( final List currentAppUsageList) { if (currentAppUsageList == null || currentAppUsageList.isEmpty()) { Log.d(TAG, "currentAppUsageList is null or empty"); } else { mAppUsageEventList.addAll(currentAppUsageList); } mIsCurrentAppUsageLoaded = true; tryToProcessAppUsageData(); } }.execute(); } private void loadDatabaseAppUsageList() { new AsyncTask>() { @Override protected List doInBackground(Void... voids) { if (!shouldLoadAppUsageData()) { Log.d(TAG, "not loadDatabaseAppUsageList"); return null; } final long startTime = System.currentTimeMillis(); // Loads the app usage data from the database. final List appUsageEventList = DatabaseUtils.getAppUsageEventForUsers( mContext, Calendar.getInstance(), getCurrentUserIds(), mRawStartTimestamp); Log.d(TAG, String.format("execute loadDatabaseAppUsageList size=%d in %d/ms", appUsageEventList.size(), (System.currentTimeMillis() - startTime))); return appUsageEventList; } @Override protected void onPostExecute( final List databaseAppUsageList) { if (databaseAppUsageList == null || databaseAppUsageList.isEmpty()) { Log.d(TAG, "databaseAppUsageList is null or empty"); } else { mAppUsageEventList.addAll(databaseAppUsageList); } mIsDatabaseAppUsageLoaded = true; tryToProcessAppUsageData(); } }.execute(); } private void loadPowerConnectionBatteryEventList() { new AsyncTask>() { @Override protected List doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); // Loads the battery event data from the database. final List batteryEventList = DatabaseUtils.getBatteryEvents( mContext, Calendar.getInstance(), mRawStartTimestamp, POWER_CONNECTION_EVENTS); Log.d(TAG, String.format( "execute loadPowerConnectionBatteryEventList size=%d in %d/ms", batteryEventList.size(), (System.currentTimeMillis() - startTime))); return batteryEventList; } @Override protected void onPostExecute( final List batteryEventList) { if (batteryEventList == null || batteryEventList.isEmpty()) { Log.d(TAG, "batteryEventList is null or empty"); } else { mBatteryEventList.clear(); mBatteryEventList.addAll(batteryEventList); } mIsBatteryEventLoaded = true; tryToProcessAppUsageData(); } }.execute(); } private void loadBatteryUsageSlotList() { new AsyncTask>() { @Override protected List doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); // Loads the battery usage slot data from the database. final List batteryUsageSlotList = DatabaseUtils.getBatteryUsageSlots( mContext, Calendar.getInstance(), mLastFullChargeTimestamp); Log.d(TAG, String.format("execute loadBatteryUsageSlotList size=%d in %d/ms", batteryUsageSlotList.size(), (System.currentTimeMillis() - startTime))); return batteryUsageSlotList; } @Override protected void onPostExecute(final List batteryUsageSlotList) { if (batteryUsageSlotList == null || batteryUsageSlotList.isEmpty()) { Log.d(TAG, "batteryUsageSlotList is null or empty"); } else { mBatteryUsageSlotList.clear(); mBatteryUsageSlotList.addAll(batteryUsageSlotList); } mIsBatteryUsageSlotLoaded = true; tryToGenerateFinalDataAndApplyCallback(); } }.execute(); } private void loadAndApplyBatteryMapFromServiceOnly() { new AsyncTask>() { @Override protected Map doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); final Map batteryDiffDataMap = DataProcessor.getBatteryDiffDataMapFromStatsService( mContext, mRawStartTimestamp, getSystemAppsPackageNames(), getSystemAppsUids()); Log.d(TAG, String.format( "execute loadAndApplyBatteryMapFromServiceOnly size=%d in %d/ms", batteryDiffDataMap.size(), (System.currentTimeMillis() - startTime))); return batteryDiffDataMap; } @Override protected void onPostExecute(final Map batteryDiffDataMap) { // Post results back to main thread to refresh UI. if (mHandler != null && mCallbackFunction != null) { mHandler.post(() -> { mCallbackFunction.onBatteryDiffDataMapLoaded(batteryDiffDataMap); }); } } }.execute(); } private void tryToProcessAppUsageData() { // Ignore processing the data if any required data is not loaded. if (!mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded || !mIsBatteryEventLoaded) { return; } processAppUsageData(); tryToGenerateFinalDataAndApplyCallback(); } private void processAppUsageData() { // If there is no screen-on time data, no need to process. if (!mShowScreenOnTime) { return; } // Generates the indexed AppUsagePeriod list data for each corresponding time slot for // further use. mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap( mContext, mHourlyBatteryLevelsPerDay, mAppUsageEventList, mBatteryEventList); } private void tryToGenerateFinalDataAndApplyCallback() { // Ignore processing the data if any required data is not loaded. if (!mIsCurrentBatteryHistoryLoaded || !mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded || !mIsBatteryEventLoaded || !mIsBatteryUsageSlotLoaded) { return; } generateFinalDataAndApplyCallback(); } private synchronized void generateFinalDataAndApplyCallback() { new AsyncTask>() { @Override protected Map doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); final Map batteryDiffDataMap = new ArrayMap<>(); for (BatteryUsageSlot batteryUsageSlot : mBatteryUsageSlotList) { batteryDiffDataMap.put(batteryUsageSlot.getStartTimestamp(), ConvertUtils.convertToBatteryDiffData( mContext, batteryUsageSlot, getSystemAppsPackageNames(), getSystemAppsUids())); } batteryDiffDataMap.putAll(DataProcessor.getBatteryDiffDataMap(mContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap, mAppUsagePeriodMap, getSystemAppsPackageNames(), getSystemAppsUids())); Log.d(TAG, String.format( "execute generateFinalDataAndApplyCallback size=%d in %d/ms", batteryDiffDataMap.size(), System.currentTimeMillis() - startTime)); return batteryDiffDataMap; } @Override protected void onPostExecute(final Map batteryDiffDataMap) { // Post results back to main thread to refresh UI. if (mHandler != null && mCallbackFunction != null) { mHandler.post(() -> { mCallbackFunction.onBatteryDiffDataMapLoaded(batteryDiffDataMap); }); } } }.execute(); } // Whether we should load app usage data from service or database. private synchronized boolean shouldLoadAppUsageData() { if (!mShowScreenOnTime) { return false; } final int currentUserId = getCurrentUserId(); // If current user is locked, no need to load app usage data from service or database. if (mUserManager == null || !mUserManager.isUserUnlocked(currentUserId)) { Log.d(TAG, "shouldLoadAppUsageData: false, current user is locked"); mShowScreenOnTime = false; return false; } return true; } // Returns the list of current user id and work profile id if exists. private List getCurrentUserIds() { final List userIds = new ArrayList<>(); userIds.add(getCurrentUserId()); final int workProfileUserId = getWorkProfileUserId(); if (workProfileUserId != Integer.MIN_VALUE) { userIds.add(workProfileUserId); } return userIds; } private int getCurrentUserId() { return mContext.getUserId(); } private int getWorkProfileUserId() { final UserHandle userHandle = Utils.getManagedProfile(mUserManager); return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; } private synchronized Set getSystemAppsPackageNames() { if (mSystemAppsPackageNames == null) { mSystemAppsPackageNames = DataProcessor.getSystemAppsPackageNames(mContext); } return mSystemAppsPackageNames; } private synchronized Set getSystemAppsUids() { if (mSystemAppsUids == null) { mSystemAppsUids = DataProcessor.getSystemAppsUids(mContext); } return mSystemAppsUids; } /** * @return Returns battery level data and start async task to compute battery diff usage data * and load app labels + icons. * Returns null if the input is invalid or not having at least 2 hours data. */ @Nullable public static BatteryLevelData getBatteryLevelData( Context context, @Nullable Handler handler, final boolean isFromPeriodJob, final OnBatteryDiffDataMapLoadedListener onBatteryUsageMapLoadedListener) { final long start = System.currentTimeMillis(); final long lastFullChargeTime = DatabaseUtils.getLastFullChargeTime(context); final List batteryLevelRecordEvents = DatabaseUtils.getBatteryEvents( context, Calendar.getInstance(), lastFullChargeTime, BATTERY_LEVEL_RECORD_EVENTS); final long startTimestamp = batteryLevelRecordEvents.isEmpty() ? lastFullChargeTime : batteryLevelRecordEvents.get(0).getTimestamp(); final BatteryLevelData batteryLevelData = getPeriodBatteryLevelData(context, handler, startTimestamp, lastFullChargeTime, isFromPeriodJob, onBatteryUsageMapLoadedListener); Log.d(TAG, String.format("execute getBatteryLevelData in %d/ms," + " batteryLevelRecordEvents.size=%d", (System.currentTimeMillis() - start), batteryLevelRecordEvents.size())); return isFromPeriodJob ? batteryLevelData : BatteryLevelData.combine(batteryLevelData, batteryLevelRecordEvents); } private static BatteryLevelData getPeriodBatteryLevelData( Context context, @Nullable Handler handler, final long startTimestamp, final long lastFullChargeTime, final boolean isFromPeriodJob, final OnBatteryDiffDataMapLoadedListener onBatteryDiffDataMapLoadedListener) { final long currentTime = System.currentTimeMillis(); Log.d(TAG, String.format("getPeriodBatteryLevelData() startTimestamp=%s", ConvertUtils.utcToLocalTimeForLogging(startTimestamp))); if (isFromPeriodJob && startTimestamp >= TimestampUtils.getLastEvenHourTimestamp(currentTime)) { // Nothing needs to be loaded for period job. return null; } handler = handler != null ? handler : new Handler(Looper.getMainLooper()); final Map> batteryHistoryMap = sFakeBatteryHistoryMap != null ? sFakeBatteryHistoryMap : DatabaseUtils.getHistoryMapSinceLatestRecordBeforeQueryTimestamp(context, Calendar.getInstance(), startTimestamp, lastFullChargeTime); if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { Log.d(TAG, "batteryHistoryMap is null in getPeriodBatteryLevelData()"); new DataProcessManager(context, handler, onBatteryDiffDataMapLoadedListener).start(); return null; } // Process raw history map data into hourly timestamps. final Map> processedBatteryHistoryMap = DataProcessor.getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); // Wrap and processed history map into easy-to-use format for UI rendering. final BatteryLevelData batteryLevelData = DataProcessor.getLevelDataThroughProcessedHistoryMap( context, processedBatteryHistoryMap); if (batteryLevelData == null) { new DataProcessManager(context, handler, onBatteryDiffDataMapLoadedListener).start(); Log.d(TAG, "getBatteryLevelData() returns null"); return null; } // Start the async task to compute diff usage data and load labels and icons. new DataProcessManager( context, handler, startTimestamp, lastFullChargeTime, onBatteryDiffDataMapLoadedListener, batteryLevelData.getHourlyBatteryLevelsPerDay(), processedBatteryHistoryMap).start(isFromPeriodJob); return batteryLevelData; } }