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