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;
18 
19 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED;
20 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
21 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
22 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
23 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
24 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
25 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING;
26 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE;
27 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP;
28 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE;
29 import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI;
30 
31 import android.app.job.JobParameters;
32 import android.app.job.JobService;
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.os.AsyncTask;
36 import android.os.PersistableBundle;
37 import android.telephony.SubscriptionManager;
38 import android.util.Log;
39 import android.util.SparseArray;
40 
41 import androidx.annotation.Nullable;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.annotation.WorkerThread;
44 
45 import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration;
46 import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior;
47 import com.android.imsserviceentitlement.entitlement.EntitlementResult;
48 import com.android.imsserviceentitlement.job.JobManager;
49 import com.android.imsserviceentitlement.utils.ImsUtils;
50 import com.android.imsserviceentitlement.utils.TelephonyUtils;
51 
52 import java.time.Duration;
53 
54 /**
55  * The {@link JobService} for querying entitlement status in the background. The jobId is unique for
56  * different subId + job combination, so can run the same job for different subIds w/o cancelling
57  * each others. See {@link JobManager}.
58  */
59 public class ImsEntitlementPollingService extends JobService {
60     private static final String TAG = "IMSSE-ImsEntitlementPollingService";
61 
62     public static final ComponentName COMPONENT_NAME =
63             ComponentName.unflattenFromString(
64                     "com.android.imsserviceentitlement/.ImsEntitlementPollingService");
65 
66     private ImsEntitlementApi mImsEntitlementApi;
67 
68     /**
69      * Cache job id associated {@link EntitlementPollingTask} objects for canceling once job be
70      * canceled.
71      */
72     private final SparseArray<EntitlementPollingTask> mTasks = new SparseArray<>();
73 
74     @VisibleForTesting
75     EntitlementPollingTask mOngoingTask;
76 
77     @Override
78     @VisibleForTesting
attachBaseContext(Context base)79     protected void attachBaseContext(Context base) {
80         super.attachBaseContext(base);
81     }
82 
83     @VisibleForTesting
injectImsEntitlementApi(ImsEntitlementApi imsEntitlementApi)84     void injectImsEntitlementApi(ImsEntitlementApi imsEntitlementApi) {
85         this.mImsEntitlementApi = imsEntitlementApi;
86     }
87 
88     /** Enqueues a job to query entitlement status. */
enqueueJob(Context context, int subId, int retryCount)89     public static void enqueueJob(Context context, int subId, int retryCount) {
90         JobManager.getInstance(
91                 context,
92                 COMPONENT_NAME,
93                 subId)
94                 .queryEntitlementStatusOnceNetworkReady(retryCount);
95     }
96 
97     /** Enqueues a job to query entitlement status with delay. */
enqueueJobWithDelay(Context context, int subId, long delayInSeconds)98     private static void enqueueJobWithDelay(Context context, int subId, long delayInSeconds) {
99         JobManager.getInstance(
100                 context,
101                 COMPONENT_NAME,
102                 subId)
103                 .queryEntitlementStatusOnceNetworkReady(0, Duration.ofSeconds(delayInSeconds));
104     }
105 
106     @Override
onStartJob(final JobParameters params)107     public boolean onStartJob(final JobParameters params) {
108         PersistableBundle bundle = params.getExtras();
109         int subId =
110                 bundle.getInt(
111                         SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
112                         SubscriptionManager.INVALID_SUBSCRIPTION_ID);
113 
114         int jobId = params.getJobId();
115         Log.d(TAG, "onStartJob: " + jobId);
116 
117         // Ignore the job if the SIM be removed or swapped
118         if (!JobManager.isValidJob(this, params)) {
119             Log.d(TAG, "Stop for invalid job! " + jobId);
120             return false;
121         }
122 
123         // if the same job ID is scheduled again, the current one will be cancelled by platform and
124         // #onStopJob will be called to removed the job.
125         mOngoingTask = new EntitlementPollingTask(params, subId);
126         mTasks.put(jobId, mOngoingTask);
127         mOngoingTask.execute();
128         return true;
129     }
130 
131     @Override
onStopJob(final JobParameters params)132     public boolean onStopJob(final JobParameters params) {
133         int jobId = params.getJobId();
134         Log.d(TAG, "onStopJob: " + jobId);
135         EntitlementPollingTask task = mTasks.get(jobId);
136         if (task != null) {
137             task.cancel(true);
138             mTasks.remove(jobId);
139         }
140 
141         return true;
142     }
143 
144     @VisibleForTesting
145     class EntitlementPollingTask extends AsyncTask<Void, Void, Void> {
146         private final JobParameters mParams;
147         private final ImsEntitlementApi mImsEntitlementApi;
148         private final ImsUtils mImsUtils;
149         private final TelephonyUtils mTelephonyUtils;
150         private final int mSubid;
151         private final boolean mNeedsImsProvisioning;
152 
153         // States for metrics
154         private long mStartTime;
155         private long mDurationMillis;
156         private int mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE;
157         private int mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
158         private int mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
159         private int mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
160 
EntitlementPollingTask(final JobParameters params, int subId)161         EntitlementPollingTask(final JobParameters params, int subId) {
162             this.mParams = params;
163             this.mImsUtils = ImsUtils.getInstance(ImsEntitlementPollingService.this, subId);
164             this.mTelephonyUtils = new TelephonyUtils(ImsEntitlementPollingService.this, subId);
165             this.mSubid = subId;
166             this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(
167                     ImsEntitlementPollingService.this, mSubid);
168             this.mImsEntitlementApi = ImsEntitlementPollingService.this.mImsEntitlementApi != null
169                     ? ImsEntitlementPollingService.this.mImsEntitlementApi
170                     : new ImsEntitlementApi(ImsEntitlementPollingService.this, subId);
171         }
172 
173         @Override
doInBackground(Void... unused)174         protected Void doInBackground(Void... unused) {
175             mStartTime = mTelephonyUtils.getUptimeMillis();
176             int jobId = JobManager.getPureJobId(mParams.getJobId());
177             switch (jobId) {
178                 case JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID:
179                     mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING;
180                     doEntitlementCheck();
181                     break;
182                 default:
183                     break;
184             }
185             return null;
186         }
187 
188         @Override
onPostExecute(Void unused)189         protected void onPostExecute(Void unused) {
190             Log.d(TAG, "JobId:" + mParams.getJobId() + "- Task done.");
191             sendStatsLogToMetrics();
192             ImsEntitlementPollingService.this.jobFinished(mParams, false);
193         }
194 
195         @Override
onCancelled(Void unused)196         protected void onCancelled(Void unused) {
197             sendStatsLogToMetrics();
198         }
199 
doEntitlementCheck()200         private void doEntitlementCheck() {
201             if (mNeedsImsProvisioning) {
202                 // TODO(b/190476343): Unify EntitlementResult and EntitlementConfiguration.
203                 doImsEntitlementCheck();
204             } else {
205                 doWfcEntitlementCheck();
206             }
207         }
208 
209         @WorkerThread
doImsEntitlementCheck()210         private void doImsEntitlementCheck() {
211             try {
212                 EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus();
213                 Log.d(TAG, "Entitlement result: " + result);
214 
215                 if (performRetryIfNeeded(result)) {
216                     return;
217                 }
218 
219                 if (shouldTurnOffWfc(result)) {
220                     mImsUtils.setVowifiProvisioned(false);
221                     mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
222                 } else {
223                     mImsUtils.setVowifiProvisioned(true);
224                     mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
225                 }
226 
227                 if (shouldTurnOffVolte(result)) {
228                     mImsUtils.setVolteProvisioned(false);
229                     mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
230                 } else {
231                     mImsUtils.setVolteProvisioned(true);
232                     mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
233                 }
234 
235                 if (shouldTurnOffSMSoIP(result)) {
236                     mImsUtils.setSmsoipProvisioned(false);
237                     mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
238                 } else {
239                     mImsUtils.setSmsoipProvisioned(true);
240                     mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
241                 }
242             } catch (RuntimeException e) {
243                 mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
244                 mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
245                 mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
246                 Log.d(TAG, "checkEntitlementStatus failed.", e);
247             }
248             checkVersValidity();
249         }
250 
251         @WorkerThread
doWfcEntitlementCheck()252         private void doWfcEntitlementCheck() {
253             if (!mImsUtils.isWfcEnabledByUser()) {
254                 Log.d(TAG, "WFC not turned on; checkEntitlementStatus not needed this time.");
255                 return;
256             }
257             try {
258                 EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus();
259                 Log.d(TAG, "Entitlement result: " + result);
260 
261                 if (performRetryIfNeeded(result)) {
262                     return;
263                 }
264 
265                 if (shouldTurnOffWfc(result)) {
266                     mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED;
267                     mImsUtils.disableWfc();
268                 } else {
269                     mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED;
270                 }
271             } catch (RuntimeException e) {
272                 mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
273                 Log.d(TAG, "checkEntitlementStatus failed.", e);
274             }
275         }
276 
277         /**
278          * Performs retry if needed. Returns true if {@link ImsEntitlementPollingService} has
279          * scheduled.
280          */
performRetryIfNeeded(@ullable EntitlementResult result)281         private boolean performRetryIfNeeded(@Nullable EntitlementResult result) {
282             if (result == null || result.getRetryAfterSeconds() < 0) {
283                 return false;
284             }
285             mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED;
286             ImsEntitlementPollingService.enqueueJobWithDelay(
287                     ImsEntitlementPollingService.this,
288                     mSubid,
289                     result.getRetryAfterSeconds());
290             return true;
291         }
292 
293         /**
294          * Schedules entitlement status check after a VERS.validity time, if the last valid is
295          * during validity.
296          */
checkVersValidity()297         private void checkVersValidity() {
298             EntitlementConfiguration lastEntitlementConfiguration =
299                     new EntitlementConfiguration(ImsEntitlementPollingService.this, mSubid);
300             if (lastEntitlementConfiguration.entitlementValidation()
301                     == ClientBehavior.VALID_DURING_VALIDITY) {
302                 enqueueJobWithDelay(
303                         ImsEntitlementPollingService.this,
304                         mSubid,
305                         lastEntitlementConfiguration.getVersValidity());
306             }
307         }
308 
309         /**
310          * Returns {@code true} when {@code EntitlementResult} says WFC is not activated; Otherwise
311          * {@code false} if {@code EntitlementResult} is not of any known pattern.
312          */
shouldTurnOffWfc(@ullable EntitlementResult result)313         private boolean shouldTurnOffWfc(@Nullable EntitlementResult result) {
314             if (result == null) {
315                 Log.d(TAG, "Entitlement API failed to return a result; don't turn off WFC.");
316                 return false;
317             }
318 
319             // Only turn off WFC for known patterns indicating WFC not activated.
320             return result.getVowifiStatus().serverDataMissing()
321                     || result.getVowifiStatus().inProgress()
322                     || result.getVowifiStatus().incompatible();
323         }
324 
shouldTurnOffVolte(@ullable EntitlementResult result)325         private boolean shouldTurnOffVolte(@Nullable EntitlementResult result) {
326             if (result == null) {
327                 Log.d(TAG, "Entitlement API failed to return a result; don't turn off VoLTE.");
328                 return false;
329             }
330 
331             // Only turn off VoLTE for known patterns indicating VoLTE not activated.
332             return !result.getVolteStatus().isActive();
333         }
334 
shouldTurnOffSMSoIP(@ullable EntitlementResult result)335         private boolean shouldTurnOffSMSoIP(@Nullable EntitlementResult result) {
336             if (result == null) {
337                 Log.d(TAG, "Entitlement API failed to return a result; don't turn off SMSoIP.");
338                 return false;
339             }
340 
341             // Only turn off SMSoIP for known patterns indicating SMSoIP not activated.
342             return !result.getSmsoveripStatus().isActive();
343         }
344 
sendStatsLogToMetrics()345         private void sendStatsLogToMetrics() {
346             mDurationMillis = mTelephonyUtils.getUptimeMillis() - mStartTime;
347 
348             // If no result set, it was cancelled for reasons.
349             if (mVowifiResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
350                 mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
351             }
352             writeStateLogByApps(
353                     IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI, mVowifiResult);
354 
355             if (mNeedsImsProvisioning) {
356                 if (mVolteResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
357                     mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
358                 }
359                 if (mSmsoipResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
360                     mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
361                 }
362                 writeStateLogByApps(
363                         IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE, mVolteResult);
364                 writeStateLogByApps(
365                         IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP, mSmsoipResult);
366             }
367         }
368 
writeStateLogByApps(int appId, int appResult)369         private void writeStateLogByApps(int appId, int appResult) {
370             ImsServiceEntitlementStatsLog.write(
371                     IMS_SERVICE_ENTITLEMENT_UPDATED,
372                     mTelephonyUtils.getCarrierId(),
373                     mTelephonyUtils.getSpecificCarrierId(),
374                     mPurpose,
375                     appId,
376                     appResult,
377                     mDurationMillis);
378         }
379     }
380 }
381