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

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

Add hourly period job to fetch battery usage data in Settings.

Bug: 253395332
Test: make RunSettingsRoboTests + manually
Change-Id: I342066a30fed202e5013b8c2554f36d991975c3e
parent 9040efdc
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -2983,6 +2983,26 @@
            </intent-filter>
        </receiver>

        <receiver
            android:name=".fuelgauge.batteryusage.BootBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
                <action android:name="android.intent.action.MY_PACKAGE_UNSUSPENDED"/>
                <action android:name="com.google.android.setupwizard.SETUP_WIZARD_FINISHED"/>
                <action android:name="com.android.settings.battery.action.PERIODIC_JOB_RECHECK"/>
            </intent-filter>
        </receiver>

        <receiver
            android:name=".fuelgauge.batteryusage.PeriodicJobReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="com.android.settings.battery.action.PERIODIC_JOB_UPDATE"/>
            </intent-filter>
        </receiver>

        <activity
            android:name="Settings$BatterySaverSettingsActivity"
            android:label="@string/battery_saver"
+2 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;

@@ -144,7 +145,7 @@ public class BatteryUsageContentProvider extends ContentProvider {
        } catch (RuntimeException e) {
            Log.e(TAG, "query() from:" + uri + " error:" + e);
        }
        // TODO: Invokes hourly job recheck.
        AsyncTask.execute(() -> BootBroadcastReceiver.invokeJobRecheck(getContext()));
        Log.w(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms");
        return cursor;
    }
+88 −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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.time.Duration;

/** Receives broadcasts to start or stop the periodic fetching job. */
public final class BootBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "BootBroadcastReceiver";
    private static final long RESCHEDULE_FOR_BOOT_ACTION = Duration.ofSeconds(6).toMillis();

    private final Handler mHandler = new Handler(Looper.getMainLooper());

    public static final String ACTION_PERIODIC_JOB_RECHECK =
            "com.android.settings.battery.action.PERIODIC_JOB_RECHECK";
    public static final String ACTION_SETUP_WIZARD_FINISHED =
            "com.google.android.setupwizard.SETUP_WIZARD_FINISHED";

    /** Invokes periodic job rechecking process. */
    public static void invokeJobRecheck(Context context) {
        context = context.getApplicationContext();
        final Intent intent = new Intent(ACTION_PERIODIC_JOB_RECHECK);
        intent.setClass(context, BootBroadcastReceiver.class);
        context.sendBroadcast(intent);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent == null ? "" : intent.getAction();
        if (DatabaseUtils.isWorkProfile(context)) {
            Log.w(TAG, "do not start job for work profile action=" + action);
            return;
        }

        switch (action) {
            case Intent.ACTION_BOOT_COMPLETED:
            case Intent.ACTION_MY_PACKAGE_REPLACED:
            case Intent.ACTION_MY_PACKAGE_UNSUSPENDED:
            case ACTION_SETUP_WIZARD_FINISHED:
            case ACTION_PERIODIC_JOB_RECHECK:
                Log.d(TAG, "refresh periodic job from action=" + action);
                refreshJobs(context);
                break;
            case Intent.ACTION_TIME_CHANGED:
                Log.d(TAG, "refresh job and clear all data from action=" + action);
                DatabaseUtils.clearAll(context);
                PeriodicJobManager.getInstance(context).refreshJob();
                break;
            default:
                Log.w(TAG, "receive unsupported action=" + action);
        }

        // Waits a while to recheck the scheduler to avoid AlarmManager is not ready.
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            final Intent recheckIntent = new Intent(ACTION_PERIODIC_JOB_RECHECK);
            recheckIntent.setClass(context, BootBroadcastReceiver.class);
            mHandler.postDelayed(() -> context.sendBroadcast(recheckIntent),
                    RESCHEDULE_FOR_BOOT_ACTION);
        }
    }

    private static void refreshJobs(Context context) {
        // Clears useless data from battery usage database if needed.
        DatabaseUtils.clearExpiredDataIfNeeded(context);
        PeriodicJobManager.getInstance(context).refreshJob();
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -57,7 +57,9 @@ public final class DatabaseUtils {
    /** Clear memory threshold for device booting phase. **/
    private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
    private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
    private static final long DATA_RETENTION_INTERVAL_MS = Duration.ofDays(9).toMillis();

    @VisibleForTesting
    static final int DATA_RETENTION_INTERVAL_DAY = 9;

    /** An authority name of the battery content provider. */
    public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
@@ -65,8 +67,6 @@ public final class DatabaseUtils {
    public static final String BATTERY_STATE_TABLE = "BatteryState";
    /** A class name for battery usage data provider. */
    public static final String SETTINGS_PACKAGE_PATH = "com.android.settings";
    public static final String BATTERY_PROVIDER_CLASS_PATH =
            "com.android.settings.fuelgauge.batteryusage.BatteryUsageContentProvider";

    /** A content URI to access battery usage states data. */
    public static final Uri BATTERY_CONTENT_URI =
@@ -133,7 +133,8 @@ public final class DatabaseUtils {
                BatteryStateDatabase
                        .getInstance(context.getApplicationContext())
                        .batteryStateDao()
                        .clearAllBefore(Clock.systemUTC().millis() - DATA_RETENTION_INTERVAL_MS);
                        .clearAllBefore(Clock.systemUTC().millis()
                                - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis());
            } catch (RuntimeException e) {
                Log.e(TAG, "clearAllBefore() failed", e);
            }
+123 −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.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import java.text.SimpleDateFormat;
import java.time.Clock;
import java.time.Duration;
import java.util.Date;
import java.util.Locale;

/** Manages the periodic job to schedule or cancel the next job. */
public final class PeriodicJobManager {
    private static final String TAG = "PeriodicJobManager";
    private static final int ALARM_MANAGER_REQUEST_CODE = TAG.hashCode();

    private static PeriodicJobManager sSingleton;

    private final Context mContext;
    private final AlarmManager mAlarmManager;
    private final SimpleDateFormat mSimpleDateFormat =
            new SimpleDateFormat("MMM dd,yyyy HH:mm:ss", Locale.ENGLISH);

    @VisibleForTesting
    static final int DATA_FETCH_INTERVAL_MINUTE = 60;

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void reset() {
        sSingleton = null; // for testing only
    }

    /** Gets or creates the new {@link PeriodicJobManager} instance. */
    public static synchronized PeriodicJobManager getInstance(Context context) {
        if (sSingleton == null || sSingleton.mAlarmManager == null) {
            sSingleton = new PeriodicJobManager(context);
        }
        return sSingleton;
    }

    private PeriodicJobManager(Context context) {
        this.mContext = context.getApplicationContext();
        this.mAlarmManager = context.getSystemService(AlarmManager.class);
    }

    /** Schedules the next alarm job if it is available. */
    @SuppressWarnings("JavaUtilDate")
    public void refreshJob() {
        if (mAlarmManager == null) {
            Log.e(TAG, "cannot schedule next alarm job");
            return;
        }
        // Cancels the previous alert job and schedules the next one.
        final PendingIntent pendingIntent = getPendingIntent();
        cancelJob(pendingIntent);
        if (!canScheduleExactAlarms()) {
            Log.w(TAG, "cannot schedule exact alarm job");
            return;
        }
        // Uses UTC time to avoid scheduler is impacted by different timezone.
        final long triggerAtMillis = getTriggerAtMillis(Clock.systemUTC());
        mAlarmManager.setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
        Log.d(TAG, "schedule next alarm job at "
                + mSimpleDateFormat.format(new Date(triggerAtMillis)));
    }

    void cancelJob(PendingIntent pendingIntent) {
        if (mAlarmManager != null) {
            mAlarmManager.cancel(pendingIntent);
        } else {
            Log.e(TAG, "cannot cancel the alarm job");
        }
    }

    /** Gets the next alarm trigger UTC time in milliseconds. */
    static long getTriggerAtMillis(Clock clock) {
        long currentTimeMillis = clock.millis();
        // Rounds to the previous nearest time slot and shifts to the next one.
        long timeSlotUnit = Duration.ofMinutes(DATA_FETCH_INTERVAL_MINUTE).toMillis();
        return (currentTimeMillis / timeSlotUnit) * timeSlotUnit + timeSlotUnit;
    }

    private PendingIntent getPendingIntent() {
        final Intent broadcastIntent =
                new Intent(mContext, PeriodicJobReceiver.class)
                        .setAction(PeriodicJobReceiver.ACTION_PERIODIC_JOB_UPDATE);
        return PendingIntent.getBroadcast(
                mContext.getApplicationContext(),
                ALARM_MANAGER_REQUEST_CODE,
                broadcastIntent,
                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    }

    private boolean canScheduleExactAlarms() {
        return canScheduleExactAlarms(mAlarmManager);
    }

    /** Whether we can schedule exact alarm or not? */
    public static boolean canScheduleExactAlarms(AlarmManager alarmManager) {
        return alarmManager.canScheduleExactAlarms();
    }
}
Loading