1 /*
2  * Copyright (C) 2021 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.imsserviceentitlement.job;
18 
19 import android.app.job.JobInfo;
20 import android.app.job.JobParameters;
21 import android.app.job.JobScheduler;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.os.PersistableBundle;
25 import android.telephony.SubscriptionManager;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 
29 import androidx.annotation.GuardedBy;
30 
31 import com.android.imsserviceentitlement.utils.TelephonyUtils;
32 
33 import java.time.Duration;
34 
35 /** Manages all scheduled jobs and provides common job scheduler. */
36 public class JobManager {
37     private static final String TAG = "IMSSE-JobManager";
38 
39     private static final int JOB_ID_BASE_INDEX = 1000;
40 
41     // Query entitlement status
42     public static final int QUERY_ENTITLEMENT_STATUS_JOB_ID = 1;
43     // Register FCM to listen push notification, this job not associated with subscription id.
44     public static final int REGISTER_FCM_JOB_ID = 2;
45 
46     public static final String EXTRA_SLOT_ID = "SLOT_ID";
47     public static final String EXTRA_RETRY_COUNT = "RETRY_COUNT";
48 
49     private final Context mContext;
50     private final int mSubId;
51     private final JobScheduler mJobScheduler;
52     private final ComponentName mComponentName;
53 
54     // Cache subscription id associated {@link JobManager} objects for reusing.
55     @GuardedBy("JobManager.class")
56     private static final ArrayMap<String, JobManager> sInstances = new ArrayMap<>();
57 
JobManager(Context context, ComponentName componentName, int subId)58     private JobManager(Context context, ComponentName componentName, int subId) {
59         this.mContext = context;
60         this.mComponentName = componentName;
61         this.mJobScheduler = context.getSystemService(JobScheduler.class);
62         this.mSubId = subId;
63     }
64 
65     /** Returns {@link JobManager} instance. */
getInstance( Context context, ComponentName componentName, int subId)66     public static synchronized JobManager getInstance(
67             Context context, ComponentName componentName, int subId) {
68         String key = componentName.flattenToShortString() + "." + subId;
69         JobManager instance = sInstances.get(key);
70         if (instance != null) {
71             return instance;
72         }
73 
74         instance = new JobManager(context, componentName, subId);
75         sInstances.put(key, instance);
76         return instance;
77     }
78 
newJobInfoBuilder(int jobId)79     private JobInfo.Builder newJobInfoBuilder(int jobId) {
80         return newJobInfoBuilder(jobId, 0 /* retryCount */);
81     }
82 
newJobInfoBuilder(int jobId, int retryCount)83     private JobInfo.Builder newJobInfoBuilder(int jobId, int retryCount) {
84         JobInfo.Builder builder = new JobInfo.Builder(getJobIdWithSubId(jobId), mComponentName);
85         putSubIdAndRetryExtra(builder, retryCount);
86         return builder;
87     }
88 
89     /**
90      * Returns a new job id with {@code JOB_ID_BASE_INDEX} for separating job for different
91      * subscription id, in order to avoid job be overrided for different SIM on multi SIM device.
92      * Returns original {@code jobId} if the subscription id invalid. For example, if subscription
93      * id be 8, the job id would be 8001, 8002, etc; if subscription id be -1, the job id would be
94      * 1, 2, etc.
95      */
getJobIdWithSubId(int jobId)96     private int getJobIdWithSubId(int jobId) {
97         if (SubscriptionManager.isValidSubscriptionId(mSubId)) {
98             return JOB_ID_BASE_INDEX * mSubId + jobId;
99         }
100         return jobId;
101     }
102 
103     /** Returns job id which remove {@code JOB_ID_BASE_INDEX}. */
getPureJobId(int jobId)104     public static int getPureJobId(int jobId) {
105         return jobId % JOB_ID_BASE_INDEX;
106     }
107 
putSubIdAndRetryExtra(JobInfo.Builder builder, int retryCount)108     private void putSubIdAndRetryExtra(JobInfo.Builder builder, int retryCount) {
109         PersistableBundle bundle = new PersistableBundle();
110         bundle.putInt(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mSubId);
111         bundle.putInt(EXTRA_SLOT_ID, TelephonyUtils.getSlotId(mContext, mSubId));
112         bundle.putInt(EXTRA_RETRY_COUNT, retryCount);
113         builder.setExtras(bundle);
114     }
115 
116     /** Checks Entitlement Status once has network connection without retry and delay. */
queryEntitlementStatusOnceNetworkReady()117     public void queryEntitlementStatusOnceNetworkReady() {
118         queryEntitlementStatusOnceNetworkReady(/* retryCount= */ 0, Duration.ofSeconds(0));
119     }
120 
121     /** Checks Entitlement Status once has network connection with retry count. */
queryEntitlementStatusOnceNetworkReady(int retryCount)122     public void queryEntitlementStatusOnceNetworkReady(int retryCount) {
123         queryEntitlementStatusOnceNetworkReady(retryCount, Duration.ofSeconds(0));
124     }
125 
126     /** Checks Entitlement Status once has network connection with retry count and delay. */
queryEntitlementStatusOnceNetworkReady(int retryCount, Duration delay)127     public void queryEntitlementStatusOnceNetworkReady(int retryCount, Duration delay) {
128         Log.d(
129                 TAG,
130                 "schedule QUERY_ENTITLEMENT_STATUS_JOB_ID once has network connection, "
131                         + "retryCount="
132                         + retryCount
133                         + ", delay="
134                         + delay);
135         JobInfo job =
136                 newJobInfoBuilder(QUERY_ENTITLEMENT_STATUS_JOB_ID, retryCount)
137                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
138                         .setMinimumLatency(delay.toMillis())
139                         .build();
140         mJobScheduler.schedule(job);
141     }
142 
143     /** Registers FCM service to listen push notification once has network connection. */
registerFcmOnceNetworkReady()144     public void registerFcmOnceNetworkReady() {
145         Log.d(TAG, "Schedule REGISTER_FCM_JOB_ID once has network connection.");
146         JobInfo job =
147                 newJobInfoBuilder(REGISTER_FCM_JOB_ID)
148                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
149                         .build();
150         mJobScheduler.schedule(job);
151     }
152 
153     /**
154      * Returns {@code true} if this job's subscription id still actived and still on same slot.
155      * Returns {@code false} otherwise.
156      */
isValidJob(Context context, final JobParameters params)157     public static boolean isValidJob(Context context, final JobParameters params) {
158         PersistableBundle bundle = params.getExtras();
159         int subId =
160                 bundle.getInt(
161                         SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
162                         SubscriptionManager.INVALID_SUBSCRIPTION_ID);
163         int slotId = bundle.getInt(EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
164 
165         // Avoids to do anything after user removed or swapped SIM
166         if (!TelephonyUtils.isActivedSubId(context, subId)) {
167             Log.d(TAG, "Stop reason: SUBID(" + subId + ") not point to active SIM.");
168             return false;
169         }
170 
171         // For example, the job scheduled for slot 1 then SIM been swapped to slot 2 and then start
172         // this job. So, let's ignore this case.
173         if (TelephonyUtils.getSlotId(context, subId) != slotId) {
174             Log.d(TAG, "Stop reason: SLOTID(" + slotId + ") not matched.");
175             return false;
176         }
177 
178         return true;
179     }
180 }
181