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