/* * 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.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.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 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 final Handler mHandler; private final DataProcessor.UsageMapAsyncResponse mCallbackFunction; private final List mAppUsageEventList = new ArrayList<>(); private final List mBatteryEventList = new ArrayList<>(); private Context mContext; private UserManager mUserManager; private List mHourlyBatteryLevelsPerDay; private Map> mBatteryHistoryMap; // Raw start timestamp with round to the nearest hour. private long mRawStartTimestamp; private boolean mIsCurrentBatteryHistoryLoaded = false; private boolean mIsCurrentAppUsageLoaded = false; private boolean mIsDatabaseAppUsageLoaded = false; private boolean mIsBatteryEventLoaded = false; // Used to identify whether screen-on time data should be shown in the UI. private boolean mShowScreenOnTime = true; // Used to identify whether battery level data should be shown in the UI. private boolean mShowBatteryLevel = true; /** * 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; /** * Constructor when there exists battery level data. */ DataProcessManager( Context context, Handler handler, final long rawStartTimestamp, @NonNull final DataProcessor.UsageMapAsyncResponse callbackFunction, @NonNull final List hourlyBatteryLevelsPerDay, @NonNull final Map> batteryHistoryMap) { mContext = context.getApplicationContext(); mHandler = handler; mUserManager = mContext.getSystemService(UserManager.class); mCallbackFunction = callbackFunction; mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; mBatteryHistoryMap = batteryHistoryMap; mRawStartTimestamp = rawStartTimestamp; } /** * Constructor when there is no battery level data. */ DataProcessManager( Context context, Handler handler, @NonNull final DataProcessor.UsageMapAsyncResponse callbackFunction) { mContext = context.getApplicationContext(); mHandler = handler; mUserManager = mContext.getSystemService(UserManager.class); mCallbackFunction = callbackFunction; // When there is no battery level data, don't show screen-on time and battery level chart on // the UI. mShowScreenOnTime = false; mShowBatteryLevel = 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 (mShowBatteryLevel) { // Loads the latest battery history data from the service. loadCurrentBatteryHistoryMap(); // Loads app usage list from database. loadDatabaseAppUsageList(); // Loads the latest app usage list from the service. loadCurrentAppUsageList(); // Loads the battery event list from database. loadBatteryEventList(); } 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; } @VisibleForTesting boolean getShowBatteryLevel() { return mShowBatteryLevel; } 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 loadBatteryEventList() { 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); Log.d(TAG, String.format("execute loadBatteryEventList 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 loadAndApplyBatteryMapFromServiceOnly() { new AsyncTask() { @Override protected BatteryCallbackData doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); final Map> batteryUsageMap = DataProcessor.getBatteryUsageMapFromStatsService(mContext); DataProcessor.loadLabelAndIcon(batteryUsageMap); Log.d(TAG, String.format( "execute loadAndApplyBatteryMapFromServiceOnly size=%d in %d/ms", batteryUsageMap.size(), (System.currentTimeMillis() - startTime))); return new BatteryCallbackData(batteryUsageMap, /*deviceScreenOnTime=*/ null); } @Override protected void onPostExecute( final BatteryCallbackData batteryCallbackData) { // Set the unused variables to null. mContext = null; // Post results back to main thread to refresh UI. if (mHandler != null && mCallbackFunction != null) { mHandler.post(() -> { mCallbackFunction.onBatteryCallbackDataLoaded(batteryCallbackData); }); } } }.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(mRawStartTimestamp, mHourlyBatteryLevelsPerDay, mAppUsageEventList, mBatteryEventList); } private void tryToGenerateFinalDataAndApplyCallback() { // Ignore processing the data if any required data is not loaded. if (!mIsCurrentBatteryHistoryLoaded || !mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded || !mIsBatteryEventLoaded) { return; } generateFinalDataAndApplyCallback(); } private void generateFinalDataAndApplyCallback() { new AsyncTask() { @Override protected BatteryCallbackData doInBackground(Void... voids) { final long startTime = System.currentTimeMillis(); final Map> batteryUsageMap = DataProcessor.getBatteryUsageMap( mContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap, mAppUsagePeriodMap); final Map> deviceScreenOnTime = DataProcessor.getDeviceScreenOnTime(mAppUsagePeriodMap); DataProcessor.loadLabelAndIcon(batteryUsageMap); Log.d(TAG, String.format("execute generateFinalDataAndApplyCallback in %d/ms", (System.currentTimeMillis() - startTime))); return new BatteryCallbackData(batteryUsageMap, deviceScreenOnTime); } @Override protected void onPostExecute(final BatteryCallbackData batteryCallbackData) { // Set the unused variables to null. mContext = null; mHourlyBatteryLevelsPerDay = null; mBatteryHistoryMap = null; // Post results back to main thread to refresh UI. if (mHandler != null && mCallbackFunction != null) { mHandler.post(() -> { mCallbackFunction.onBatteryCallbackDataLoaded(batteryCallbackData); }); } } }.execute(); } // Whether we should load app usage data from service or database. private 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; } /** * @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, @Nullable final Map> batteryHistoryMap, final DataProcessor.UsageMapAsyncResponse asyncResponseDelegate) { if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { Log.d(TAG, "batteryHistoryMap is null in getBatteryLevelData()"); new DataProcessManager(context, handler, asyncResponseDelegate).start(); return null; } handler = handler != null ? handler : new Handler(Looper.getMainLooper()); // 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, asyncResponseDelegate).start(); Log.d(TAG, "getBatteryLevelData() returns null"); return null; } final long rawStartTimestamp = Collections.min(batteryHistoryMap.keySet()); // Start the async task to compute diff usage data and load labels and icons. new DataProcessManager( context, handler, rawStartTimestamp, asyncResponseDelegate, batteryLevelData.getHourlyBatteryLevelsPerDay(), processedBatteryHistoryMap).start(); return batteryLevelData; } }