summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java
blob: a09f6e10b9250e010389a24354ff1125b5efeee9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.historyitemactions;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.text.TextUtils;
import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.clipboard.ClipboardUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.duo.Duo;
import com.android.dialer.duo.DuoComponent;
import com.android.dialer.logging.ReportingLocation;
import com.android.dialer.spam.Spam;
import com.android.dialer.util.CallUtil;
import com.android.dialer.util.UriUtils;
import java.util.ArrayList;
import java.util.List;

/**
 * Builds a list of {@link HistoryItemActionModule HistoryItemActionModules}.
 *
 * <p>Example usage:
 *
 * <pre><code>
 *    // Create a HistoryItemActionModuleInfo proto with the information you have.
 *    // You can simply skip a field if there is no information for it.
 *    HistoryItemActionModuleInfo moduleInfo =
 *        HistoryItemActionModuleInfo.newBuilder()
 *            .setNormalizedNumber("+16502530000")
 *            .setCountryIso("US")
 *            .setName("Google")
 *            .build();
 *
 *    // Initialize the builder using the module info above.
 *    // Note that some modules require an activity context to work so it is preferred to pass one
 *    // instead of an application context to the builder.
 *    HistoryItemActionModulesBuilder modulesBuilder =
 *        new HistoryItemActionModulesBuilder(activityContext, moduleInfo);
 *
 *    // Add all modules you want in the order you like.
 *    // If a module shouldn't be added according to the module info, it won't be.
 *    // For example, if the module info is not for a video call and doesn't indicate the presence
 *    // of video calling capabilities, calling addModuleForVideoCall() is a no-op.
 *    modulesBuilder
 *        .addModuleForVoiceCall()
 *        .addModuleForVideoCall()
 *        .addModuleForSendingTextMessage()
 *        .addModuleForDivider()
 *        .addModuleForAddingToContacts()
 *        .addModuleForBlockedOrSpamNumber()
 *        .addModuleForCopyingNumber();
 *
 *    List<HistoryItemActionModule> modules = modulesBuilder.build();
 * </code></pre>
 */
public final class HistoryItemActionModulesBuilder {

  private final Context context;
  private final HistoryItemActionModuleInfo moduleInfo;
  private final List<HistoryItemActionModule> modules;

  public HistoryItemActionModulesBuilder(Context context, HistoryItemActionModuleInfo moduleInfo) {
    Assert.checkArgument(
        moduleInfo.getHost() != HistoryItemActionModuleInfo.Host.UNKNOWN,
        "A host must be specified.");

    this.context = context;
    this.moduleInfo = moduleInfo;
    this.modules = new ArrayList<>();
  }

  public List<HistoryItemActionModule> build() {
    return new ArrayList<>(modules);
  }

  /**
   * Adds a module for placing a voice call.
   *
   * <p>The method is a no-op if the number is blocked.
   */
  public HistoryItemActionModulesBuilder addModuleForVoiceCall() {
    if (moduleInfo.getIsBlocked()) {
      return this;
    }

    // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber.
    // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to
    // place or receive the call should be ignored for voice calls.
    CallIntentBuilder callIntentBuilder =
        new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType())
            .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing());
    modules.add(IntentModule.newCallModule(context, callIntentBuilder));
    return this;
  }

  /**
   * Adds a module for a carrier video call *or* a Duo video call.
   *
   * <p>This method is a no-op if
   *
   * <ul>
   *   <li>the call is one made to/received from an emergency number,
   *   <li>the call is one made to a voicemail box,
   *   <li>the call should be shown as spam, or
   *   <li>the number is blocked.
   * </ul>
   *
   * <p>If the provided module info is for a Duo video call and Duo is available, add a Duo video
   * call module.
   *
   * <p>If the provided module info is for a Duo video call but Duo is unavailable, add a carrier
   * video call module.
   *
   * <p>If the provided module info is for a carrier video call, add a carrier video call module.
   *
   * <p>If the provided module info is for a voice call and the device has carrier video call
   * capability, add a carrier video call module.
   *
   * <p>If the provided module info is for a voice call, the device doesn't have carrier video call
   * capability, and Duo is available, add a Duo video call module.
   */
  public HistoryItemActionModulesBuilder addModuleForVideoCall() {
    if (moduleInfo.getIsEmergencyNumber()
        || moduleInfo.getIsVoicemailCall()
        || Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType())
        || moduleInfo.getIsBlocked()) {
      return this;
    }

    // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to
    // place or receive the call should be ignored for carrier video calls.
    // TODO(a bug): figure out the correct video call behavior
    CallIntentBuilder callIntentBuilder =
        new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType())
            .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing())
            .setIsVideoCall(true);

    // If the module info is for a video call, add an appropriate video call module.
    if ((moduleInfo.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
      modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsDuoCall(isDuoCall())));
      return this;
    }

    // At this point, the module info is for an audio call. We will also add a video call module if
    // the video capability is present.
    //
    // The carrier video call module takes precedence over the Duo module.
    if (canPlaceCarrierVideoCall()) {
      modules.add(IntentModule.newCallModule(context, callIntentBuilder));
    } else if (canPlaceDuoCall()) {
      modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsDuoCall(true)));
    }
    return this;
  }

  /**
   * Adds a module for sending text messages.
   *
   * <p>The method is a no-op if
   *
   * <ul>
   *   <li>the call is one made to/received from an emergency number,
   *   <li>the call is one made to a voicemail box,
   *   <li>the number is blocked, or
   *   <li>the number is empty.
   * </ul>
   */
  public HistoryItemActionModulesBuilder addModuleForSendingTextMessage() {
    // TODO(zachh): There are other conditions where this module should not be shown
    // (e.g., business numbers).
    if (moduleInfo.getIsEmergencyNumber()
        || moduleInfo.getIsVoicemailCall()
        || moduleInfo.getIsBlocked()
        || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
      return this;
    }

    modules.add(
        IntentModule.newModuleForSendingTextMessage(context, moduleInfo.getNormalizedNumber()));
    return this;
  }

  /**
   * Adds a module for a divider.
   *
   * <p>The method is a no-op if the divider module will be the first module.
   */
  public HistoryItemActionModulesBuilder addModuleForDivider() {
    if (modules.isEmpty()) {
      return this;
    }

    modules.add(new DividerModule());
    return this;
  }

  /**
   * Adds a module for adding a number to Contacts.
   *
   * <p>The method is a no-op if
   *
   * <ul>
   *   <li>the call is one made to/received from an emergency number,
   *   <li>the call is one made to a voicemail box,
   *   <li>the call should be shown as spam,
   *   <li>the number is blocked,
   *   <li>the number is empty, or
   *   <li>the number belongs to an existing contact.
   * </ul>
   */
  public HistoryItemActionModulesBuilder addModuleForAddingToContacts() {
    if (moduleInfo.getIsEmergencyNumber()
        || moduleInfo.getIsVoicemailCall()
        || Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType())
        || moduleInfo.getIsBlocked()
        || isExistingContact()
        || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
      return this;
    }

    Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
    intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
    intent.putExtra(ContactsContract.Intents.Insert.PHONE, moduleInfo.getNormalizedNumber());

    if (!TextUtils.isEmpty(moduleInfo.getName())) {
      intent.putExtra(ContactsContract.Intents.Insert.NAME, moduleInfo.getName());
    }

    modules.add(
        new IntentModule(
            context,
            intent,
            R.string.add_to_contacts,
            R.drawable.quantum_ic_person_add_vd_theme_24));
    return this;
  }

  /**
   * Add modules for blocking/unblocking a number and/or marking it as spam/not spam.
   *
   * <p>The method is a no-op if
   *
   * <ul>
   *   <li>the call is one made to/received from an emergency number, or
   *   <li>the call is one made to a voicemail box.
   * </ul>
   *
   * <p>If the call should be shown as spam, add two modules:
   *
   * <ul>
   *   <li>"Not spam" and "Block", or
   *   <li>"Not spam" and "Unblock".
   * </ul>
   *
   * <p>If the number is blocked but the call should not be shown as spam, add the "Unblock" module.
   *
   * <p>If the number is not blocked and the call should not be shown as spam, add the "Block/Report
   * spam" module.
   */
  public HistoryItemActionModulesBuilder addModuleForBlockedOrSpamNumber() {
    if (moduleInfo.getIsEmergencyNumber() || moduleInfo.getIsVoicemailCall()) {
      return this;
    }

    BlockReportSpamDialogInfo blockReportSpamDialogInfo =
        BlockReportSpamDialogInfo.newBuilder()
            .setNormalizedNumber(moduleInfo.getNormalizedNumber())
            .setCountryIso(moduleInfo.getCountryIso())
            .setCallType(moduleInfo.getCallType())
            .setReportingLocation(getReportingLocation())
            .setContactSource(moduleInfo.getContactSource())
            .build();

    // For a call that should be shown as spam, add two modules:
    // (1) "Not spam" and "Block", or
    // (2) "Not spam" and "Unblock".
    if (Spam.shouldShowAsSpam(moduleInfo.getIsSpam(), moduleInfo.getCallType())) {
      modules.add(
          BlockReportSpamModules.moduleForMarkingNumberAsNotSpam(
              context, blockReportSpamDialogInfo));
      modules.add(
          moduleInfo.getIsBlocked()
              ? BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo)
              : BlockReportSpamModules.moduleForBlockingNumber(context, blockReportSpamDialogInfo));
      return this;
    }

    // For a blocked number associated with a call that should not be shown as spam, add the
    // "Unblock" module.
    if (moduleInfo.getIsBlocked()) {
      modules.add(
          BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo));
      return this;
    }

    // For a number that is not blocked and is associated with a call that should not be shown as
    // spam, add the "Block/Report spam" module.
    modules.add(
        BlockReportSpamModules.moduleForBlockingNumberAndOptionallyReportingSpam(
            context, blockReportSpamDialogInfo));
    return this;
  }

  /**
   * Adds a module for copying a number.
   *
   * <p>The method is a no-op if the number is empty.
   */
  public HistoryItemActionModulesBuilder addModuleForCopyingNumber() {
    if (TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) {
      return this;
    }

    modules.add(
        new HistoryItemActionModule() {
          @Override
          public int getStringId() {
            return R.string.copy_number;
          }

          @Override
          public int getDrawableId() {
            return R.drawable.quantum_ic_content_copy_vd_theme_24;
          }

          @Override
          public boolean onClick() {
            ClipboardUtils.copyText(
                context,
                /* label = */ null,
                moduleInfo.getNormalizedNumber(),
                /* showToast = */ true);
            return false;
          }
        });
    return this;
  }

  private boolean canPlaceCarrierVideoCall() {
    int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context);
    boolean isCarrierVideoCallingEnabled =
        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED)
            == CallUtil.VIDEO_CALLING_ENABLED);
    boolean canRelyOnCarrierVideoPresence =
        ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE)
            == CallUtil.VIDEO_CALLING_PRESENCE);

    return isCarrierVideoCallingEnabled
        && canRelyOnCarrierVideoPresence
        && moduleInfo.getCanSupportCarrierVideoCall();
  }

  private boolean isDuoCall() {
    return DuoComponent.get(context)
        .getDuo()
        .isDuoAccount(moduleInfo.getPhoneAccountComponentName());
  }

  private boolean canPlaceDuoCall() {
    Duo duo = DuoComponent.get(context).getDuo();

    return duo.isInstalled(context)
        && duo.isEnabled(context)
        && duo.isActivated(context)
        && duo.isReachable(context, moduleInfo.getNormalizedNumber());
  }

  /**
   * Lookup URIs are currently fetched from the cached column of the system call log. This URI
   * contains encoded information for non-contacts for the purposes of populating contact cards.
   *
   * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or
   * not.
   *
   * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the
   * cached column in the system database, in case we decide not to overload the column.
   */
  private boolean isExistingContact() {
    return !TextUtils.isEmpty(moduleInfo.getLookupUri())
        && !UriUtils.isEncodedContactUri(Uri.parse(moduleInfo.getLookupUri()));
  }

  /**
   * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link
   * CallInitiationType.Type}, which is required by {@link CallIntentBuilder} to build a call
   * intent.
   */
  private CallInitiationType.Type getCallInitiationType() {
    switch (moduleInfo.getHost()) {
      case CALL_LOG:
        return CallInitiationType.Type.CALL_LOG;
      case VOICEMAIL:
        return CallInitiationType.Type.VOICEMAIL_LOG;
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported host: %s", moduleInfo.getHost()));
    }
  }

  /**
   * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link
   * ReportingLocation.Type}, which is for logging where a spam number is reported.
   */
  private ReportingLocation.Type getReportingLocation() {
    switch (moduleInfo.getHost()) {
      case CALL_LOG:
        return ReportingLocation.Type.CALL_LOG_HISTORY;
      case VOICEMAIL:
        return ReportingLocation.Type.VOICEMAIL_HISTORY;
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported host: %s", moduleInfo.getHost()));
    }
  }
}