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