1 /*
2  * Copyright (C) 2020 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.ims.rcs.uce.eab;
18 
19 import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
20 import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
21 import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
22 import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
23 
24 import static com.android.ims.rcs.uce.eab.EabProvider.EAB_OPTIONS_TABLE_NAME;
25 import static com.android.ims.rcs.uce.eab.EabProvider.EAB_PRESENCE_TUPLE_TABLE_NAME;
26 
27 import android.annotation.NonNull;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.os.PersistableBundle;
35 import android.telephony.CarrierConfigManager;
36 import android.telephony.TelephonyManager;
37 import android.telephony.ims.ProvisioningManager;
38 import android.telephony.ims.RcsContactPresenceTuple;
39 import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
40 import android.telephony.ims.RcsContactUceCapability;
41 import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
42 import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import com.android.i18n.phonenumbers.NumberParseException;
47 import com.android.i18n.phonenumbers.PhoneNumberUtil;
48 import com.android.i18n.phonenumbers.Phonenumber;
49 import com.android.ims.RcsFeatureManager;
50 import com.android.ims.rcs.uce.UceController.UceControllerCallback;
51 import com.android.internal.annotations.VisibleForTesting;
52 
53 import java.time.Instant;
54 import java.time.format.DateTimeParseException;
55 import java.time.temporal.ChronoUnit;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Objects;
59 import java.util.Optional;
60 import java.util.function.Predicate;
61 
62 /**
63  * The implementation of EabController.
64  */
65 public class EabControllerImpl implements EabController {
66     private static final String TAG = "EabControllerImpl";
67 
68     // 7 days
69     private static final int DEFAULT_NON_RCS_CAPABILITY_CACHE_EXPIRATION_SEC = 7 * 24 * 60 * 60;
70     // 1 day
71     private static final int DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC = 24 * 60 * 60;
72     private static final int DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC = 60;
73 
74     // 1 week
75     private static final int CLEAN_UP_LEGACY_CAPABILITY_SEC = 7 * 24 * 60 * 60;
76     private static final int CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC = 30 * 1000;
77 
78     private final Context mContext;
79     private final int mSubId;
80     private final EabBulkCapabilityUpdater mEabBulkCapabilityUpdater;
81     private final Handler mHandler;
82 
83     private UceControllerCallback mUceControllerCallback;
84     private volatile boolean mIsSetDestroyedFlag = false;
85 
86     private ExpirationTimeFactory mExpirationTimeFactory = () -> Instant.now().getEpochSecond();
87 
88     @VisibleForTesting
89     public final Runnable mCapabilityCleanupRunnable = () -> {
90         Log.d(TAG, "Cleanup Capabilities");
91         cleanupExpiredCapabilities();
92     };
93 
94     @VisibleForTesting
95     public interface ExpirationTimeFactory {
getExpirationTime()96         long getExpirationTime();
97     }
98 
EabControllerImpl(Context context, int subId, UceControllerCallback c, Looper looper)99     public EabControllerImpl(Context context, int subId, UceControllerCallback c, Looper looper) {
100         mContext = context;
101         mSubId = subId;
102         mUceControllerCallback = c;
103         mHandler = new Handler(looper);
104         mEabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(mContext, mSubId,
105                 this,
106                 new EabContactSyncController(),
107                 mUceControllerCallback,
108                 mHandler);
109     }
110 
111     @Override
onRcsConnected(RcsFeatureManager manager)112     public void onRcsConnected(RcsFeatureManager manager) {
113     }
114 
115     @Override
onRcsDisconnected()116     public void onRcsDisconnected() {
117     }
118 
119     @Override
onDestroy()120     public void onDestroy() {
121         Log.d(TAG, "onDestroy");
122         mIsSetDestroyedFlag = true;
123         mEabBulkCapabilityUpdater.onDestroy();
124     }
125 
126     @Override
onCarrierConfigChanged()127     public void onCarrierConfigChanged() {
128         // Pick up changes to CarrierConfig and run any applicable cleanup tasks associated with
129         // that configuration.
130         mCapabilityCleanupRunnable.run();
131         cleanupOrphanedRows();
132         if (!mIsSetDestroyedFlag) {
133             mEabBulkCapabilityUpdater.onCarrierConfigChanged();
134         }
135     }
136 
137     /**
138      * Set the callback for sending the request to UceController.
139      */
140     @Override
setUceRequestCallback(UceControllerCallback c)141     public void setUceRequestCallback(UceControllerCallback c) {
142         Objects.requireNonNull(c);
143         if (mIsSetDestroyedFlag) {
144             Log.d(TAG, "EabController destroyed.");
145             return;
146         }
147         mUceControllerCallback = c;
148         mEabBulkCapabilityUpdater.setUceRequestCallback(c);
149     }
150 
151     /**
152      * Retrieve the contacts' capabilities from the EAB database.
153      */
154     @Override
getCapabilities(@onNull List<Uri> uris)155     public @NonNull List<EabCapabilityResult> getCapabilities(@NonNull List<Uri> uris) {
156         Objects.requireNonNull(uris);
157         if (mIsSetDestroyedFlag) {
158             Log.d(TAG, "EabController destroyed.");
159             return generateDestroyedResult(uris);
160         }
161 
162         Log.d(TAG, "getCapabilities uri size=" + uris.size());
163         List<EabCapabilityResult> capabilityResultList = new ArrayList();
164 
165         for (Uri uri : uris) {
166             EabCapabilityResult result = generateEabResult(uri, this::isCapabilityExpired);
167             capabilityResultList.add(result);
168         }
169         return capabilityResultList;
170     }
171 
172     /**
173      * Retrieve the contacts' capabilities from the EAB database including expired capabilities.
174      */
175     @Override
getCapabilitiesIncludingExpired( @onNull List<Uri> uris)176     public @NonNull List<EabCapabilityResult> getCapabilitiesIncludingExpired(
177             @NonNull List<Uri> uris) {
178         Objects.requireNonNull(uris);
179         if (mIsSetDestroyedFlag) {
180             Log.d(TAG, "EabController destroyed.");
181             return generateDestroyedResult(uris);
182         }
183 
184         Log.d(TAG, "getCapabilitiesIncludingExpired uri size=" + uris.size());
185         List<EabCapabilityResult> capabilityResultList = new ArrayList();
186 
187         for (Uri uri : uris) {
188             EabCapabilityResult result = generateEabResultIncludingExpired(uri,
189                     this::isCapabilityExpired);
190             capabilityResultList.add(result);
191         }
192         return capabilityResultList;
193     }
194 
195     /**
196      * Retrieve the contact's capabilities from the availability cache.
197      */
198     @Override
getAvailability(@onNull Uri contactUri)199     public @NonNull EabCapabilityResult getAvailability(@NonNull Uri contactUri) {
200         Objects.requireNonNull(contactUri);
201         if (mIsSetDestroyedFlag) {
202             Log.d(TAG, "EabController destroyed.");
203             return new EabCapabilityResult(
204                     contactUri,
205                     EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
206                     null);
207         }
208         return generateEabResult(contactUri, this::isAvailabilityExpired);
209     }
210 
211     /**
212      * Retrieve the contact's capabilities from the availability cache including expired
213      * capabilities.
214      */
215     @Override
getAvailabilityIncludingExpired(@onNull Uri contactUri)216     public @NonNull EabCapabilityResult getAvailabilityIncludingExpired(@NonNull Uri contactUri) {
217         Objects.requireNonNull(contactUri);
218         if (mIsSetDestroyedFlag) {
219             Log.d(TAG, "EabController destroyed.");
220             return new EabCapabilityResult(
221                 contactUri,
222                 EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
223                 null);
224         }
225         return generateEabResultIncludingExpired(contactUri, this::isAvailabilityExpired);
226     }
227 
228     /**
229      * Update the availability catch and save the capabilities to the EAB database.
230      */
231     @Override
saveCapabilities(@onNull List<RcsContactUceCapability> contactCapabilities)232     public void saveCapabilities(@NonNull List<RcsContactUceCapability> contactCapabilities) {
233         Objects.requireNonNull(contactCapabilities);
234         if (mIsSetDestroyedFlag) {
235             Log.d(TAG, "EabController destroyed.");
236             return;
237         }
238 
239         Log.d(TAG, "Save capabilities: " + contactCapabilities.size());
240 
241         // Update the capabilities
242         for (RcsContactUceCapability capability : contactCapabilities) {
243             String phoneNumber = getNumberFromUri(mContext, capability.getContactUri());
244             Cursor c = mContext.getContentResolver().query(
245                     EabProvider.CONTACT_URI, null,
246                     EabProvider.ContactColumns.PHONE_NUMBER + "=?",
247                     new String[]{phoneNumber}, null);
248 
249             if (c != null && c.moveToNext()) {
250                 int contactId = getIntValue(c, EabProvider.ContactColumns._ID);
251                 if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
252                     Log.d(TAG, "Insert presence capability");
253                     deleteOldPresenceCapability(contactId);
254                     insertNewPresenceCapability(contactId, capability);
255                 } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
256                     Log.d(TAG, "Insert options capability");
257                     deleteOldOptionCapability(contactId);
258                     insertNewOptionCapability(contactId, capability);
259                 }
260             } else {
261                 Log.e(TAG, "The phone number can't find in contact table. ");
262                 int contactId = insertNewContact(phoneNumber);
263                 if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
264                     insertNewPresenceCapability(contactId, capability);
265                 } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
266                     insertNewOptionCapability(contactId, capability);
267                 }
268             }
269 
270             if (c != null) {
271                 c.close();
272             }
273         }
274         cleanupOrphanedRows();
275         mEabBulkCapabilityUpdater.updateExpiredTimeAlert();
276 
277         if (mHandler.hasCallbacks(mCapabilityCleanupRunnable)) {
278             mHandler.removeCallbacks(mCapabilityCleanupRunnable);
279         }
280         mHandler.postDelayed(mCapabilityCleanupRunnable,
281                 CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC);
282     }
283 
284     /**
285      * Cleanup the entry of common table that can't map to presence or option table
286      */
287     @VisibleForTesting
cleanupOrphanedRows()288     public void cleanupOrphanedRows() {
289         String presenceSelection =
290                 " (SELECT " + EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
291                         " FROM " + EAB_PRESENCE_TUPLE_TABLE_NAME + ") ";
292         String optionSelection =
293                 " (SELECT " + EabProvider.OptionsColumns.EAB_COMMON_ID +
294                         " FROM " + EAB_OPTIONS_TABLE_NAME + ") ";
295 
296         mContext.getContentResolver().delete(
297                 EabProvider.COMMON_URI,
298                 EabProvider.EabCommonColumns._ID + " NOT IN " + presenceSelection +
299                         " AND " + EabProvider.EabCommonColumns._ID+ " NOT IN " + optionSelection,
300                 null);
301     }
302 
generateDestroyedResult(List<Uri> contactUri)303     private List<EabCapabilityResult> generateDestroyedResult(List<Uri> contactUri) {
304         List<EabCapabilityResult> destroyedResult = new ArrayList<>();
305         for (Uri uri : contactUri) {
306             destroyedResult.add(new EabCapabilityResult(
307                     uri,
308                     EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
309                     null));
310         }
311         return destroyedResult;
312     }
313 
generateEabResult(Uri contactUri, Predicate<Cursor> isExpiredMethod)314     private EabCapabilityResult generateEabResult(Uri contactUri,
315             Predicate<Cursor> isExpiredMethod) {
316         RcsUceCapabilityBuilderWrapper builder = null;
317         EabCapabilityResult result;
318 
319         // query EAB provider
320         Uri queryUri = Uri.withAppendedPath(
321                 Uri.withAppendedPath(EabProvider.ALL_DATA_URI, String.valueOf(mSubId)),
322                 getNumberFromUri(mContext, contactUri));
323         Cursor cursor = mContext.getContentResolver().query(
324                 queryUri, null, null, null, null);
325 
326         if (cursor != null && cursor.getCount() != 0) {
327             while (cursor.moveToNext()) {
328                 if (isExpiredMethod.test(cursor)) {
329                     continue;
330                 }
331 
332                 if (builder == null) {
333                     builder = createNewBuilder(contactUri, cursor);
334                 } else {
335                     updateCapability(contactUri, cursor, builder);
336                 }
337             }
338             cursor.close();
339 
340             if (builder == null) {
341                 result = new EabCapabilityResult(contactUri,
342                         EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
343                         null);
344             } else {
345                 if (builder.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
346                     PresenceBuilder presenceBuilder = builder.getPresenceBuilder();
347                     result = new EabCapabilityResult(contactUri,
348                             EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
349                             presenceBuilder.build());
350                 } else {
351                     OptionsBuilder optionsBuilder = builder.getOptionsBuilder();
352                     result = new EabCapabilityResult(contactUri,
353                             EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
354                             optionsBuilder.build());
355                 }
356 
357             }
358         } else {
359             result = new EabCapabilityResult(contactUri,
360                     EabCapabilityResult.EAB_CONTACT_NOT_FOUND_FAILURE, null);
361         }
362         return result;
363     }
364 
generateEabResultIncludingExpired(Uri contactUri, Predicate<Cursor> isExpiredMethod)365     private EabCapabilityResult generateEabResultIncludingExpired(Uri contactUri,
366             Predicate<Cursor> isExpiredMethod) {
367         RcsUceCapabilityBuilderWrapper builder = null;
368         EabCapabilityResult result;
369         Optional<Boolean> isExpired = Optional.empty();
370 
371         // query EAB provider
372         Uri queryUri = Uri.withAppendedPath(
373                 Uri.withAppendedPath(EabProvider.ALL_DATA_URI, String.valueOf(mSubId)),
374                 getNumberFromUri(mContext, contactUri));
375         Cursor cursor = mContext.getContentResolver().query(queryUri, null, null, null, null);
376 
377         if (cursor != null && cursor.getCount() != 0) {
378             while (cursor.moveToNext()) {
379                 // Record whether it has expired.
380                 if (!isExpired.isPresent()) {
381                     isExpired = Optional.of(isExpiredMethod.test(cursor));
382                 }
383                 if (builder == null) {
384                     builder = createNewBuilder(contactUri, cursor);
385                 } else {
386                     updateCapability(contactUri, cursor, builder);
387                 }
388             }
389             cursor.close();
390 
391             // Determine the query result
392             int eabResult = EabCapabilityResult.EAB_QUERY_SUCCESSFUL;
393             if (isExpired.orElse(false)) {
394                 eabResult = EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE;
395             }
396 
397             if (builder.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
398                 PresenceBuilder presenceBuilder = builder.getPresenceBuilder();
399                 result = new EabCapabilityResult(contactUri, eabResult, presenceBuilder.build());
400             } else {
401                 OptionsBuilder optionsBuilder = builder.getOptionsBuilder();
402                 result = new EabCapabilityResult(contactUri, eabResult, optionsBuilder.build());
403             }
404         } else {
405             result = new EabCapabilityResult(contactUri,
406                     EabCapabilityResult.EAB_CONTACT_NOT_FOUND_FAILURE, null);
407         }
408         return result;
409     }
410 
updateCapability(Uri contactUri, Cursor cursor, RcsUceCapabilityBuilderWrapper builderWrapper)411     private void updateCapability(Uri contactUri, Cursor cursor,
412                 RcsUceCapabilityBuilderWrapper builderWrapper) {
413         if (builderWrapper.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
414             PresenceBuilder builder = builderWrapper.getPresenceBuilder();
415             if (builder == null) {
416                 return;
417             }
418             RcsContactPresenceTuple presenceTuple = createPresenceTuple(contactUri, cursor);
419             if (presenceTuple != null) {
420                 builder.addCapabilityTuple(presenceTuple);
421             }
422         } else {
423             OptionsBuilder builder = builderWrapper.getOptionsBuilder();
424             if (builder != null) {
425                 builder.addFeatureTag(createOptionTuple(cursor));
426             }
427         }
428     }
429 
createNewBuilder(Uri contactUri, Cursor cursor)430     private RcsUceCapabilityBuilderWrapper createNewBuilder(Uri contactUri, Cursor cursor) {
431         int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
432         int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
433         RcsUceCapabilityBuilderWrapper builderWrapper =
434                 new RcsUceCapabilityBuilderWrapper(mechanism);
435 
436         if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
437             PresenceBuilder builder = new PresenceBuilder(
438                     contactUri, SOURCE_TYPE_CACHED, result);
439             RcsContactPresenceTuple tuple = createPresenceTuple(contactUri, cursor);
440             if (tuple != null) {
441                 builder.addCapabilityTuple(tuple);
442             }
443             String entityUri = getStringValue(cursor, EabProvider.EabCommonColumns.ENTITY_URI);
444             if (!TextUtils.isEmpty(entityUri)) {
445                 builder.setEntityUri(Uri.parse(entityUri));
446             }
447             builderWrapper.setPresenceBuilder(builder);
448         } else {
449             OptionsBuilder builder = new OptionsBuilder(contactUri, SOURCE_TYPE_CACHED);
450             builder.setRequestResult(result);
451             builder.addFeatureTag(createOptionTuple(cursor));
452             builderWrapper.setOptionsBuilder(builder);
453         }
454         return builderWrapper;
455     }
456 
createOptionTuple(Cursor cursor)457     private String createOptionTuple(Cursor cursor) {
458         return getStringValue(cursor, EabProvider.OptionsColumns.FEATURE_TAG);
459     }
460 
createPresenceTuple(Uri contactUri, Cursor cursor)461     private RcsContactPresenceTuple createPresenceTuple(Uri contactUri, Cursor cursor) {
462         // RcsContactPresenceTuple fields
463         String status = getStringValue(cursor, EabProvider.PresenceTupleColumns.BASIC_STATUS);
464         String serviceId = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_ID);
465         String version = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_VERSION);
466         String description = getStringValue(cursor, EabProvider.PresenceTupleColumns.DESCRIPTION);
467         String timeStamp = getStringValue(cursor,
468                 EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
469 
470         // ServiceCapabilities fields
471         boolean audioCapable = getIntValue(cursor,
472                 EabProvider.PresenceTupleColumns.AUDIO_CAPABLE) == 1;
473         boolean videoCapable = getIntValue(cursor,
474                 EabProvider.PresenceTupleColumns.VIDEO_CAPABLE) == 1;
475         String duplexModes = getStringValue(cursor,
476                 EabProvider.PresenceTupleColumns.DUPLEX_MODE);
477         String unsupportedDuplexModes = getStringValue(cursor,
478                 EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE);
479         String[] duplexModeList, unsupportedDuplexModeList;
480 
481         if (!TextUtils.isEmpty(duplexModes)) {
482             duplexModeList = duplexModes.split(",");
483         } else {
484             duplexModeList = new String[0];
485         }
486         if (!TextUtils.isEmpty(unsupportedDuplexModes)) {
487             unsupportedDuplexModeList = unsupportedDuplexModes.split(",");
488         } else {
489             unsupportedDuplexModeList = new String[0];
490         }
491 
492         // Create ServiceCapabilities
493         ServiceCapabilities serviceCapabilities;
494         ServiceCapabilities.Builder serviceCapabilitiesBuilder =
495                 new ServiceCapabilities.Builder(audioCapable, videoCapable);
496         if (!TextUtils.isEmpty(duplexModes)
497                 || !TextUtils.isEmpty(unsupportedDuplexModes)) {
498             for (String duplexMode : duplexModeList) {
499                 serviceCapabilitiesBuilder.addSupportedDuplexMode(duplexMode);
500             }
501             for (String unsupportedDuplex : unsupportedDuplexModeList) {
502                 serviceCapabilitiesBuilder.addUnsupportedDuplexMode(unsupportedDuplex);
503             }
504         }
505         serviceCapabilities = serviceCapabilitiesBuilder.build();
506 
507         // Create RcsContactPresenceTuple
508         boolean isTupleEmpty = TextUtils.isEmpty(status) && TextUtils.isEmpty(serviceId)
509                 && TextUtils.isEmpty(version);
510         if (!isTupleEmpty) {
511             RcsContactPresenceTuple.Builder rcsContactPresenceTupleBuilder =
512                     new RcsContactPresenceTuple.Builder(status, serviceId, version);
513             if (description != null) {
514                 rcsContactPresenceTupleBuilder.setServiceDescription(description);
515             }
516             if (contactUri != null) {
517                 rcsContactPresenceTupleBuilder.setContactUri(contactUri);
518             }
519             if (serviceCapabilities != null) {
520                 rcsContactPresenceTupleBuilder.setServiceCapabilities(serviceCapabilities);
521             }
522             if (timeStamp != null) {
523                 try {
524                     Instant instant = Instant.ofEpochSecond(Long.parseLong(timeStamp));
525                     rcsContactPresenceTupleBuilder.setTime(instant);
526                 } catch (NumberFormatException ex) {
527                     Log.w(TAG, "Create presence tuple: NumberFormatException");
528                 } catch (DateTimeParseException e) {
529                     Log.w(TAG, "Create presence tuple: parse timestamp failed");
530                 }
531             }
532             return rcsContactPresenceTupleBuilder.build();
533         } else {
534             return null;
535         }
536     }
537 
isCapabilityExpired(Cursor cursor)538     private boolean isCapabilityExpired(Cursor cursor) {
539         boolean expired = false;
540         String requestTimeStamp = getRequestTimestamp(cursor);
541         int capabilityCacheExpiration;
542 
543         if (isNonRcsCapability(cursor)) {
544             capabilityCacheExpiration = getNonRcsCapabilityCacheExpiration(mSubId);
545         } else {
546             capabilityCacheExpiration = getCapabilityCacheExpiration(mSubId);
547         }
548 
549         if (requestTimeStamp != null) {
550             Instant expiredTimestamp = Instant
551                     .ofEpochSecond(Long.parseLong(requestTimeStamp))
552                     .plus(capabilityCacheExpiration, ChronoUnit.SECONDS);
553             expired = expiredTimestamp.isBefore(Instant.now());
554             Log.d(TAG, "Capability expiredTimestamp: " + expiredTimestamp.getEpochSecond() +
555                     ", isNonRcsCapability: " +  isNonRcsCapability(cursor) +
556                     ", capabilityCacheExpiration: " + capabilityCacheExpiration +
557                     ", expired:" + expired);
558         } else {
559             Log.d(TAG, "Capability requestTimeStamp is null");
560         }
561         return expired;
562     }
563 
isNonRcsCapability(Cursor cursor)564     private boolean isNonRcsCapability(Cursor cursor) {
565         int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
566         return result == REQUEST_RESULT_NOT_FOUND;
567     }
568 
isAvailabilityExpired(Cursor cursor)569     private boolean isAvailabilityExpired(Cursor cursor) {
570         boolean expired = false;
571         String requestTimeStamp = getRequestTimestamp(cursor);
572 
573         if (requestTimeStamp != null) {
574             Instant expiredTimestamp = Instant
575                     .ofEpochSecond(Long.parseLong(requestTimeStamp))
576                     .plus(getAvailabilityCacheExpiration(mSubId), ChronoUnit.SECONDS);
577             expired = expiredTimestamp.isBefore(Instant.now());
578             Log.d(TAG, "Availability insertedTimestamp: "
579                     + expiredTimestamp.getEpochSecond() + ", expired:" + expired);
580         } else {
581             Log.d(TAG, "Capability requestTimeStamp is null");
582         }
583         return expired;
584     }
585 
getRequestTimestamp(Cursor cursor)586     private String getRequestTimestamp(Cursor cursor) {
587         String expiredTimestamp = null;
588         int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
589         if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
590             expiredTimestamp = getStringValue(cursor,
591                     EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
592 
593         } else if (mechanism == CAPABILITY_MECHANISM_OPTIONS) {
594             expiredTimestamp = getStringValue(cursor, EabProvider.OptionsColumns.REQUEST_TIMESTAMP);
595         }
596         return expiredTimestamp;
597     }
598 
getNonRcsCapabilityCacheExpiration(int subId)599     private int getNonRcsCapabilityCacheExpiration(int subId) {
600         int value;
601         PersistableBundle carrierConfig =
602                 mContext.getSystemService(CarrierConfigManager.class).getConfigForSubId(subId);
603 
604         if (carrierConfig != null) {
605             value = carrierConfig.getInt(
606                     CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT);
607         } else {
608             value = DEFAULT_NON_RCS_CAPABILITY_CACHE_EXPIRATION_SEC;
609             Log.e(TAG, "getNonRcsCapabilityCacheExpiration: " +
610                     "CarrierConfig is null, returning default");
611         }
612         return value;
613     }
614 
getCapabilityCacheExpiration(int subId)615     protected int getCapabilityCacheExpiration(int subId) {
616         int value = -1;
617         try {
618             ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
619             value = pm.getProvisioningIntValue(
620                     ProvisioningManager.KEY_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC);
621         } catch (Exception ex) {
622             Log.e(TAG, "Exception in getCapabilityCacheExpiration(): " + ex);
623         }
624 
625         if (value <= 0) {
626             value = DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC;
627             Log.e(TAG, "The capability expiration cannot be less than 0.");
628         }
629         return value;
630     }
631 
getAvailabilityCacheExpiration(int subId)632     protected long getAvailabilityCacheExpiration(int subId) {
633         long value = -1;
634         try {
635             ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
636             value = pm.getProvisioningIntValue(
637                     ProvisioningManager.KEY_RCS_AVAILABILITY_CACHE_EXPIRATION_SEC);
638         } catch (Exception ex) {
639             Log.e(TAG, "Exception in getAvailabilityCacheExpiration(): " + ex);
640         }
641 
642         if (value <= 0) {
643             value = DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC;
644             Log.e(TAG, "The Availability expiration cannot be less than 0.");
645         }
646         return value;
647     }
648 
insertNewContact(String phoneNumber)649     private int insertNewContact(String phoneNumber) {
650         ContentValues contentValues = new ContentValues();
651         contentValues.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
652         Uri result = mContext.getContentResolver().insert(EabProvider.CONTACT_URI, contentValues);
653         return Integer.parseInt(result.getLastPathSegment());
654     }
655 
deleteOldPresenceCapability(int id)656     private void deleteOldPresenceCapability(int id) {
657         Cursor c = mContext.getContentResolver().query(
658                 EabProvider.COMMON_URI,
659                 new String[]{EabProvider.EabCommonColumns._ID},
660                 EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
661                 new String[]{String.valueOf(id)}, null);
662 
663         if (c != null && c.getCount() > 0) {
664             while(c.moveToNext()) {
665                 int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
666                 mContext.getContentResolver().delete(
667                         EabProvider.PRESENCE_URI,
668                         EabProvider.PresenceTupleColumns.EAB_COMMON_ID + "=?",
669                         new String[]{String.valueOf(commonId)});
670             }
671         }
672 
673         if (c != null) {
674             c.close();
675         }
676     }
677 
insertNewPresenceCapability(int contactId, RcsContactUceCapability capability)678     private void insertNewPresenceCapability(int contactId, RcsContactUceCapability capability) {
679         ContentValues contentValues = new ContentValues();
680         contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
681         contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
682         contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
683         contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
684                 capability.getRequestResult());
685         if (capability.getEntityUri() != null) {
686             contentValues.put(EabProvider.EabCommonColumns.ENTITY_URI,
687                     capability.getEntityUri().toString());
688         }
689         Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
690         int commonId = Integer.parseInt(result.getLastPathSegment());
691         Log.d(TAG, "Insert into common table. Id: " + commonId);
692 
693         if (capability.getCapabilityTuples().size() == 0) {
694             insertEmptyTuple(commonId);
695         } else {
696             insertAllTuples(commonId, capability);
697         }
698     }
699 
insertEmptyTuple(int commonId)700     private void insertEmptyTuple(int commonId) {
701         Log.d(TAG, "Insert empty tuple into presence table.");
702         ContentValues contentValues = new ContentValues();
703         contentValues.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonId);
704         // Using current timestamp instead of network timestamp since there is not use cases for
705         // network timestamp and the network timestamp may cause capability expire immediately.
706         contentValues.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP,
707                 mExpirationTimeFactory.getExpirationTime());
708         mContext.getContentResolver().insert(EabProvider.PRESENCE_URI, contentValues);
709     }
710 
insertAllTuples(int commonId, RcsContactUceCapability capability)711     private void insertAllTuples(int commonId, RcsContactUceCapability capability) {
712         ContentValues[] presenceContent =
713                 new ContentValues[capability.getCapabilityTuples().size()];
714 
715         for (int i = 0; i < presenceContent.length; i++) {
716             RcsContactPresenceTuple tuple = capability.getCapabilityTuples().get(i);
717 
718             // Create new ServiceCapabilities
719             ServiceCapabilities serviceCapabilities = tuple.getServiceCapabilities();
720             String duplexMode = null, unsupportedDuplexMode = null;
721             if (serviceCapabilities != null) {
722                 List<String> duplexModes = serviceCapabilities.getSupportedDuplexModes();
723                 if (duplexModes.size() != 0) {
724                     duplexMode = TextUtils.join(",", duplexModes);
725                 }
726 
727                 List<String> unsupportedDuplexModes =
728                         serviceCapabilities.getUnsupportedDuplexModes();
729                 if (unsupportedDuplexModes.size() != 0) {
730                     unsupportedDuplexMode =
731                             TextUtils.join(",", unsupportedDuplexModes);
732                 }
733             }
734 
735             ContentValues contentValues = new ContentValues();
736             contentValues.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonId);
737             contentValues.put(EabProvider.PresenceTupleColumns.BASIC_STATUS, tuple.getStatus());
738             contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_ID, tuple.getServiceId());
739             contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_VERSION,
740                     tuple.getServiceVersion());
741             contentValues.put(EabProvider.PresenceTupleColumns.DESCRIPTION,
742                     tuple.getServiceDescription());
743 
744             // Using current timestamp instead of network timestamp since there is not use cases for
745             // network timestamp and the network timestamp may cause capability expire immediately.
746             contentValues.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP,
747                     mExpirationTimeFactory.getExpirationTime());
748             contentValues.put(EabProvider.PresenceTupleColumns.CONTACT_URI,
749                     tuple.getContactUri().toString());
750             if (serviceCapabilities != null) {
751                 contentValues.put(EabProvider.PresenceTupleColumns.DUPLEX_MODE, duplexMode);
752                 contentValues.put(EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE,
753                         unsupportedDuplexMode);
754 
755                 contentValues.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE,
756                         serviceCapabilities.isAudioCapable());
757                 contentValues.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE,
758                         serviceCapabilities.isVideoCapable());
759             }
760             presenceContent[i] = contentValues;
761         }
762         Log.d(TAG, "Insert into presence table. count: " + presenceContent.length);
763         mContext.getContentResolver().bulkInsert(EabProvider.PRESENCE_URI, presenceContent);
764     }
765 
deleteOldOptionCapability(int contactId)766     private void deleteOldOptionCapability(int contactId) {
767         Cursor c = mContext.getContentResolver().query(
768                 EabProvider.COMMON_URI,
769                 new String[]{EabProvider.EabCommonColumns._ID},
770                 EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
771                 new String[]{String.valueOf(contactId)}, null);
772 
773         if (c != null && c.getCount() > 0) {
774             while(c.moveToNext()) {
775                 int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
776                 mContext.getContentResolver().delete(
777                         EabProvider.OPTIONS_URI,
778                         EabProvider.OptionsColumns.EAB_COMMON_ID + "=?",
779                         new String[]{String.valueOf(commonId)});
780             }
781         }
782 
783         if (c != null) {
784             c.close();
785         }
786     }
787 
insertNewOptionCapability(int contactId, RcsContactUceCapability capability)788     private void insertNewOptionCapability(int contactId, RcsContactUceCapability capability) {
789         ContentValues contentValues = new ContentValues();
790         contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
791         contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_OPTIONS);
792         contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
793         contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
794                 capability.getRequestResult());
795         Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
796 
797         int commonId = Integer.valueOf(result.getLastPathSegment());
798         List<ContentValues> optionContentList = new ArrayList<>();
799         for (String feature : capability.getFeatureTags()) {
800             contentValues = new ContentValues();
801             contentValues.put(EabProvider.OptionsColumns.EAB_COMMON_ID, commonId);
802             contentValues.put(EabProvider.OptionsColumns.FEATURE_TAG, feature);
803             contentValues.put(EabProvider.OptionsColumns.REQUEST_TIMESTAMP,
804                     Instant.now().getEpochSecond());
805             optionContentList.add(contentValues);
806         }
807 
808         ContentValues[] optionContent = new ContentValues[optionContentList.size()];
809         optionContent = optionContentList.toArray(optionContent);
810         mContext.getContentResolver().bulkInsert(EabProvider.OPTIONS_URI, optionContent);
811     }
812 
cleanupExpiredCapabilities()813     private void cleanupExpiredCapabilities() {
814         // Cleanup the capabilities that expired more than 1 week
815         long rcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
816                 getCapabilityCacheExpiration(mSubId) -
817                 CLEAN_UP_LEGACY_CAPABILITY_SEC;
818 
819         // Cleanup the capabilities that expired more than 1 week
820         long nonRcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
821                 getNonRcsCapabilityCacheExpiration(mSubId) -
822                 CLEAN_UP_LEGACY_CAPABILITY_SEC;
823 
824         cleanupCapabilities(rcsCapabilitiesExpiredTime, getRcsCommonIdList());
825         cleanupCapabilities(nonRcsCapabilitiesExpiredTime, getNonRcsCommonIdList());
826     }
827 
cleanupCapabilities(long rcsCapabilitiesExpiredTime, List<Integer> commonIdList)828     private void cleanupCapabilities(long rcsCapabilitiesExpiredTime, List<Integer> commonIdList) {
829         if (commonIdList.size() > 0) {
830             String presenceClause =
831                     EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
832                             " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
833                             EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<?";
834 
835             String optionClause =
836                     EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
837                             " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
838                             EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<?";
839 
840             int deletePresenceCount = mContext.getContentResolver().delete(
841                     EabProvider.PRESENCE_URI,
842                     presenceClause,
843                     new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
844 
845             int deleteOptionsCount = mContext.getContentResolver().delete(
846                     EabProvider.OPTIONS_URI,
847                     optionClause,
848                     new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
849 
850             Log.d(TAG, "Cleanup capabilities. deletePresenceCount: " + deletePresenceCount +
851                 ",deleteOptionsCount: " + deleteOptionsCount);
852         }
853     }
854 
getRcsCommonIdList()855     private List<Integer> getRcsCommonIdList() {
856         ArrayList<Integer> list = new ArrayList<>();
857         Cursor cursor = mContext.getContentResolver().query(
858                 EabProvider.COMMON_URI,
859                 null,
860                 EabProvider.EabCommonColumns.REQUEST_RESULT + "<>?",
861                 new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
862                 null);
863 
864         if (cursor == null) return list;
865 
866         while (cursor.moveToNext()) {
867             list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
868         }
869         cursor.close();
870 
871         return list;
872     }
873 
getNonRcsCommonIdList()874     private List<Integer> getNonRcsCommonIdList() {
875         ArrayList<Integer> list = new ArrayList<>();
876         Cursor cursor = mContext.getContentResolver().query(
877                 EabProvider.COMMON_URI,
878                 null,
879                 EabProvider.EabCommonColumns.REQUEST_RESULT + "=?",
880                 new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
881                 null);
882 
883         if (cursor == null) return list;
884 
885         while (cursor.moveToNext()) {
886             list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
887         }
888         cursor.close();
889 
890         return list;
891     }
892 
getStringValue(Cursor cursor, String column)893     private String getStringValue(Cursor cursor, String column) {
894         return cursor.getString(cursor.getColumnIndex(column));
895     }
896 
getIntValue(Cursor cursor, String column)897     private int getIntValue(Cursor cursor, String column) {
898         return cursor.getInt(cursor.getColumnIndex(column));
899     }
900 
getNumberFromUri(Context context, Uri uri)901     private static String getNumberFromUri(Context context, Uri uri) {
902         String number = uri.getSchemeSpecificPart();
903         String[] numberParts = number.split("[@;:]");
904         if (numberParts.length == 0) {
905             return null;
906         }
907         return formatNumber(context, numberParts[0]);
908     }
909 
formatNumber(Context context, String number)910     static String formatNumber(Context context, String number) {
911         TelephonyManager manager = context.getSystemService(TelephonyManager.class);
912         String simCountryIso = manager.getSimCountryIso();
913         if (simCountryIso != null) {
914             simCountryIso = simCountryIso.toUpperCase();
915             PhoneNumberUtil util = PhoneNumberUtil.getInstance();
916             try {
917                 Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso);
918                 return util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
919             } catch (NumberParseException e) {
920                 Log.w(TAG, "formatNumber: could not format " + number + ", error: " + e);
921             }
922         }
923         return number;
924     }
925 
926     @VisibleForTesting
setExpirationTimeFactory(ExpirationTimeFactory factory)927     public void setExpirationTimeFactory(ExpirationTimeFactory factory) {
928         mExpirationTimeFactory = factory;
929     }
930 }
931