1 /*
2  * Copyright (C) 2023 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.server.healthconnect.migration;
18 
19 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_ALLOWED;
20 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IN_PROGRESS;
21 
22 import static com.android.server.healthconnect.migration.MigrationConstants.COUNT_DEFAULT;
23 import static com.android.server.healthconnect.migration.MigrationConstants.EXTRA_USER_ID;
24 import static com.android.server.healthconnect.migration.MigrationConstants.INTERVAL_DEFAULT;
25 
26 import android.app.job.JobInfo;
27 import android.app.job.JobScheduler;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.health.connect.Constants;
31 import android.os.PersistableBundle;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
37 
38 import java.time.Duration;
39 import java.util.Objects;
40 import java.util.UUID;
41 
42 /**
43  * This class schedules the {@link MigrationBroadcastJobService} service.
44  *
45  * @hide
46  */
47 public final class MigrationBroadcastScheduler {
48 
49     private static final String TAG = "MigrationBroadcastScheduler";
50 
51     @VisibleForTesting
52     static final String MIGRATION_BROADCAST_NAMESPACE = "HEALTH_CONNECT_MIGRATION_BROADCAST";
53 
54     private final Object mLock = new Object();
55     private final HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager =
56             HealthConnectDeviceConfigManager.getInitialisedInstance();
57 
58     @GuardedBy("mLock")
59     private int mUserId;
60 
MigrationBroadcastScheduler(int userId)61     public MigrationBroadcastScheduler(int userId) {
62         mUserId = userId;
63     }
64 
65     /** Sets userId. Invoked when the user is switched. */
setUserId(int userId)66     public void setUserId(int userId) {
67         synchronized (mLock) {
68             mUserId = userId;
69         }
70     }
71 
72     /***
73      * Cancels all previously scheduled {@link MigrationBroadcastJobService} service jobs.
74      * Retrieves the requiredCount and requiredInterval corresponding to the given migration
75      * state.
76      * If the requiredInterval is greater than or equal to the minimum interval allowed for
77      * periodic jobs, a periodic job is scheduled, else a set of non-periodic jobs are
78      * pre-scheduled.
79      */
scheduleNewJobs(Context context)80     public void scheduleNewJobs(Context context) {
81         synchronized (mLock) {
82             int migrationState = MigrationStateManager.getInitialisedInstance().getMigrationState();
83 
84             if (Constants.DEBUG) {
85                 Slog.d(TAG, "Current migration state: " + migrationState);
86                 Slog.d(TAG, "Current user: " + mUserId);
87             }
88 
89             Objects.requireNonNull(context.getSystemService(JobScheduler.class))
90                     .forNamespace(MIGRATION_BROADCAST_NAMESPACE)
91                     .cancelAll();
92 
93             try {
94                 // When migration state is not in progress or allowed the count will be zero and no
95                 // job will be scheduled.
96                 if (getRequiredCount(migrationState) > 0) {
97                     createJobLocked(
98                             Math.max(
99                                     getRequiredInterval(migrationState),
100                                     JobInfo.getMinPeriodMillis()),
101                             context);
102                 }
103             } catch (Exception e) {
104                 Slog.e(TAG, "Exception while creating job : ", e);
105             }
106         }
107     }
108 
109     /***
110      * Creates a new {@link MigrationBroadcastJobService} job, to which it passes the user id in a
111      * PersistableBundle object.
112      *
113      * @param requiredInterval Time interval between each successive job for that current
114      *                         migration state
115      * @param context Context
116      *
117      * @throws Exception if migration broadcast job scheduling fails.
118      */
119     @GuardedBy("mLock")
createJobLocked(long requiredInterval, Context context)120     private void createJobLocked(long requiredInterval, Context context) throws Exception {
121         ComponentName schedulerServiceComponent =
122                 new ComponentName(context, MigrationBroadcastJobService.class);
123 
124         int uuid = UUID.randomUUID().toString().hashCode();
125         int jobId = String.valueOf(mUserId + uuid).hashCode();
126 
127         final PersistableBundle extras = new PersistableBundle();
128         extras.putInt(EXTRA_USER_ID, mUserId);
129 
130         JobInfo.Builder builder =
131                 new JobInfo.Builder(jobId, schedulerServiceComponent).setExtras(extras);
132 
133         builder.setPeriodic(requiredInterval);
134 
135         JobScheduler jobScheduler =
136                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
137                         .forNamespace(MIGRATION_BROADCAST_NAMESPACE);
138         int result = jobScheduler.schedule(builder.build());
139         if (result == JobScheduler.RESULT_SUCCESS) {
140             if (Constants.DEBUG) {
141                 Slog.d(TAG, "Successfully scheduled migration broadcast job");
142             }
143         } else {
144             throw new Exception("Failed to schedule migration broadcast job");
145         }
146     }
147 
148     /**
149      * Returns the number of migration broadcast jobs to be scheduled for the given migration state.
150      */
151     @VisibleForTesting
getRequiredCount(int migrationState)152     int getRequiredCount(int migrationState) {
153         switch (migrationState) {
154             case MIGRATION_STATE_IN_PROGRESS:
155                 return mHealthConnectDeviceConfigManager.getMigrationStateInProgressCount();
156             case MIGRATION_STATE_ALLOWED:
157                 return mHealthConnectDeviceConfigManager.getMigrationStateAllowedCount();
158             default:
159                 return COUNT_DEFAULT;
160         }
161     }
162 
163     /** Returns the interval between each migration broadcast job for the given migration state. */
164     @VisibleForTesting
getRequiredInterval(int migrationState)165     long getRequiredInterval(int migrationState) {
166         switch (migrationState) {
167             case MIGRATION_STATE_IN_PROGRESS:
168                 return calculateRequiredInterval(
169                         mHealthConnectDeviceConfigManager.getInProgressStateTimeoutPeriod(),
170                         getRequiredCount(MIGRATION_STATE_IN_PROGRESS));
171             case MIGRATION_STATE_ALLOWED:
172                 return calculateRequiredInterval(
173                         mHealthConnectDeviceConfigManager.getNonIdleStateTimeoutPeriod(),
174                         getRequiredCount(MIGRATION_STATE_ALLOWED));
175             default:
176                 return INTERVAL_DEFAULT;
177         }
178     }
179 
calculateRequiredInterval(Duration timeoutPeriod, int maxBroadcastCount)180     private static long calculateRequiredInterval(Duration timeoutPeriod, int maxBroadcastCount) {
181         return timeoutPeriod.toMillis() / maxBroadcastCount;
182     }
183 }
184