Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8d2a26ca authored by Kuan Wang's avatar Kuan Wang
Browse files

Add DataProcessManager to manage the async tasks of battery usage data

processing.

Test: make RunSettingsRoboTests + manually
Bug: 260964903
Change-Id: Id3b2772a98ec2ab3b03910c8a5e81adf7ccd5646
parent 16e6c4ab
Loading
Loading
Loading
Loading
+275 −0
Original line number Diff line number Diff line
/*
 * 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.UserHandle;
import android.os.UserManager;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.Utils;

import java.util.ArrayList;
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 3 async tasks in this manager:
 * <ul>
 *  <li>loadCurrentBatteryHistoryMap: load the latest battery history data from battery stats
 *  service.</li>
 *  <li>loadCurrentAppUsageList: load the latest app usage data (last timestamp in database - now)
 *  from usage stats service.</li>
 *  <li>loadDatabaseAppUsageList: load the necessary app usage data (after last full charge) from
 *  database</li>
 * </ul>
 *
 * The 3 async tasks will be started at the same time.
 * <ul>
 *  <li>After loadCurrentAppUsageList and loadDatabaseAppUsageList complete, which means all app
 *  usage data has been loaded, the intermediate usage result will be generated.</li>
 *  <li>Then after all 3 async tasks complete, the battery history data and app usage data will be
 *  combined to generate final data used for UI rendering. And the callback function will be
 *  applied.</li>
 *  <li>If current user is locked, which means we couldn't get the latest app usage data,
 *  screen-on time will not be shown in the UI and empty screen-on time data will be returned.</li>
 * </ul>
 */
public class DataProcessManager {
    private static final String TAG = "DataProcessManager";

    private final Handler mHandler;
    private final DataProcessor.UsageMapAsyncResponse mCallbackFunction;

    private Context mContext;
    private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
    private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;

    private boolean mIsCurrentBatteryHistoryLoaded = false;
    private boolean mIsCurrentAppUsageLoaded = false;
    private boolean mIsDatabaseAppUsageLoaded = false;
    // Used to identify whether screen-on time data should be shown in the UI.
    private boolean mShowScreenOnTime = true;

    private List<AppUsageEvent> mAppUsageEventList = new ArrayList<>();

    /**
     * Constructor when this exists battery level data.
     */
    DataProcessManager(
            Context context,
            Handler handler,
            final DataProcessor.UsageMapAsyncResponse callbackFunction,
            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
        mContext = context.getApplicationContext();
        mHandler = handler;
        mCallbackFunction = callbackFunction;
        mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
        mBatteryHistoryMap = batteryHistoryMap;
    }

    /**
     * Starts the async tasks to load battery history data and app usage data.
     */
    public void start() {
        // Load the latest battery history data from the service.
        loadCurrentBatteryHistoryMap();
        // Load app usage list from database.
        loadDatabaseAppUsageList();
        // Load the latest app usage list from the service.
        loadCurrentAppUsageList();
    }

    @VisibleForTesting
    List<AppUsageEvent> getAppUsageEventList() {
        return mAppUsageEventList;
    }

    @VisibleForTesting
    boolean getIsCurrentAppUsageLoaded() {
        return mIsCurrentAppUsageLoaded;
    }

    @VisibleForTesting
    boolean getIsDatabaseAppUsageLoaded() {
        return mIsDatabaseAppUsageLoaded;
    }

    @VisibleForTesting
    boolean getIsCurrentBatteryHistoryLoaded() {
        return mIsCurrentBatteryHistoryLoaded;
    }

    @VisibleForTesting
    boolean getShowScreenOnTime() {
        return mShowScreenOnTime;
    }

    private void loadCurrentBatteryHistoryMap() {
        new AsyncTask<Void, Void, Map<String, BatteryHistEntry>>() {
            @Override
            protected Map<String, BatteryHistEntry> doInBackground(Void... voids) {
                final long startTime = System.currentTimeMillis();
                // Loads the current battery usage data from the battery stats service.
                final Map<String, BatteryHistEntry> 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<String, BatteryHistEntry> currentBatteryHistoryMap) {
                if (mBatteryHistoryMap != null) {
                    // Replaces the placeholder in mBatteryHistoryMap.
                    for (Map.Entry<Long, Map<String, BatteryHistEntry>> 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<Void, Void, List<AppUsageEvent>>() {
            @Override
            protected List<AppUsageEvent> doInBackground(Void... voids) {
                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);
                // 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);
                } else {
                    Log.d(TAG, "there is no work profile");
                }

                final Map<Long, UsageEvents> 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<AppUsageEvent> 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<AppUsageEvent> currentAppUsageList) {
                final int currentUserId = getCurrentUserId();
                final UserManager userManager = mContext.getSystemService(UserManager.class);
                // If current user is locked, don't show screen-on time data in the UI.
                // Even if we have data in the database, we won't show screen-on time because we
                // don't have the latest data.
                if (userManager == null || !userManager.isUserUnlocked(currentUserId)) {
                    Log.d(TAG, "current user is locked");
                    mShowScreenOnTime = false;
                } else if (currentAppUsageList == null || currentAppUsageList.isEmpty()) {
                    Log.d(TAG, "usageEventsForWorkProfile is null or empty");
                } else {
                    mAppUsageEventList.addAll(currentAppUsageList);
                }
                mIsCurrentAppUsageLoaded = true;
                tryToProcessAppUsageData();
            }
        }.execute();
    }

    private void loadDatabaseAppUsageList() {
        // TODO: load app usage data from database.
        mIsDatabaseAppUsageLoaded = true;
        tryToProcessAppUsageData();
    }

    private void tryToProcessAppUsageData() {
        // Only when all app usage events has been loaded, start processing app usage data to an
        // intermediate result for further use.
        if (!mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded) {
            return;
        }
        processAppUsageData();
        tryToGenerateFinalDataAndApplyCallback();
    }

    private void processAppUsageData() {
        // If there is no screen-on time data, no need to process.
        if (!mShowScreenOnTime) {
            return;
        }
        // TODO: process app usage data to an intermediate result for further use.
    }

    private void tryToGenerateFinalDataAndApplyCallback() {
        // Only when both battery history data and app usage events data has been loaded, start the
        // final data processing.
        if (!mIsCurrentBatteryHistoryLoaded
                || !mIsCurrentAppUsageLoaded
                || !mIsDatabaseAppUsageLoaded) {
            return;
        }
        generateFinalDataAndApplyCallback();
    }

    private void generateFinalDataAndApplyCallback() {
        // TODO: generate the final data including battery usage map and device screen-on time and
        // then apply the callback function.
    }

    private int getCurrentUserId() {
        return mContext.getUserId();
    }

    private int getWorkProfileUserId() {
        final UserHandle userHandle =
                Utils.getManagedProfile(
                        mContext.getSystemService(UserManager.class));
        return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
    }
}
+60 −34
Original line number Diff line number Diff line
@@ -89,9 +89,6 @@ public final class DataProcessor {

    @VisibleForTesting
    static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL;
    @VisibleForTesting
    static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER =
            "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER";

    @VisibleForTesting
    static long sFakeCurrentTimeMillis = 0;
@@ -101,6 +98,9 @@ public final class DataProcessor {
            IUsageStatsManager.Stub.asInterface(
                    ServiceManager.getService(Context.USAGE_STATS_SERVICE));

    public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER =
            "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER";

    /** A callback listener when battery usage loading async task is executed. */
    public interface UsageMapAsyncResponse {
        /** The callback function when batteryUsageMap is loaded. */
@@ -200,19 +200,10 @@ public final class DataProcessor {
    @Nullable
    public static Map<Long, UsageEvents> getAppUsageEvents(Context context) {
        final long start = System.currentTimeMillis();
        final boolean isWorkProfileUser = DatabaseUtils.isWorkProfile(context);
        Log.d(TAG, "getAppUsageEvents() isWorkProfileUser:" + isWorkProfileUser);
        if (isWorkProfileUser) {
            try {
                context = context.createPackageContextAsUser(
                        /*packageName=*/ context.getPackageName(),
                        /*flags=*/ 0,
                        /*user=*/ UserHandle.OWNER);
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "context.createPackageContextAsUser() fail:" + e);
        context = DatabaseUtils.getOwnerContext(context);
        if (context == null) {
            return null;
        }
        }
        final Map<Long, UsageEvents> resultMap = new HashMap();
        final UserManager userManager = context.getSystemService(UserManager.class);
        if (userManager == null) {
@@ -220,19 +211,9 @@ public final class DataProcessor {
        }
        final long sixDaysAgoTimestamp =
                DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance());
        final String callingPackage = context.getPackageName();
        final long now = System.currentTimeMillis();
        for (final UserInfo user : userManager.getAliveUsers()) {
            // When the user is not unlocked, UsageStatsManager will return null, so bypass the
            // following data loading logics directly.
            if (!userManager.isUserUnlocked(user.id)) {
                Log.w(TAG, "fail to load app usage event for user :" + user.id + " because locked");
                continue;
            }
            final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser(
                    context, user.id, sixDaysAgoTimestamp);
            final UsageEvents events = getAppUsageEventsForUser(
                    sUsageStatsManager, startTime, now, user.id, callingPackage);
                    context, userManager, user.id, sixDaysAgoTimestamp);
            if (events != null) {
                resultMap.put(Long.valueOf(user.id), events);
            }
@@ -243,6 +224,30 @@ public final class DataProcessor {
        return resultMap.isEmpty() ? null : resultMap;
    }

    /**
     * Gets the {@link UsageEvents} from system service for the specific user.
     */
    @Nullable
    public static UsageEvents getAppUsageEventsForUser(Context context, final int userID) {
        final long start = System.currentTimeMillis();
        context = DatabaseUtils.getOwnerContext(context);
        if (context == null) {
            return null;
        }
        final UserManager userManager = context.getSystemService(UserManager.class);
        if (userManager == null) {
            return null;
        }
        final long sixDaysAgoTimestamp =
                DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance());
        final UsageEvents events = getAppUsageEventsForUser(
                context, userManager, userID, sixDaysAgoTimestamp);
        final long elapsedTime = System.currentTimeMillis() - start;
        Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms",
                userID, elapsedTime));
        return events;
    }

    /**
     * Closes the {@link BatteryUsageStats} after using it.
     */
@@ -335,6 +340,17 @@ public final class DataProcessor {
        return usageList;
    }

    /**
     * @return Returns the latest battery history map loaded from the battery stats service.
     */
    public static Map<String, BatteryHistEntry> getCurrentBatteryHistoryMapFromStatsService(
            final Context context) {
        final List<BatteryHistEntry> batteryHistEntryList =
                getBatteryHistListFromFromStatsService(context);
        return batteryHistEntryList == null ? new HashMap<>()
                : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e));
    }

    /**
     * @return Returns the processed history map which has interpolated to every hour data.
     * The start and end timestamp must be the even hours.
@@ -621,6 +637,24 @@ public final class DataProcessor {

    @Nullable
    private static UsageEvents getAppUsageEventsForUser(
            Context context, final UserManager userManager, final int userID,
            final long sixDaysAgoTimestamp) {
        final String callingPackage = context.getPackageName();
        final long now = System.currentTimeMillis();
        // When the user is not unlocked, UsageStatsManager will return null, so bypass the
        // following data loading logics directly.
        if (!userManager.isUserUnlocked(userID)) {
            Log.w(TAG, "fail to load app usage event for user :" + userID + " because locked");
            return null;
        }
        final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser(
                context, userID, sixDaysAgoTimestamp);
        return loadAppUsageEventsForUserFromService(
                sUsageStatsManager, startTime, now, userID, callingPackage);
    }

    @Nullable
    private static UsageEvents loadAppUsageEventsForUserFromService(
            final IUsageStatsManager usageStatsManager, final long startTime, final long endTime,
            final int userId, final String callingPackage) {
        final long start = System.currentTimeMillis();
@@ -672,14 +706,6 @@ public final class DataProcessor {
        return batteryHistEntryList;
    }

    private static Map<String, BatteryHistEntry> getCurrentBatteryHistoryMapFromStatsService(
            final Context context) {
        final List<BatteryHistEntry> batteryHistEntryList =
                getBatteryHistListFromFromStatsService(context);
        return batteryHistEntryList == null ? new HashMap<>()
                : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e));
    }

    @VisibleForTesting
    @Nullable
    static List<BatteryHistEntry> convertToBatteryHistEntry(
+20 −12
Original line number Diff line number Diff line
@@ -194,6 +194,23 @@ public final class DatabaseUtils {
        return startCalendar.getTimeInMillis();
    }

    /** Returns the context with OWNER identity when current user is work profile. */
    public static Context getOwnerContext(Context context) {
        final boolean isWorkProfileUser = isWorkProfile(context);
        if (isWorkProfileUser) {
            try {
                return context.createPackageContextAsUser(
                        /*packageName=*/ context.getPackageName(),
                        /*flags=*/ 0,
                        /*user=*/ UserHandle.OWNER);
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "context.createPackageContextAsUser() fail:" + e);
                return null;
            }
        }
        return context;
    }

    static List<ContentValues> sendAppUsageEventData(
            final Context context, final List<AppUsageEvent> appUsageEventList) {
        final long startTime = System.currentTimeMillis();
@@ -342,19 +359,10 @@ public final class DatabaseUtils {

    private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
            Context context, Uri batteryStateUri) {
        final boolean isWorkProfileUser = isWorkProfile(context);
        Log.d(TAG, "loadHistoryMapFromContentProvider() isWorkProfileUser:" + isWorkProfileUser);
        if (isWorkProfileUser) {
            try {
                context = context.createPackageContextAsUser(
                        /*packageName=*/ context.getPackageName(),
                        /*flags=*/ 0,
                        /*user=*/ UserHandle.OWNER);
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "context.createPackageContextAsUser() fail:" + e);
        context = DatabaseUtils.getOwnerContext(context);
        if (context == null) {
            return null;
        }
        }
        final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
        try (Cursor cursor = sFakeBatteryStateSupplier != null ? sFakeBatteryStateSupplier.get() :
                     context.getContentResolver().query(batteryStateUri, null, null, null)) {
+139 −0

File added.

Preview size limit exceeded, changes collapsed.

+37 −6
Original line number Diff line number Diff line
@@ -64,7 +64,7 @@ import java.util.Set;
import java.util.TimeZone;

@RunWith(RobolectricTestRunner.class)
public class DataProcessorTest {
public final class DataProcessorTest {
    private static final String FAKE_ENTRY_KEY = "fake_entry_key";

    private Context mContext;
@@ -177,7 +177,7 @@ public class DataProcessorTest {
    }

    @Test
    public void getAppUsageEvents_lockedUser_returnNull() throws RemoteException {
    public void getAppUsageEvents_lockedUser_returnNull() {
        UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
        final List<UserInfo> userInfoList = new ArrayList<>();
        userInfoList.add(userInfo);
@@ -205,6 +205,37 @@ public class DataProcessorTest {
        assertThat(resultMap).isNull();
    }

    @Test
    public void getAppUsageEventsForUser_returnExpectedResult() throws RemoteException {
        final int userId = 1;
        doReturn(true).when(mUserManager).isUserUnlocked(userId);
        doReturn(mUsageEvents1)
                .when(mUsageStatsManager)
                .queryEventsForUser(anyLong(), anyLong(), anyInt(), any());

        assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId))
                .isEqualTo(mUsageEvents1);
    }

    @Test
    public void getAppUsageEventsForUser_lockedUser_returnNull() {
        final int userId = 1;
        // Test locked user.
        doReturn(false).when(mUserManager).isUserUnlocked(userId);

        assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull();
    }

    @Test
    public void getAppUsageEventsForUser_nullUsageEvents_returnNull() throws RemoteException {
        final int userId = 1;
        doReturn(true).when(mUserManager).isUserUnlocked(userId);
        doReturn(null)
                .when(mUsageStatsManager).queryEventsForUser(anyLong(), anyLong(), anyInt(), any());

        assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull();
    }

    @Test public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() {
        Event event1 = getUsageEvent(Event.NOTIFICATION_INTERRUPTION, /*timestamp=*/ 1);
        Event event2 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 2);
@@ -231,11 +262,11 @@ public class DataProcessorTest {
                DataProcessor.generateAppUsageEventListFromUsageEvents(mContext, appUsageEvents);

        assertThat(appUsageEventList.size()).isEqualTo(3);
        assetAppUsageEvent(
        assertAppUsageEvent(
                appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2);
        assetAppUsageEvent(
        assertAppUsageEvent(
                appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 3);
        assetAppUsageEvent(
        assertAppUsageEvent(
                appUsageEventList.get(2), AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 4);
    }

@@ -1327,7 +1358,7 @@ public class DataProcessorTest {
        return event;
    }

    private void assetAppUsageEvent(
    private void assertAppUsageEvent(
            final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) {
        assertThat(event.getType()).isEqualTo(eventType);
        assertThat(event.getTimestamp()).isEqualTo(timestamp);