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

Commit a4f80100 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add DataProcessManager to manage the async tasks of battery usage data processing."

parents da69b259 8d2a26ca
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);