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.Duration;
32 
33 /** Manages the periodic job to schedule or cancel the next job. */
34 public final class PeriodicJobManager {
35     private static final String TAG = "PeriodicJobManager";
36     private static final int ALARM_MANAGER_REQUEST_CODE = TAG.hashCode();
37 
38     private static PeriodicJobManager sSingleton;
39 
40     private final Context mContext;
41     private final AlarmManager mAlarmManager;
42 
43     @VisibleForTesting static long sBroadcastDelayFromBoot = Duration.ofMinutes(40).toMillis();
44 
45     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
reset()46     void reset() {
47         sSingleton = null; // for testing only
48     }
49 
50     /** Gets or creates the new {@link PeriodicJobManager} instance. */
getInstance(Context context)51     public static synchronized PeriodicJobManager getInstance(Context context) {
52         if (sSingleton == null || sSingleton.mAlarmManager == null) {
53             sSingleton = new PeriodicJobManager(context);
54         }
55         return sSingleton;
56     }
57 
PeriodicJobManager(Context context)58     private PeriodicJobManager(Context context) {
59         this.mContext = context.getApplicationContext();
60         this.mAlarmManager = context.getSystemService(AlarmManager.class);
61     }
62 
63     /** Schedules the next alarm job if it is available. */
refreshJob(final boolean fromBoot)64     public void refreshJob(final boolean fromBoot) {
65         if (mAlarmManager == null) {
66             BatteryUsageLogUtils.writeLog(
67                     mContext,
68                     Action.SCHEDULE_JOB,
69                     "cannot schedule next alarm job due to AlarmManager is null");
70             Log.e(TAG, "cannot schedule next alarm job");
71             return;
72         }
73         // Cancels the previous alert job and schedules the next one.
74         final PendingIntent pendingIntent = getPendingIntent();
75         cancelJob(pendingIntent);
76         // Uses the timestamp of next full hour in local timezone.
77         long currentTimeMillis = System.currentTimeMillis();
78         final long triggerAtMillis = getTriggerAtMillis(currentTimeMillis, fromBoot);
79         mAlarmManager.setExactAndAllowWhileIdle(
80                 AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
81 
82         final String timeForLogging = ConvertUtils.utcToLocalTimeForLogging(triggerAtMillis);
83         BatteryUsageLogUtils.writeLog(
84                 mContext,
85                 Action.SCHEDULE_JOB,
86                 String.format("triggerTime=%s, fromBoot=%b", timeForLogging, fromBoot));
87         Log.d(TAG, "schedule next alarm job at " + timeForLogging);
88     }
89 
cancelJob(PendingIntent pendingIntent)90     private 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 time in milliseconds. */
99     @VisibleForTesting
getTriggerAtMillis(final long currentTimeMillis, final boolean fromBoot)100     static long getTriggerAtMillis(final long currentTimeMillis, final boolean fromBoot) {
101         final boolean delayHourlyJobWhenBooting =
102                 FeatureFactory.getFeatureFactory()
103                         .getPowerUsageFeatureProvider()
104                         .delayHourlyJobWhenBooting();
105         long targetTimeMillis = TimestampUtils.getNextHourTimestamp(currentTimeMillis);
106         if (delayHourlyJobWhenBooting
107                 && fromBoot
108                 && (targetTimeMillis - currentTimeMillis) <= sBroadcastDelayFromBoot) {
109             // Skips this time broadcast, schedule in the next alarm trigger.
110             targetTimeMillis = TimestampUtils.getNextHourTimestamp(targetTimeMillis);
111         }
112         return targetTimeMillis;
113     }
114 
getPendingIntent()115     private PendingIntent getPendingIntent() {
116         final Intent broadcastIntent =
117                 new Intent(mContext, PeriodicJobReceiver.class)
118                         .setAction(PeriodicJobReceiver.ACTION_PERIODIC_JOB_UPDATE);
119         return PendingIntent.getBroadcast(
120                 mContext.getApplicationContext(),
121                 ALARM_MANAGER_REQUEST_CODE,
122                 broadcastIntent,
123                 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
124     }
125 }
126