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

Commit d16201a3 authored by Kuan Wang's avatar Kuan Wang Committed by Android (Google) Code Review
Browse files

Merge "Copy BatterySettingsContentProvider from SettingsIntelligence to...

Merge "Copy BatterySettingsContentProvider from SettingsIntelligence to Settings and rename it to BatteryUsageContentProvider."
parents f41ae57f 0a0ba915
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -6,6 +6,11 @@

    <original-package android:name="com.android.settings"/>

    <!-- Permissions for reading or writing battery-related data. -->
    <permission
        android:name="com.android.settings.BATTERY_DATA"
        android:protectionLevel="signature|privileged"/>

    <uses-permission android:name="android.permission.REQUEST_NETWORK_SCORES" />
    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
@@ -2961,6 +2966,13 @@
                       android:value="@string/menu_key_battery"/>
        </activity>

        <provider
            android:name=".fuelgauge.batteryusage.BatteryUsageContentProvider"
            android:enabled="true"
            android:exported="true"
            android:authorities="${applicationId}.battery.usage.provider"
            android:permission="com.android.settings.BATTERY_DATA"/>

        <activity
            android:name="Settings$BatterySaverSettingsActivity"
            android:label="@string/battery_saver"
+168 −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.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;

import java.time.Clock;
import java.time.Duration;

/** {@link ContentProvider} class to fetch battery usage data. */
public class BatteryUsageContentProvider extends ContentProvider {
    private static final String TAG = "BatteryUsageContentProvider";

    // TODO: Updates the duration to a more reasonable value for since-last-full-charge.
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static final Duration QUERY_DURATION_HOURS = Duration.ofHours(28);

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static final String QUERY_KEY_TIMESTAMP = "timestamp";

    /** Codes */
    private static final int BATTERY_STATE_CODE = 1;
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(
                DatabaseUtils.AUTHORITY,
                /*path=*/ DatabaseUtils.BATTERY_STATE_TABLE,
                /*code=*/ BATTERY_STATE_CODE);
    }

    private Clock mClock;
    private BatteryStateDao mBatteryStateDao;

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public void setClock(Clock clock) {
        this.mClock = clock;
    }

    @Override
    public boolean onCreate() {
        if (DatabaseUtils.isWorkProfile(getContext())) {
            Log.w(TAG, "do not create provider for work profile");
            return false;
        }
        mClock = Clock.systemUTC();
        mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao();
        Log.w(TAG, "create content provider from " + getCallingPackage());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(
            @NonNull Uri uri,
            @Nullable String[] strings,
            @Nullable String s,
            @Nullable String[] strings1,
            @Nullable String s1) {
        switch (sUriMatcher.match(uri)) {
            case BATTERY_STATE_CODE:
                return getBatteryStates(uri);
            default:
                throw new IllegalArgumentException("unknown URI: " + uri);
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        switch (sUriMatcher.match(uri)) {
            case BATTERY_STATE_CODE:
                try {
                    mBatteryStateDao.insert(BatteryState.create(contentValues));
                    return uri;
                } catch (RuntimeException e) {
                    Log.e(TAG, "insert() from:" + uri + " error:" + e);
                    return null;
                }
            default:
                throw new IllegalArgumentException("unknown URI: " + uri);
        }
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        throw new UnsupportedOperationException("unsupported!");
    }

    @Override
    public int update(
            @NonNull Uri uri,
            @Nullable ContentValues contentValues,
            @Nullable String s,
            @Nullable String[] strings) {
        throw new UnsupportedOperationException("unsupported!");
    }

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

    private Cursor getBatteryStates(Uri uri, long firstTimestamp) {
        final long timestamp = mClock.millis();
        Cursor cursor = null;
        try {
            cursor = mBatteryStateDao.getCursorAfter(firstTimestamp);
        } catch (RuntimeException e) {
            Log.e(TAG, "query() from:" + uri + " error:" + e);
        }
        // TODO: Invokes hourly job recheck.
        Log.w(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms");
        return cursor;
    }

    // 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) {
        final String firstTimestampString = uri.getQueryParameter(QUERY_KEY_TIMESTAMP);
        if (TextUtils.isEmpty(firstTimestampString)) {
            Log.w(TAG, "empty query timestamp");
            return defaultTimestamp;
        }

        try {
            return Long.parseLong(firstTimestampString);
        } catch (NumberFormatException e) {
            Log.e(TAG, "invalid query timestamp: " + firstTimestampString, e);
            return defaultTimestamp;
        }
    }
}
+36 −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.content.Context;
import android.os.UserManager;

/** A utility class to operate battery usage database. */
public final class DatabaseUtils {
    private static final String TAG = "DatabaseUtils";

    /** An authority name of the battery content provider. */
    public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
    /** A table name for battery usage history. */
    public static final String BATTERY_STATE_TABLE = "BatteryState";

    /** Returns true if current user is a work profile user. */
    public static boolean isWorkProfile(Context context) {
        final UserManager userManager = context.getSystemService(UserManager.class);
        return userManager.isManagedProfile() && !userManager.isSystemUser();
    }
}
+311 −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 static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertThrows;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;

import androidx.test.core.app.ApplicationProvider;

import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settings.testutils.BatteryTestUtils;
import com.android.settings.testutils.FakeClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

import java.time.Duration;
import java.util.List;

/** Tests for {@link BatteryUsageContentProvider}. */
@RunWith(RobolectricTestRunner.class)
public final class BatteryUsageContentProviderTest {
    private static final Uri VALID_BATTERY_STATE_CONTENT_URI =
            new Uri.Builder()
                    .scheme(ContentResolver.SCHEME_CONTENT)
                    .authority(DatabaseUtils.AUTHORITY)
                    .appendPath(DatabaseUtils.BATTERY_STATE_TABLE)
                    .build();

    private Context mContext;
    private BatteryUsageContentProvider mProvider;

    @Before
    public void setUp() {
        mContext = ApplicationProvider.getApplicationContext();
        mProvider = new BatteryUsageContentProvider();
        mProvider.attachInfo(mContext, /*info=*/ null);
        BatteryTestUtils.setUpBatteryStateDatabase(mContext);
    }

    @Test
    public void onCreate_withoutWorkProfileMode_returnsTrue() {
        assertThat(mProvider.onCreate()).isTrue();
    }

    @Test
    public void onCreate_withWorkProfileMode_returnsFalse() {
        BatteryTestUtils.setWorkProfile(mContext);
        assertThat(mProvider.onCreate()).isFalse();
    }

    @Test
    public void queryAndInsert_incorrectContentUri_throwsIllegalArgumentException() {
        final Uri.Builder builder =
                new Uri.Builder()
                        .scheme(ContentResolver.SCHEME_CONTENT)
                        .authority(DatabaseUtils.AUTHORITY)
                        .appendPath(DatabaseUtils.BATTERY_STATE_TABLE + "/0");
        final Uri uri = builder.build();
        mProvider.onCreate();

        assertThrows(
                IllegalArgumentException.class,
                () ->
                        mProvider.query(
                                uri, /*strings=*/ null, /*s=*/ null, /*strings1=*/ null,
                                /*s1=*/ null));
        assertThrows(
                IllegalArgumentException.class,
                () -> mProvider.insert(uri, /*contentValues=*/ null));
    }

    @Test
    public void queryAndInsert_incorrectAuthority_throwsIllegalArgumentException() {
        final Uri.Builder builder =
                new Uri.Builder()
                        .scheme(ContentResolver.SCHEME_CONTENT)
                        .authority(DatabaseUtils.AUTHORITY + ".debug")
                        .appendPath(DatabaseUtils.BATTERY_STATE_TABLE);
        final Uri uri = builder.build();
        mProvider.onCreate();

        assertThrows(
                IllegalArgumentException.class,
                () ->
                        mProvider.query(
                                uri, /*strings=*/ null, /*s=*/ null, /*strings1=*/ null,
                                /*s1=*/ null));
        assertThrows(
                IllegalArgumentException.class,
                () -> mProvider.insert(uri, /*contentValues=*/ null));
    }

    @Test
    public void query_batteryState_returnsExpectedResult() throws Exception {
        mProvider.onCreate();
        final Duration currentTime = Duration.ofHours(52);
        final long expiredTimeCutoff = currentTime.toMillis()
                - BatteryUsageContentProvider.QUERY_DURATION_HOURS.toMillis();
        testQueryBatteryState(currentTime, expiredTimeCutoff, /*hasQueryTimestamp=*/ false);
    }

    @Test
    public void query_batteryStateTimestamp_returnsExpectedResult() throws Exception {
        mProvider.onCreate();
        final Duration currentTime = Duration.ofHours(52);
        final long expiredTimeCutoff = currentTime.toMillis() - Duration.ofHours(10).toMillis();
        testQueryBatteryState(currentTime, expiredTimeCutoff, /*hasQueryTimestamp=*/ true);
    }

    @Test
    public void query_incorrectParameterFormat_returnsExpectedResult() throws Exception {
        mProvider.onCreate();
        final Duration currentTime = Duration.ofHours(52);
        final long expiredTimeCutoff =
                currentTime.toMillis()
                        - BatteryUsageContentProvider.QUERY_DURATION_HOURS.toMillis();
        testQueryBatteryState(
                currentTime,
                expiredTimeCutoff,
                /*hasQueryTimestamp=*/ false,
                /*customParameter=*/ "invalid number format");
    }

    @Test
    public void insert_batteryState_returnsExpectedResult() {
        mProvider.onCreate();
        ContentValues values = new ContentValues();
        values.put("uid", Long.valueOf(101L));
        values.put("userId", Long.valueOf(1001L));
        values.put("appLabel", new String("Settings"));
        values.put("packageName", new String("com.android.settings"));
        values.put("timestamp", Long.valueOf(2100021L));
        values.put("isHidden", Boolean.valueOf(true));
        values.put("totalPower", Double.valueOf(99.0));
        values.put("consumePower", Double.valueOf(9.0));
        values.put("percentOfTotal", Double.valueOf(0.9));
        values.put("foregroundUsageTimeInMs", Long.valueOf(1000));
        values.put("backgroundUsageTimeInMs", Long.valueOf(2000));
        values.put("drainType", Integer.valueOf(1));
        values.put("consumerType", Integer.valueOf(2));
        values.put("batteryLevel", Integer.valueOf(51));
        values.put("batteryStatus", Integer.valueOf(2));
        values.put("batteryHealth", Integer.valueOf(3));

        final Uri uri = mProvider.insert(VALID_BATTERY_STATE_CONTENT_URI, values);

        assertThat(uri).isEqualTo(VALID_BATTERY_STATE_CONTENT_URI);
        // Verifies the BatteryState content.
        final List<BatteryState> states =
                BatteryStateDatabase.getInstance(mContext).batteryStateDao().getAllAfter(0);
        assertThat(states).hasSize(1);
        assertThat(states.get(0).uid).isEqualTo(101L);
        assertThat(states.get(0).userId).isEqualTo(1001L);
        assertThat(states.get(0).appLabel).isEqualTo("Settings");
        assertThat(states.get(0).packageName).isEqualTo("com.android.settings");
        assertThat(states.get(0).isHidden).isTrue();
        assertThat(states.get(0).timestamp).isEqualTo(2100021L);
        assertThat(states.get(0).totalPower).isEqualTo(99.0);
        assertThat(states.get(0).consumePower).isEqualTo(9.0);
        assertThat(states.get(0).percentOfTotal).isEqualTo(0.9);
        assertThat(states.get(0).foregroundUsageTimeInMs).isEqualTo(1000);
        assertThat(states.get(0).backgroundUsageTimeInMs).isEqualTo(2000);
        assertThat(states.get(0).drainType).isEqualTo(1);
        assertThat(states.get(0).consumerType).isEqualTo(2);
        assertThat(states.get(0).batteryLevel).isEqualTo(51);
        assertThat(states.get(0).batteryStatus).isEqualTo(2);
        assertThat(states.get(0).batteryHealth).isEqualTo(3);
    }

    @Test
    public void insert_partialFieldsContentValues_returnsExpectedResult() {
        mProvider.onCreate();
        final ContentValues values = new ContentValues();
        values.put("packageName", new String("fake_data"));
        values.put("timestamp", Long.valueOf(2100022L));
        values.put("batteryLevel", Integer.valueOf(52));
        values.put("batteryStatus", Integer.valueOf(3));
        values.put("batteryHealth", Integer.valueOf(2));

        final Uri uri = mProvider.insert(VALID_BATTERY_STATE_CONTENT_URI, values);

        assertThat(uri).isEqualTo(VALID_BATTERY_STATE_CONTENT_URI);
        // Verifies the BatteryState content.
        final List<BatteryState> states =
                BatteryStateDatabase.getInstance(mContext).batteryStateDao().getAllAfter(0);
        assertThat(states).hasSize(1);
        assertThat(states.get(0).packageName).isEqualTo("fake_data");
        assertThat(states.get(0).timestamp).isEqualTo(2100022L);
        assertThat(states.get(0).batteryLevel).isEqualTo(52);
        assertThat(states.get(0).batteryStatus).isEqualTo(3);
        assertThat(states.get(0).batteryHealth).isEqualTo(2);
    }

    @Test
    public void delete_throwsUnsupportedOperationException() {
        assertThrows(
                UnsupportedOperationException.class,
                () -> mProvider.delete(/*uri=*/ null, /*s=*/ null, /*strings=*/ null));
    }

    @Test
    public void update_throwsUnsupportedOperationException() {
        assertThrows(
                UnsupportedOperationException.class,
                () ->
                        mProvider.update(
                                /*uri=*/ null, /*contentValues=*/ null, /*s=*/ null,
                                /*strings=*/ null));
    }

    private void testQueryBatteryState(
            Duration currentTime, long expiredTimeCutoff, boolean hasQueryTimestamp)
            throws Exception {
        testQueryBatteryState(currentTime, expiredTimeCutoff, hasQueryTimestamp, null);
    }

    private void testQueryBatteryState(
            Duration currentTime,
            long expiredTimeCutoff,
            boolean hasQueryTimestamp,
            String customParameter)
            throws Exception {
        mProvider.onCreate();
        final FakeClock fakeClock = new FakeClock();
        fakeClock.setCurrentTime(currentTime);
        mProvider.setClock(fakeClock);
        // Inserts some expired testing data.
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, expiredTimeCutoff - 1, "com.android.sysui1");
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, expiredTimeCutoff - 2, "com.android.sysui2");
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, expiredTimeCutoff - 3, "com.android.sysui3");
        // Inserts some valid testing data.
        final String packageName1 = "com.android.settings1";
        final String packageName2 = "com.android.settings2";
        final String packageName3 = "com.android.settings3";
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, currentTime.toMillis(), packageName1);
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, expiredTimeCutoff + 2, packageName2);
        BatteryTestUtils.insertDataToBatteryStateDatabase(
                mContext, expiredTimeCutoff, packageName3);

        final Uri.Builder builder =
                new Uri.Builder()
                        .scheme(ContentResolver.SCHEME_CONTENT)
                        .authority(DatabaseUtils.AUTHORITY)
                        .appendPath(DatabaseUtils.BATTERY_STATE_TABLE);
        if (customParameter != null) {
            builder.appendQueryParameter(
                    BatteryUsageContentProvider.QUERY_KEY_TIMESTAMP, customParameter);
        } else if (hasQueryTimestamp) {
            builder.appendQueryParameter(
                    BatteryUsageContentProvider.QUERY_KEY_TIMESTAMP,
                    Long.toString(expiredTimeCutoff));
        }
        final Uri batteryStateQueryContentUri = builder.build();

        final Cursor cursor =
                mProvider.query(
                        batteryStateQueryContentUri,
                        /*strings=*/ null,
                        /*s=*/ null,
                        /*strings1=*/ null,
                        /*s1=*/ null);

        // Verifies the result not include expired data.
        assertThat(cursor.getCount()).isEqualTo(3);
        final int packageNameIndex = cursor.getColumnIndex("packageName");
        // Verifies the first data package name.
        cursor.moveToFirst();
        final String actualPackageName1 = cursor.getString(packageNameIndex);
        assertThat(actualPackageName1).isEqualTo(packageName1);
        // Verifies the second data package name.
        cursor.moveToNext();
        final String actualPackageName2 = cursor.getString(packageNameIndex);
        assertThat(actualPackageName2).isEqualTo(packageName2);
        // Verifies the third data package name.
        cursor.moveToNext();
        final String actualPackageName3 = cursor.getString(packageNameIndex);
        assertThat(actualPackageName3).isEqualTo(packageName3);
        cursor.close();
        // TODO: add verification for recheck broadcast.
    }
}
+54 −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.testutils;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;

/** A fake {@link Clock} class for testing. */
public final class FakeClock extends Clock {
    private long mCurrentTimeMillis;

    public FakeClock() {}

    /** Sets the time in millis for {@link Clock#millis()} method. */
    public void setCurrentTime(Duration duration) {
        mCurrentTimeMillis = duration.toMillis();
    }

    @Override
    public ZoneId getZone() {
        throw new UnsupportedOperationException("unsupported!");
    }

    @Override
    public Clock withZone(ZoneId zone) {
        throw new UnsupportedOperationException("unsupported!");
    }

    @Override
    public Instant instant() {
        throw new UnsupportedOperationException("unsupported!");
    }

    @Override
    public long millis() {
        return mCurrentTimeMillis;
    }
}