1 /* 2 * Copyright (C) 2018 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.dialer.historyitemactions; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.net.Uri; 22 import android.provider.CallLog.Calls; 23 import android.provider.ContactsContract; 24 import android.support.annotation.IntDef; 25 import android.text.TextUtils; 26 import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; 27 import com.android.dialer.callintent.CallInitiationType; 28 import com.android.dialer.callintent.CallIntentBuilder; 29 import com.android.dialer.clipboard.ClipboardUtils; 30 import com.android.dialer.common.Assert; 31 import com.android.dialer.duo.Duo; 32 import com.android.dialer.duo.DuoComponent; 33 import com.android.dialer.logging.DialerImpression; 34 import com.android.dialer.logging.ReportingLocation; 35 import com.android.dialer.spam.Spam; 36 import com.android.dialer.util.CallUtil; 37 import com.android.dialer.util.PermissionsUtil; 38 import com.android.dialer.util.UriUtils; 39 import com.google.common.collect.ImmutableList; 40 import com.google.common.collect.ImmutableMap; 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Optional; 46 47 /** 48 * Builds a list of {@link HistoryItemActionModule HistoryItemActionModules}. 49 * 50 * <p>Example usage: 51 * 52 * <pre><code> 53 * // Create a HistoryItemActionModuleInfo proto with the information you have. 54 * // You can simply skip a field if there is no information for it. 55 * HistoryItemActionModuleInfo moduleInfo = 56 * HistoryItemActionModuleInfo.newBuilder() 57 * .setNormalizedNumber("+16502530000") 58 * .setCountryIso("US") 59 * .setName("Google") 60 * .build(); 61 * 62 * // Initialize the builder using the module info above. 63 * // Note that some modules require an activity context to work so it is preferred to pass one 64 * // instead of an application context to the builder. 65 * HistoryItemActionModulesBuilder modulesBuilder = 66 * new HistoryItemActionModulesBuilder(activityContext, moduleInfo); 67 * 68 * // Add all modules you want in the order you like. 69 * // If a module shouldn't be added according to the module info, it won't be. 70 * // For example, if the module info is not for a video call and doesn't indicate the presence 71 * // of video calling capabilities, calling addModuleForVideoCall() is a no-op. 72 * modulesBuilder 73 * .addModuleForVoiceCall() 74 * .addModuleForVideoCall() 75 * .addModuleForSendingTextMessage() 76 * .addModuleForDivider() 77 * .addModuleForAddingToContacts() 78 * .addModuleForBlockedOrSpamNumber() 79 * .addModuleForCopyingNumber(); 80 * 81 * List<HistoryItemActionModule> modules = modulesBuilder.build(); 82 * </code></pre> 83 */ 84 public final class HistoryItemActionModulesBuilder { 85 86 /** Represents events when a module is tapped by the user. */ 87 @Retention(RetentionPolicy.SOURCE) 88 @IntDef({ 89 Event.ADD_TO_CONTACT, 90 Event.BLOCK_NUMBER, 91 Event.BLOCK_NUMBER_AND_REPORT_SPAM, 92 Event.REPORT_NOT_SPAM, 93 Event.REQUEST_CARRIER_VIDEO_CALL, 94 Event.REQUEST_DUO_VIDEO_CALL, 95 Event.REQUEST_DUO_VIDEO_CALL_FOR_NON_CONTACT, 96 Event.SEND_TEXT_MESSAGE, 97 Event.UNBLOCK_NUMBER 98 }) 99 @interface Event { 100 int ADD_TO_CONTACT = 1; 101 int BLOCK_NUMBER = 2; 102 int BLOCK_NUMBER_AND_REPORT_SPAM = 3; 103 int REPORT_NOT_SPAM = 4; 104 int REQUEST_CARRIER_VIDEO_CALL = 5; 105 int REQUEST_DUO_VIDEO_CALL = 6; 106 int REQUEST_DUO_VIDEO_CALL_FOR_NON_CONTACT = 7; 107 int SEND_TEXT_MESSAGE = 8; 108 int UNBLOCK_NUMBER = 9; 109 } 110 111 /** 112 * Maps each {@link Event} to a {@link DialerImpression.Type} to be logged when the modules are 113 * hosted by the call log. 114 */ 115 private static final ImmutableMap<Integer, DialerImpression.Type> CALL_LOG_IMPRESSIONS = 116 new ImmutableMap.Builder<Integer, DialerImpression.Type>() 117 .put(Event.ADD_TO_CONTACT, DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_LOG) 118 .put(Event.BLOCK_NUMBER, DialerImpression.Type.CALL_LOG_BLOCK_NUMBER) 119 .put(Event.BLOCK_NUMBER_AND_REPORT_SPAM, DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM) 120 .put(Event.REPORT_NOT_SPAM, DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM) 121 .put( 122 Event.REQUEST_CARRIER_VIDEO_CALL, 123 DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_CALL_LOG) 124 .put( 125 Event.REQUEST_DUO_VIDEO_CALL, 126 DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG) 127 .put( 128 Event.REQUEST_DUO_VIDEO_CALL_FOR_NON_CONTACT, 129 DialerImpression.Type.LIGHTBRINGER_NON_CONTACT_VIDEO_REQUESTED_FROM_CALL_LOG) 130 .put(Event.SEND_TEXT_MESSAGE, DialerImpression.Type.CALL_LOG_SEND_MESSAGE) 131 .put(Event.UNBLOCK_NUMBER, DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER) 132 .build(); 133 134 private final Context context; 135 private final HistoryItemActionModuleInfo moduleInfo; 136 private final List<HistoryItemActionModule> modules; 137 HistoryItemActionModulesBuilder(Context context, HistoryItemActionModuleInfo moduleInfo)138 public HistoryItemActionModulesBuilder(Context context, HistoryItemActionModuleInfo moduleInfo) { 139 Assert.checkArgument( 140 moduleInfo.getHost() != HistoryItemActionModuleInfo.Host.UNKNOWN, 141 "A host must be specified."); 142 143 this.context = context; 144 this.moduleInfo = moduleInfo; 145 this.modules = new ArrayList<>(); 146 } 147 build()148 public List<HistoryItemActionModule> build() { 149 return new ArrayList<>(modules); 150 } 151 152 /** 153 * Adds a module for placing a voice call. 154 * 155 * <p>The method is a no-op if the number is blocked. 156 */ addModuleForVoiceCall()157 public HistoryItemActionModulesBuilder addModuleForVoiceCall() { 158 if (moduleInfo.getIsBlocked()) { 159 return this; 160 } 161 162 // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber. 163 // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to 164 // place or receive the call should be ignored for voice calls. 165 CallIntentBuilder callIntentBuilder = 166 new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType()) 167 .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing()); 168 modules.add(IntentModule.newCallModule(context, callIntentBuilder)); 169 return this; 170 } 171 172 /** 173 * Adds a module for a carrier video call *or* a Duo video call. 174 * 175 * <p>This method is a no-op if 176 * 177 * <ul> 178 * <li>the call is one made to/received from an emergency number, 179 * <li>the call is one made to a voicemail box, 180 * <li>the call should be shown as spam, or 181 * <li>the number is blocked. 182 * </ul> 183 * 184 * <p>If the provided module info is for a Duo video call and Duo is available, add a Duo video 185 * call module. 186 * 187 * <p>If the provided module info is for a Duo video call but Duo is unavailable, add a carrier 188 * video call module. 189 * 190 * <p>If the provided module info is for a carrier video call, add a carrier video call module. 191 * 192 * <p>If the provided module info is for a voice call and the device has carrier video call 193 * capability, add a carrier video call module. 194 * 195 * <p>If the provided module info is for a voice call, the device doesn't have carrier video call 196 * capability, and Duo is available, add a Duo video call module. 197 */ addModuleForVideoCall()198 public HistoryItemActionModulesBuilder addModuleForVideoCall() { 199 if (moduleInfo.getIsEmergencyNumber() 200 || moduleInfo.getIsVoicemailCall() 201 || Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType()) 202 || moduleInfo.getIsBlocked()) { 203 return this; 204 } 205 206 // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to 207 // place or receive the call should be ignored for carrier video calls. 208 // TODO(a bug): figure out the correct video call behavior 209 CallIntentBuilder callIntentBuilder = 210 new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType()) 211 .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing()) 212 .setIsVideoCall(true); 213 214 // If the module info is for a video call, add an appropriate video call module. 215 if ((moduleInfo.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { 216 boolean isDuoCall = isDuoCall(); 217 modules.add( 218 IntentModule.newCallModule( 219 context, 220 callIntentBuilder.setIsDuoCall(isDuoCall), 221 isDuoCall 222 ? getImpressionsForDuoVideoCall() 223 : getImpressions(Event.REQUEST_CARRIER_VIDEO_CALL))); 224 return this; 225 } 226 227 // At this point, the module info is for an audio call. We will also add a video call module if 228 // the video capability is present. 229 // 230 // The carrier video call module takes precedence over the Duo module. 231 if (canPlaceCarrierVideoCall()) { 232 modules.add( 233 IntentModule.newCallModule( 234 context, callIntentBuilder, getImpressions(Event.REQUEST_CARRIER_VIDEO_CALL))); 235 } else if (canPlaceDuoCall()) { 236 modules.add( 237 IntentModule.newCallModule( 238 context, callIntentBuilder.setIsDuoCall(true), getImpressionsForDuoVideoCall())); 239 } 240 return this; 241 } 242 243 /** 244 * Returns a list of impressions to be logged when the user taps the module that attempts to 245 * initiate a Duo video call. 246 */ getImpressionsForDuoVideoCall()247 private ImmutableList<DialerImpression.Type> getImpressionsForDuoVideoCall() { 248 return isExistingContact() 249 ? getImpressions(Event.REQUEST_DUO_VIDEO_CALL) 250 : getImpressions( 251 Event.REQUEST_DUO_VIDEO_CALL, Event.REQUEST_DUO_VIDEO_CALL_FOR_NON_CONTACT); 252 } 253 254 /** 255 * Adds a module for sending text messages. 256 * 257 * <p>The method is a no-op if 258 * 259 * <ul> 260 * <li>the permission to send SMS is not granted, 261 * <li>the call is one made to/received from an emergency number, 262 * <li>the call is one made to a voicemail box, 263 * <li>the number is blocked, or 264 * <li>the number is empty. 265 * </ul> 266 */ addModuleForSendingTextMessage()267 public HistoryItemActionModulesBuilder addModuleForSendingTextMessage() { 268 // TODO(zachh): There are other conditions where this module should not be shown 269 // (e.g., business numbers). 270 if (!PermissionsUtil.hasSendSmsPermissions(context) 271 || moduleInfo.getIsEmergencyNumber() 272 || moduleInfo.getIsVoicemailCall() 273 || moduleInfo.getIsBlocked() 274 || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { 275 return this; 276 } 277 278 modules.add( 279 IntentModule.newModuleForSendingTextMessage( 280 context, moduleInfo.getNormalizedNumber(), getImpressions(Event.SEND_TEXT_MESSAGE))); 281 return this; 282 } 283 284 /** 285 * Adds a module for a divider. 286 * 287 * <p>The method is a no-op if the divider module will be the first module. 288 */ addModuleForDivider()289 public HistoryItemActionModulesBuilder addModuleForDivider() { 290 if (modules.isEmpty()) { 291 return this; 292 } 293 294 modules.add(new DividerModule()); 295 return this; 296 } 297 298 /** 299 * Adds a module for adding a number to Contacts. 300 * 301 * <p>The method is a no-op if 302 * 303 * <ul> 304 * <li>the permission to write contacts is not granted, 305 * <li>the call is one made to/received from an emergency number, 306 * <li>the call is one made to a voicemail box, 307 * <li>the call should be shown as spam, 308 * <li>the number is blocked, 309 * <li>the number is empty, or 310 * <li>the number belongs to an existing contact. 311 * </ul> 312 */ addModuleForAddingToContacts()313 public HistoryItemActionModulesBuilder addModuleForAddingToContacts() { 314 if (!PermissionsUtil.hasContactsWritePermissions(context) 315 || moduleInfo.getIsEmergencyNumber() 316 || moduleInfo.getIsVoicemailCall() 317 || Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType()) 318 || moduleInfo.getIsBlocked() 319 || isExistingContact() 320 || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { 321 return this; 322 } 323 324 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 325 intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); 326 intent.putExtra(ContactsContract.Intents.Insert.PHONE, moduleInfo.getNormalizedNumber()); 327 328 if (!TextUtils.isEmpty(moduleInfo.getName())) { 329 intent.putExtra(ContactsContract.Intents.Insert.NAME, moduleInfo.getName()); 330 } 331 332 modules.add( 333 new IntentModule( 334 context, 335 intent, 336 R.string.add_to_contacts, 337 R.drawable.quantum_ic_person_add_vd_theme_24, 338 getImpressions(Event.ADD_TO_CONTACT))); 339 return this; 340 } 341 342 /** 343 * Add modules for blocking/unblocking a number and/or marking it as spam/not spam. 344 * 345 * <p>The method is a no-op if 346 * 347 * <ul> 348 * <li>the call is one made to/received from an emergency number, or 349 * <li>the call is one made to a voicemail box. 350 * </ul> 351 * 352 * <p>If the call should be shown as spam, add two modules: 353 * 354 * <ul> 355 * <li>"Not spam" and "Block", or 356 * <li>"Not spam" and "Unblock". 357 * </ul> 358 * 359 * <p>If the number is blocked but the call should not be shown as spam, add the "Unblock" module. 360 * 361 * <p>If the number is not blocked and the call should not be shown as spam, add the "Block/Report 362 * spam" module. 363 */ addModuleForBlockedOrSpamNumber()364 public HistoryItemActionModulesBuilder addModuleForBlockedOrSpamNumber() { 365 if (moduleInfo.getIsEmergencyNumber() || moduleInfo.getIsVoicemailCall()) { 366 return this; 367 } 368 369 BlockReportSpamDialogInfo blockReportSpamDialogInfo = 370 BlockReportSpamDialogInfo.newBuilder() 371 .setNormalizedNumber(moduleInfo.getNormalizedNumber()) 372 .setCountryIso(moduleInfo.getCountryIso()) 373 .setCallType(moduleInfo.getCallType()) 374 .setReportingLocation(getReportingLocation()) 375 .setContactSource(moduleInfo.getContactSource()) 376 .build(); 377 378 // For a call that should be shown as spam, add two modules: 379 // (1) "Not spam" and "Block", or 380 // (2) "Not spam" and "Unblock". 381 if (Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType())) { 382 modules.add( 383 BlockReportSpamModules.moduleForMarkingNumberAsNotSpam( 384 context, blockReportSpamDialogInfo, getImpression(Event.REPORT_NOT_SPAM))); 385 modules.add( 386 moduleInfo.getIsBlocked() 387 ? BlockReportSpamModules.moduleForUnblockingNumber( 388 context, blockReportSpamDialogInfo, getImpression(Event.UNBLOCK_NUMBER)) 389 : BlockReportSpamModules.moduleForBlockingNumber( 390 context, blockReportSpamDialogInfo, getImpression(Event.BLOCK_NUMBER))); 391 return this; 392 } 393 394 // For a blocked number associated with a call that should not be shown as spam, add the 395 // "Unblock" module. 396 if (moduleInfo.getIsBlocked()) { 397 modules.add( 398 BlockReportSpamModules.moduleForUnblockingNumber( 399 context, blockReportSpamDialogInfo, getImpression(Event.UNBLOCK_NUMBER))); 400 return this; 401 } 402 403 // For a number that is not blocked and is associated with a call that should not be shown as 404 // spam, add the "Block/Report spam" module. 405 modules.add( 406 BlockReportSpamModules.moduleForBlockingNumberAndOptionallyReportingSpam( 407 context, blockReportSpamDialogInfo, getImpression(Event.BLOCK_NUMBER_AND_REPORT_SPAM))); 408 return this; 409 } 410 411 /** 412 * Adds a module for copying a number. 413 * 414 * <p>The method is a no-op if the number is empty. 415 */ addModuleForCopyingNumber()416 public HistoryItemActionModulesBuilder addModuleForCopyingNumber() { 417 if (TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { 418 return this; 419 } 420 421 modules.add( 422 new HistoryItemActionModule() { 423 @Override 424 public int getStringId() { 425 return R.string.copy_number; 426 } 427 428 @Override 429 public int getDrawableId() { 430 return R.drawable.quantum_ic_content_copy_vd_theme_24; 431 } 432 433 @Override 434 public boolean onClick() { 435 ClipboardUtils.copyText( 436 context, 437 /* label = */ null, 438 moduleInfo.getNormalizedNumber(), 439 /* showToast = */ true); 440 return false; 441 } 442 }); 443 return this; 444 } 445 canPlaceCarrierVideoCall()446 private boolean canPlaceCarrierVideoCall() { 447 int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context); 448 boolean isCarrierVideoCallingEnabled = 449 ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) 450 == CallUtil.VIDEO_CALLING_ENABLED); 451 boolean canRelyOnCarrierVideoPresence = 452 ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) 453 == CallUtil.VIDEO_CALLING_PRESENCE); 454 455 return isCarrierVideoCallingEnabled 456 && canRelyOnCarrierVideoPresence 457 && moduleInfo.getCanSupportCarrierVideoCall(); 458 } 459 isDuoCall()460 private boolean isDuoCall() { 461 return DuoComponent.get(context) 462 .getDuo() 463 .isDuoAccount(moduleInfo.getPhoneAccountComponentName()); 464 } 465 canPlaceDuoCall()466 private boolean canPlaceDuoCall() { 467 Duo duo = DuoComponent.get(context).getDuo(); 468 469 return duo.isInstalled(context) 470 && duo.isEnabled(context) 471 && duo.isActivated(context) 472 && duo.isReachable(context, moduleInfo.getNormalizedNumber()); 473 } 474 475 /** 476 * Lookup URIs are currently fetched from the cached column of the system call log. This URI 477 * contains encoded information for non-contacts for the purposes of populating contact cards. 478 * 479 * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or 480 * not. 481 * 482 * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the 483 * cached column in the system database, in case we decide not to overload the column. 484 */ isExistingContact()485 private boolean isExistingContact() { 486 return !TextUtils.isEmpty(moduleInfo.getLookupUri()) 487 && !UriUtils.isEncodedContactUri(Uri.parse(moduleInfo.getLookupUri())); 488 } 489 490 /** 491 * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link 492 * CallInitiationType.Type}, which is required by {@link CallIntentBuilder} to build a call 493 * intent. 494 */ getCallInitiationType()495 private CallInitiationType.Type getCallInitiationType() { 496 switch (moduleInfo.getHost()) { 497 case CALL_LOG: 498 return CallInitiationType.Type.CALL_LOG; 499 case VOICEMAIL: 500 return CallInitiationType.Type.VOICEMAIL_LOG; 501 default: 502 throw Assert.createUnsupportedOperationFailException( 503 String.format("Unsupported host: %s", moduleInfo.getHost())); 504 } 505 } 506 507 /** 508 * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link 509 * ReportingLocation.Type}, which is for logging where a spam number is reported. 510 */ getReportingLocation()511 private ReportingLocation.Type getReportingLocation() { 512 switch (moduleInfo.getHost()) { 513 case CALL_LOG: 514 return ReportingLocation.Type.CALL_LOG_HISTORY; 515 case VOICEMAIL: 516 return ReportingLocation.Type.VOICEMAIL_HISTORY; 517 default: 518 throw Assert.createUnsupportedOperationFailException( 519 String.format("Unsupported host: %s", moduleInfo.getHost())); 520 } 521 } 522 523 /** Returns a list of impressions to be logged for the given {@link Event events}. */ getImpressions(@vent int... events)524 private ImmutableList<DialerImpression.Type> getImpressions(@Event int... events) { 525 Assert.isNotNull(events); 526 527 ImmutableList.Builder<DialerImpression.Type> impressionListBuilder = 528 new ImmutableList.Builder<>(); 529 for (@Event int event : events) { 530 getImpression(event).ifPresent(impressionListBuilder::add); 531 } 532 533 return impressionListBuilder.build(); 534 } 535 536 /** 537 * Returns an impression to be logged for the given {@link Event}, or {@link Optional#empty()} if 538 * no impression is available for the event. 539 */ getImpression(@vent int event)540 private Optional<DialerImpression.Type> getImpression(@Event int event) { 541 switch (moduleInfo.getHost()) { 542 case CALL_LOG: 543 return Optional.of(CALL_LOG_IMPRESSIONS.get(event)); 544 case VOICEMAIL: 545 // TODO(a bug): Return proper impressions for voicemail. 546 return Optional.empty(); 547 default: 548 throw Assert.createUnsupportedOperationFailException( 549 String.format("Unsupported host: %s", moduleInfo.getHost())); 550 } 551 } 552 } 553