1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.fuelgauge.batteryusage; 18 19 import android.app.AlarmManager; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.util.Log; 24 25 import androidx.annotation.VisibleForTesting; 26 27 import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action; 28 import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils; 29 import com.android.settings.overlay.FeatureFactory; 30 31 import java.time.Clock; 32 import java.time.Duration; 33 34 /** Manages the periodic job to schedule or cancel the next job. */ 35 public final class PeriodicJobManager { 36 private static final String TAG = "PeriodicJobManager"; 37 private static final int ALARM_MANAGER_REQUEST_CODE = TAG.hashCode(); 38 39 private static PeriodicJobManager sSingleton; 40 41 private final Context mContext; 42 private final AlarmManager mAlarmManager; 43 44 @VisibleForTesting 45 static final int DATA_FETCH_INTERVAL_MINUTE = 60; 46 47 @VisibleForTesting 48 static long sBroadcastDelayFromBoot = Duration.ofMinutes(40).toMillis(); 49 50 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) reset()51 void reset() { 52 sSingleton = null; // for testing only 53 } 54 55 /** Gets or creates the new {@link PeriodicJobManager} instance. */ getInstance(Context context)56 public static synchronized PeriodicJobManager getInstance(Context context) { 57 if (sSingleton == null || sSingleton.mAlarmManager == null) { 58 sSingleton = new PeriodicJobManager(context); 59 } 60 return sSingleton; 61 } 62 PeriodicJobManager(Context context)63 private PeriodicJobManager(Context context) { 64 this.mContext = context.getApplicationContext(); 65 this.mAlarmManager = context.getSystemService(AlarmManager.class); 66 } 67 68 /** Schedules the next alarm job if it is available. */ refreshJob(final boolean fromBoot)69 public void refreshJob(final boolean fromBoot) { 70 if (mAlarmManager == null) { 71 BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB, 72 "cannot schedule next alarm job due to AlarmManager is null"); 73 Log.e(TAG, "cannot schedule next alarm job"); 74 return; 75 } 76 // Cancels the previous alert job and schedules the next one. 77 final PendingIntent pendingIntent = getPendingIntent(); 78 cancelJob(pendingIntent); 79 // Uses UTC time to avoid scheduler is impacted by different timezone. 80 final long triggerAtMillis = getTriggerAtMillis(mContext, Clock.systemUTC(), fromBoot); 81 mAlarmManager.setExactAndAllowWhileIdle( 82 AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); 83 84 final String utcToLocalTime = ConvertUtils.utcToLocalTimeForLogging(triggerAtMillis); 85 BatteryUsageLogUtils.writeLog(mContext, Action.SCHEDULE_JOB, 86 String.format("triggerTime=%s, fromBoot=%b", utcToLocalTime, fromBoot)); 87 Log.d(TAG, "schedule next alarm job at " + utcToLocalTime); 88 } 89 cancelJob(PendingIntent pendingIntent)90 void cancelJob(PendingIntent pendingIntent) { 91 if (mAlarmManager != null) { 92 mAlarmManager.cancel(pendingIntent); 93 } else { 94 Log.e(TAG, "cannot cancel the alarm job"); 95 } 96 } 97 98 /** Gets the next alarm trigger UTC time in milliseconds. */ getTriggerAtMillis(Context context, Clock clock, final boolean fromBoot)99 static long getTriggerAtMillis(Context context, Clock clock, final boolean fromBoot) { 100 long currentTimeMillis = clock.millis(); 101 final boolean delayHourlyJobWhenBooting = 102 FeatureFactory.getFactory(context) 103 .getPowerUsageFeatureProvider(context) 104 .delayHourlyJobWhenBooting(); 105 // Rounds to the previous nearest time slot and shifts to the next one. 106 long timeSlotUnit = Duration.ofMinutes(DATA_FETCH_INTERVAL_MINUTE).toMillis(); 107 long targetTime = (currentTimeMillis / timeSlotUnit) * timeSlotUnit + timeSlotUnit; 108 if (delayHourlyJobWhenBooting 109 && fromBoot 110 && (targetTime - currentTimeMillis) <= sBroadcastDelayFromBoot) { 111 targetTime += timeSlotUnit; 112 } 113 return targetTime; 114 } 115 getPendingIntent()116 private PendingIntent getPendingIntent() { 117 final Intent broadcastIntent = 118 new Intent(mContext, PeriodicJobReceiver.class) 119 .setAction(PeriodicJobReceiver.ACTION_PERIODIC_JOB_UPDATE); 120 return PendingIntent.getBroadcast( 121 mContext.getApplicationContext(), 122 ALARM_MANAGER_REQUEST_CODE, 123 broadcastIntent, 124 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 125 } 126 } 127