/* * 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.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.overlay.FeatureFactory; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; 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); // 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 boolean mIsFromPeriodJob; private final Context mContext; private final Handler mHandler; private final UserIdsSeries mUserIdsSeries; 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 UserIdsSeries userIdsSeries, final boolean isFromPeriodJob, final long rawStartTimestamp, final long lastFullChargeTimestamp, @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction, @NonNull final List hourlyBatteryLevelsPerDay, @NonNull final Map> batteryHistoryMap) { mContext = context.getApplicationContext(); mHandler = handler; mUserIdsSeries = userIdsSeries; mIsFromPeriodJob = isFromPeriodJob; mRawStartTimestamp = rawStartTimestamp; mLastFullChargeTimestamp = lastFullChargeTimestamp; mCallbackFunction = callbackFunction; mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; mBatteryHistoryMap = batteryHistoryMap; } /** Constructor when there is no battery level data. */ DataProcessManager( Context context, Handler handler, final UserIdsSeries userIdsSeries, @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction) { mContext = context.getApplicationContext(); mHandler = handler; mUserIdsSeries = userIdsSeries; mCallbackFunction = callbackFunction; mIsFromPeriodJob = false; 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() { // If we have battery level data, load the battery history map and app usage simultaneously. if (mHourlyBatteryLevelsPerDay != null) { if (mIsFromPeriodJob) { 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. if (mUserIdsSeries.isMainUserProfileOnly()) { loadBatteryUsageSlotList(); } else { mIsBatteryUsageSlotLoaded = true; } } // 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 @Nullable 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 Map usageEventsMap = new ArrayMap<>(); for (int userId : mUserIdsSeries.getVisibleUserIds()) { final UsageEvents usageEventsForCurrentUser = DataProcessor.getCurrentAppUsageEventsForUser( mContext, mUserIdsSeries, userId, mRawStartTimestamp); if (usageEventsForCurrentUser == null) { // If fail to load usage events for any user, return null directly and // screen-on time will not be shown in the UI. if (userId == mUserIdsSeries.getCurrentUserId()) { return null; } } else { usageEventsMap.put(Long.valueOf(userId), usageEventsForCurrentUser); } } 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(), mUserIdsSeries.getVisibleUserIds(), 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, mUserIdsSeries, 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, mUserIdsSeries, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap, mAppUsagePeriodMap, getSystemAppsPackageNames(), getSystemAppsUids())); // Process the reattributate data for the following two cases: // 1) the latest slot for the timestamp "until now" // 2) walkthrough all BatteryDiffData again to handle "re-compute" case final PowerUsageFeatureProvider featureProvider = FeatureFactory.getFeatureFactory() .getPowerUsageFeatureProvider(); featureProvider.processBatteryReattributeData( mContext, batteryDiffDataMap, mBatteryEventList, mIsFromPeriodJob); 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; } // If current user is locked, no need to load app usage data from service or database. if (mUserIdsSeries.isCurrentUserLocked()) { Log.d(TAG, "shouldLoadAppUsageData: false, current user is locked"); mShowScreenOnTime = false; return false; } return true; } 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 UserIdsSeries userIdsSeries, 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, DatabaseUtils.BATTERY_LEVEL_RECORD_EVENTS); final long startTimestamp = (batteryLevelRecordEvents.isEmpty() || (!isFromPeriodJob && !userIdsSeries.isMainUserProfileOnly())) ? lastFullChargeTime : batteryLevelRecordEvents.get(0).getTimestamp(); final BatteryLevelData batteryLevelData = getPeriodBatteryLevelData( context, handler, userIdsSeries, 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 UserIdsSeries userIdsSeries, 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, userIdsSeries, onBatteryDiffDataMapLoadedListener) .start(); return null; } // Process raw history map data into hourly timestamps. final Map> processedBatteryHistoryMap = DataProcessor.getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); if (isFromPeriodJob && !processedBatteryHistoryMap.isEmpty()) { // For periodic job, only generate battery usage data between even-hour timestamps. // Remove the timestamps: // 1) earlier than the latest completed period job (startTimestamp) // 2) later than current scheduled even-hour job (lastEvenHourTimestamp). final long lastEvenHourTimestamp = TimestampUtils.getLastEvenHourTimestamp(currentTime); final Set batteryHistMapKeySet = processedBatteryHistoryMap.keySet(); final long minTimestamp = Collections.min(batteryHistMapKeySet); final long maxTimestamp = Collections.max(batteryHistMapKeySet); if (minTimestamp < startTimestamp) { processedBatteryHistoryMap.remove(minTimestamp); } if (maxTimestamp > lastEvenHourTimestamp) { processedBatteryHistoryMap.remove(maxTimestamp); } } // 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, userIdsSeries, 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, userIdsSeries, isFromPeriodJob, startTimestamp, lastFullChargeTime, onBatteryDiffDataMapLoadedListener, batteryLevelData.getHourlyBatteryLevelsPerDay(), processedBatteryHistoryMap) .start(); return batteryLevelData; } }