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

Commit 97924455 authored by Kuan Wang's avatar Kuan Wang
Browse files

Implement the app usage data loading from database function.

Bug: 260964903
Test: make RunSettingsRoboTests + manual
Change-Id: I459dbdebe53e6b7421642955f36976b3e7c95fcb
parent 092d07fa
Loading
Loading
Loading
Loading
+46 −3
Original line number Diff line number Diff line
@@ -37,6 +37,9 @@ import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;

import java.time.Clock;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/** {@link ContentProvider} class to fetch battery usage data. */
public class BatteryUsageContentProvider extends ContentProvider {
@@ -100,6 +103,8 @@ public class BatteryUsageContentProvider extends ContentProvider {
        switch (sUriMatcher.match(uri)) {
            case BATTERY_STATE_CODE:
                return getBatteryStates(uri);
            case APP_USAGE_EVENT_CODE:
                return getAppUsageEvents(uri);
            case APP_USAGE_LATEST_TIMESTAMP_CODE:
                return getAppUsageLatestTimestamp(uri);
            default:
@@ -153,8 +158,7 @@ public class BatteryUsageContentProvider extends ContentProvider {
    }

    private Cursor getBatteryStates(Uri uri) {
        final long defaultTimestamp = mClock.millis() - QUERY_DURATION_HOURS.toMillis();
        final long queryTimestamp = getQueryTimestamp(uri, defaultTimestamp);
        final long queryTimestamp = getQueryTimestamp(uri);
        return getBatteryStates(uri, queryTimestamp);
    }

@@ -171,6 +175,24 @@ public class BatteryUsageContentProvider extends ContentProvider {
        return cursor;
    }

    private Cursor getAppUsageEvents(Uri uri) {
        final List<Long> queryUserIds = getQueryUserIds(uri);
        if (queryUserIds == null || queryUserIds.isEmpty()) {
            return null;
        }
        final long queryTimestamp = getQueryTimestamp(uri);
        final long timestamp = mClock.millis();
        Cursor cursor = null;
        try {
            cursor = mAppUsageEventDao.getAllForUsersAfter(queryUserIds, queryTimestamp);
        } catch (RuntimeException e) {
            Log.e(TAG, "query() from:" + uri + " error:" + e);
        }
        Log.w(TAG, "query app usage events in " + (mClock.millis() - timestamp) + "/ms");
        return cursor;

    }

    private Cursor getAppUsageLatestTimestamp(Uri uri) {
        final long queryUserId = getQueryUserId(uri);
        if (queryUserId == DatabaseUtils.INVALID_USER_ID) {
@@ -188,6 +210,26 @@ public class BatteryUsageContentProvider extends ContentProvider {
        return cursor;
    }

    // If URI contains query parameter QUERY_KEY_USERID, use the value directly.
    // Otherwise, return null.
    private List<Long> getQueryUserIds(Uri uri) {
        Log.d(TAG, "getQueryUserIds from uri: " + uri);
        final String value = uri.getQueryParameter(DatabaseUtils.QUERY_KEY_USERID);
        if (TextUtils.isEmpty(value)) {
            Log.w(TAG, "empty query value");
            return null;
        }
        try {
            return Arrays.asList(value.split(","))
                    .stream()
                    .map(s -> Long.parseLong(s.trim()))
                    .collect(Collectors.toList());
        } catch (NumberFormatException e) {
            Log.e(TAG, "invalid query value: " + value, e);
            return null;
        }
    }

    // If URI contains query parameter QUERY_KEY_USERID, use the value directly.
    // Otherwise, return INVALID_USER_ID.
    private long getQueryUserId(Uri uri) {
@@ -198,8 +240,9 @@ public class BatteryUsageContentProvider extends ContentProvider {

    // If URI contains query parameter QUERY_KEY_TIMESTAMP, use the value directly.
    // Otherwise, load the data for QUERY_DURATION_HOURS by default.
    private long getQueryTimestamp(Uri uri, long defaultTimestamp) {
    private long getQueryTimestamp(Uri uri) {
        Log.d(TAG, "getQueryTimestamp from uri: " + uri);
        final long defaultTimestamp = mClock.millis() - QUERY_DURATION_HOURS.toMillis();
        return getQueryValueFromUri(uri, DatabaseUtils.QUERY_KEY_TIMESTAMP, defaultTimestamp);
    }

+43 −0
Original line number Diff line number Diff line
@@ -209,6 +209,25 @@ public final class ConvertUtils {
        return appUsageEventBuilder.build();
    }

    /** Converts to {@link AppUsageEvent} from {@link Cursor} */
    public static AppUsageEvent convertToAppUsageEventFromCursor(final Cursor cursor) {
        final AppUsageEvent.Builder eventBuilder = AppUsageEvent.newBuilder();
        eventBuilder.setTimestamp(getLongFromCursor(cursor, AppUsageEventEntity.KEY_TIMESTAMP));
        eventBuilder.setType(
                AppUsageEventType.forNumber(
                        getIntegerFromCursor(
                                cursor, AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE)));
        eventBuilder.setPackageName(
                getStringFromCursor(cursor, AppUsageEventEntity.KEY_PACKAGE_NAME));
        eventBuilder.setInstanceId(
                getIntegerFromCursor(cursor, AppUsageEventEntity.KEY_INSTANCE_ID));
        eventBuilder.setTaskRootPackageName(
                getStringFromCursor(cursor, AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME));
        eventBuilder.setUserId(getLongFromCursor(cursor, AppUsageEventEntity.KEY_USER_ID));
        eventBuilder.setUid(getLongFromCursor(cursor, AppUsageEventEntity.KEY_UID));
        return eventBuilder.build();
    }

    /** Converts UTC timestamp to human readable local time string. */
    public static String utcToLocalTime(Context context, long timestamp) {
        final Locale locale = getLocale(context);
@@ -331,4 +350,28 @@ public final class ConvertUtils {

        return batteryInformationBuilder.build();
    }

    private static int getIntegerFromCursor(final Cursor cursor, final String key) {
        final int columnIndex = cursor.getColumnIndex(key);
        if (columnIndex >= 0) {
            return cursor.getInt(columnIndex);
        }
        return 0;
    }

    private static long getLongFromCursor(final Cursor cursor, final String key) {
        final int columnIndex = cursor.getColumnIndex(key);
        if (columnIndex >= 0) {
            return cursor.getLong(columnIndex);
        }
        return 0L;
    }

    private static String getStringFromCursor(final Cursor cursor, final String key) {
        final int columnIndex = cursor.getColumnIndex(key);
        if (columnIndex >= 0) {
            return cursor.getString(columnIndex);
        }
        return "";
    }
}
+97 −19
Original line number Diff line number Diff line
@@ -24,10 +24,14 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;

import androidx.annotation.NonNull;

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;
@@ -63,9 +67,15 @@ public class DataProcessManager {
    private final DataProcessor.UsageMapAsyncResponse mCallbackFunction;

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

    // The start timestamp of battery level data. As we don't know when is the full charge cycle
    // start time when loading app usage data, this value is used as the start time of querying app
    // usage data.
    private long mStartTimestampOfLevelData = 0;

    private boolean mIsCurrentBatteryHistoryLoaded = false;
    private boolean mIsCurrentAppUsageLoaded = false;
    private boolean mIsDatabaseAppUsageLoaded = false;
@@ -81,13 +91,15 @@ public class DataProcessManager {
            Context context,
            Handler handler,
            final DataProcessor.UsageMapAsyncResponse callbackFunction,
            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
            @NonNull final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
            @NonNull final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
        mContext = context.getApplicationContext();
        mHandler = handler;
        mUserManager = mContext.getSystemService(UserManager.class);
        mCallbackFunction = callbackFunction;
        mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
        mBatteryHistoryMap = batteryHistoryMap;
        mStartTimestampOfLevelData = getStartTimestampOfBatteryLevelData();
    }

    /**
@@ -102,6 +114,21 @@ public class DataProcessManager {
        loadCurrentAppUsageList();
    }

    @VisibleForTesting
    long getStartTimestampOfBatteryLevelData() {
        for (int dailyIndex = 0; dailyIndex < mHourlyBatteryLevelsPerDay.size(); dailyIndex++) {
            if (mHourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
                continue;
            }
            final List<Long> timestamps =
                    mHourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
            if (timestamps.size() > 0) {
                return timestamps.get(0);
            }
        }
        return 0;
    }

    @VisibleForTesting
    List<AppUsageEvent> getAppUsageEventList() {
        return mAppUsageEventList;
@@ -164,12 +191,17 @@ public class DataProcessManager {
        new AsyncTask<Void, Void, List<AppUsageEvent>>() {
            @Override
            protected List<AppUsageEvent> 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);
                        DataProcessor.getAppUsageEventsForUser(
                                mContext, currentUserId, mStartTimestampOfLevelData);
                // 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) {
@@ -180,7 +212,7 @@ public class DataProcessManager {
                if (workProfileUserId != Integer.MIN_VALUE) {
                    usageEventsForWorkProfile =
                            DataProcessor.getAppUsageEventsForUser(
                                    mContext, workProfileUserId);
                                    mContext, workProfileUserId, mStartTimestampOfLevelData);
                } else {
                    Log.d(TAG, "there is no work profile");
                }
@@ -203,16 +235,8 @@ public class DataProcessManager {
            @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");
                if (currentAppUsageList == null || currentAppUsageList.isEmpty()) {
                    Log.d(TAG, "currentAppUsageList is null or empty");
                } else {
                    mAppUsageEventList.addAll(currentAppUsageList);
                }
@@ -223,10 +247,37 @@ public class DataProcessManager {
    }

    private void loadDatabaseAppUsageList() {
        // TODO: load app usage data from database.
        new AsyncTask<Void, Void, List<AppUsageEvent>>() {
            @Override
            protected List<AppUsageEvent> doInBackground(Void... voids) {
                if (!shouldLoadAppUsageData()) {
                    Log.d(TAG, "not loadDatabaseAppUsageList");
                    return null;
                }
                final long startTime = System.currentTimeMillis();
                // Loads the current battery usage data from the battery stats service.
                final List<AppUsageEvent> appUsageEventList =
                        DatabaseUtils.getAppUsageEventForUsers(
                                mContext, Calendar.getInstance(), getCurrentUserIds(),
                                mStartTimestampOfLevelData);
                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<AppUsageEvent> databaseAppUsageList) {
                if (databaseAppUsageList == null || databaseAppUsageList.isEmpty()) {
                    Log.d(TAG, "databaseAppUsageList is null or empty");
                } else {
                    mAppUsageEventList.addAll(databaseAppUsageList);
                }
                mIsDatabaseAppUsageLoaded = true;
                tryToProcessAppUsageData();
            }
        }.execute();
    }

    private void tryToProcessAppUsageData() {
        // Only when all app usage events has been loaded, start processing app usage data to an
@@ -243,6 +294,8 @@ public class DataProcessManager {
        if (!mShowScreenOnTime) {
            return;
        }
        // Sort the appUsageEventList in ascending order based on the timestamp.
        Collections.sort(mAppUsageEventList, DataProcessor.TIMESTAMP_COMPARATOR);
        // TODO: process app usage data to an intermediate result for further use.
    }

@@ -262,14 +315,39 @@ public class DataProcessManager {
        // then apply the callback function.
    }

    // 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<Integer> getCurrentUserIds() {
        final List<Integer> 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(
                        mContext.getSystemService(UserManager.class));
                Utils.getManagedProfile(mUserManager);
        return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
    }
}
+8 −4
Original line number Diff line number Diff line
@@ -100,6 +100,8 @@ public final class DataProcessor {

    public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER =
            "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER";
    public static final Comparator<AppUsageEvent> TIMESTAMP_COMPARATOR =
            Comparator.comparing(AppUsageEvent::getTimestamp);

    /** A callback listener when battery usage loading async task is executed. */
    public interface UsageMapAsyncResponse {
@@ -228,7 +230,8 @@ public final class DataProcessor {
     * Gets the {@link UsageEvents} from system service for the specific user.
     */
    @Nullable
    public static UsageEvents getAppUsageEventsForUser(Context context, final int userID) {
    public static UsageEvents getAppUsageEventsForUser(
            Context context, final int userID, final long startTimestampOfLevelData) {
        final long start = System.currentTimeMillis();
        context = DatabaseUtils.getOwnerContext(context);
        if (context == null) {
@@ -240,8 +243,9 @@ public final class DataProcessor {
        }
        final long sixDaysAgoTimestamp =
                DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance());
        final long earliestTimestamp = Math.max(sixDaysAgoTimestamp, startTimestampOfLevelData);
        final UsageEvents events = getAppUsageEventsForUser(
                context, userManager, userID, sixDaysAgoTimestamp);
                context, userManager, userID, earliestTimestamp);
        final long elapsedTime = System.currentTimeMillis() - start;
        Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms",
                userID, elapsedTime));
@@ -638,7 +642,7 @@ public final class DataProcessor {
    @Nullable
    private static UsageEvents getAppUsageEventsForUser(
            Context context, final UserManager userManager, final int userID,
            final long sixDaysAgoTimestamp) {
            final long earliestTimestamp) {
        final String callingPackage = context.getPackageName();
        final long now = System.currentTimeMillis();
        // When the user is not unlocked, UsageStatsManager will return null, so bypass the
@@ -648,7 +652,7 @@ public final class DataProcessor {
            return null;
        }
        final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser(
                context, userID, sixDaysAgoTimestamp);
                context, userID, earliestTimestamp);
        return loadAppUsageEventsForUserFromService(
                sUsageStatsManager, startTime, now, userID, callingPackage);
    }
+61 −0
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/** A utility class to operate battery usage database. */
public final class DatabaseUtils {
@@ -93,6 +94,8 @@ public final class DatabaseUtils {
    @VisibleForTesting
    static Supplier<Cursor> sFakeBatteryStateSupplier;
    @VisibleForTesting
    static Supplier<Cursor> sFakeAppUsageEventSupplier;
    @VisibleForTesting
    static Supplier<Cursor> sFakeAppUsageLatestTimestampSupplier;

    private DatabaseUtils() {
@@ -125,6 +128,38 @@ public final class DatabaseUtils {
        return Math.max(latestTimestamp, earliestTimestamp);
    }

    /** Returns the current user data in app usage event table. */
    public static List<AppUsageEvent> getAppUsageEventForUsers(
            Context context,
            final Calendar calendar,
            final List<Integer> userIds,
            final long startTimestampOfLevelData) {
        final long startTime = System.currentTimeMillis();
        final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar);
        final long queryTimestamp = Math.max(startTimestampOfLevelData, sixDaysAgoTimestamp);
        Log.d(TAG, "sixDayAgoTimestamp: " + sixDaysAgoTimestamp);
        final String queryUserIdString = userIds.stream()
                .map(userId -> String.valueOf(userId))
                .collect(Collectors.joining(","));
        // Builds the content uri everytime to avoid cache.
        final Uri appUsageEventUri =
                new Uri.Builder()
                        .scheme(ContentResolver.SCHEME_CONTENT)
                        .authority(AUTHORITY)
                        .appendPath(APP_USAGE_EVENT_TABLE)
                        .appendQueryParameter(
                                QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
                        .appendQueryParameter(QUERY_KEY_USERID, queryUserIdString)
                        .build();

        final List<AppUsageEvent> appUsageEventList =
                loadAppUsageEventsFromContentProvider(context, appUsageEventUri);
        Log.d(TAG, String.format("getAppUsageEventForUser userId=%s size=%d in %d/ms",
                queryUserIdString, appUsageEventList.size(),
                (System.currentTimeMillis() - startTime)));
        return appUsageEventList;
    }

    /** Long: for timestamp and String: for BatteryHistEntry.getKey() */
    public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
            Context context, Calendar calendar) {
@@ -357,6 +392,32 @@ public final class DatabaseUtils {
        }
    }

    private static List<AppUsageEvent> loadAppUsageEventsFromContentProvider(
            Context context, Uri appUsageEventUri) {
        final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
        context = getOwnerContext(context);
        if (context == null) {
            return appUsageEventList;
        }
        try (Cursor cursor = sFakeAppUsageEventSupplier != null
                ? sFakeAppUsageEventSupplier.get()
                : context.getContentResolver().query(appUsageEventUri, null, null, null)) {
            if (cursor == null || cursor.getCount() == 0) {
                return appUsageEventList;
            }
            // Loads and recovers all AppUsageEvent data from cursor.
            while (cursor.moveToNext()) {
                appUsageEventList.add(ConvertUtils.convertToAppUsageEventFromCursor(cursor));
            }
            try {
                cursor.close();
            } catch (Exception e) {
                Log.e(TAG, "cursor.close() failed", e);
            }
        }
        return appUsageEventList;
    }

    private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
            Context context, Uri batteryStateUri) {
        context = DatabaseUtils.getOwnerContext(context);
Loading