• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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 package com.android.dialer.interactions;
17 
18 import android.app.Activity;
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.app.DialogFragment;
22 import android.app.FragmentManager;
23 import android.content.Context;
24 import android.content.CursorLoader;
25 import android.content.DialogInterface;
26 import android.content.DialogInterface.OnDismissListener;
27 import android.content.Intent;
28 import android.content.Loader;
29 import android.content.Loader.OnLoadCompleteListener;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Parcel;
34 import android.os.Parcelable;
35 import android.provider.ContactsContract.CommonDataKinds.Phone;
36 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37 import android.provider.ContactsContract.Contacts;
38 import android.provider.ContactsContract.Data;
39 import android.provider.ContactsContract.RawContacts;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.widget.ArrayAdapter;
44 import android.widget.CheckBox;
45 import android.widget.ListAdapter;
46 import android.widget.TextView;
47 
48 import com.android.contacts.common.Collapser;
49 import com.android.contacts.common.Collapser.Collapsible;
50 import com.android.contacts.common.MoreContactUtils;
51 import com.android.contacts.common.util.ContactDisplayUtils;
52 import com.android.dialer.R;
53 import com.android.dialer.TransactionSafeActivity;
54 import com.android.dialer.contact.ContactUpdateService;
55 import com.android.dialer.util.IntentUtil;
56 import com.android.dialer.util.IntentUtil.CallIntentBuilder;
57 import com.android.incallui.Call.LogState;
58 import com.android.dialer.util.DialerUtils;
59 
60 import com.google.common.annotations.VisibleForTesting;
61 
62 import java.util.ArrayList;
63 import java.util.List;
64 
65 /**
66  * Initiates phone calls or a text message. If there are multiple candidates, this class shows a
67  * dialog to pick one. Creating one of these interactions should be done through the static
68  * factory methods.
69  *
70  * Note that this class initiates not only usual *phone* calls but also *SIP* calls.
71  *
72  * TODO: clean up code and documents since it is quite confusing to use "phone numbers" or
73  *        "phone calls" here while they can be SIP addresses or SIP calls (See also issue 5039627).
74  */
75 public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
76     private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
77 
78     /**
79      * A model object for capturing a phone number for a given contact.
80      */
81     @VisibleForTesting
82     /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> {
83         long id;
84         String phoneNumber;
85         String accountType;
86         String dataSet;
87         long type;
88         String label;
89         /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */
90         String mimeType;
91 
PhoneItem()92         public PhoneItem() {
93         }
94 
PhoneItem(Parcel in)95         private PhoneItem(Parcel in) {
96             this.id          = in.readLong();
97             this.phoneNumber = in.readString();
98             this.accountType = in.readString();
99             this.dataSet     = in.readString();
100             this.type        = in.readLong();
101             this.label       = in.readString();
102             this.mimeType    = in.readString();
103         }
104 
105         @Override
writeToParcel(Parcel dest, int flags)106         public void writeToParcel(Parcel dest, int flags) {
107             dest.writeLong(id);
108             dest.writeString(phoneNumber);
109             dest.writeString(accountType);
110             dest.writeString(dataSet);
111             dest.writeLong(type);
112             dest.writeString(label);
113             dest.writeString(mimeType);
114         }
115 
116         @Override
describeContents()117         public int describeContents() {
118             return 0;
119         }
120 
121         @Override
collapseWith(PhoneItem phoneItem)122         public void collapseWith(PhoneItem phoneItem) {
123             // Just keep the number and id we already have.
124         }
125 
126         @Override
shouldCollapseWith(PhoneItem phoneItem, Context context)127         public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) {
128             return MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE, phoneNumber,
129                     Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
130         }
131 
132         @Override
toString()133         public String toString() {
134             return phoneNumber;
135         }
136 
137         public static final Parcelable.Creator<PhoneItem> CREATOR
138                 = new Parcelable.Creator<PhoneItem>() {
139             @Override
140             public PhoneItem createFromParcel(Parcel in) {
141                 return new PhoneItem(in);
142             }
143 
144             @Override
145             public PhoneItem[] newArray(int size) {
146                 return new PhoneItem[size];
147             }
148         };
149     }
150 
151     /**
152      * A list adapter that populates the list of contact's phone numbers.
153      */
154     private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> {
155         private final int mInteractionType;
156 
PhoneItemAdapter(Context context, List<PhoneItem> list, int interactionType)157         public PhoneItemAdapter(Context context, List<PhoneItem> list,
158                 int interactionType) {
159             super(context, R.layout.phone_disambig_item, android.R.id.text2, list);
160             mInteractionType = interactionType;
161         }
162 
163         @Override
getView(int position, View convertView, ViewGroup parent)164         public View getView(int position, View convertView, ViewGroup parent) {
165             final View view = super.getView(position, convertView, parent);
166 
167             final PhoneItem item = getItem(position);
168             final TextView typeView = (TextView) view.findViewById(android.R.id.text1);
169             CharSequence value = ContactDisplayUtils.getLabelForCallOrSms((int) item.type,
170                     item.label, mInteractionType, getContext());
171 
172             typeView.setText(value);
173             return view;
174         }
175     }
176 
177     /**
178      * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which
179      * one will be chosen to make a call or initiate an sms message.
180      *
181      * It is recommended to use
182      * {@link PhoneNumberInteraction#startInteractionForPhoneCall(TransactionSafeActivity, Uri)} or
183      * {@link PhoneNumberInteraction#startInteractionForTextMessage(TransactionSafeActivity, Uri)}
184      * instead of directly using this class, as those methods handle one or multiple data cases
185      * appropriately.
186      */
187     /* Made public to let the system reach this class */
188     public static class PhoneDisambiguationDialogFragment extends DialogFragment
189             implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
190 
191         private static final String ARG_PHONE_LIST = "phoneList";
192         private static final String ARG_INTERACTION_TYPE = "interactionType";
193         private static final String ARG_CALL_INITIATION_TYPE = "callInitiation";
194         private static final String ARG_IS_VIDEO_CALL = "is_video_call";
195 
196         private int mInteractionType;
197         private ListAdapter mPhonesAdapter;
198         private List<PhoneItem> mPhoneList;
199         private int mCallInitiationType;
200         private boolean mIsVideoCall;
201 
show(FragmentManager fragmentManager, ArrayList<PhoneItem> phoneList, int interactionType, boolean isVideoCall, int callInitiationType)202         public static void show(FragmentManager fragmentManager, ArrayList<PhoneItem> phoneList,
203                 int interactionType, boolean isVideoCall, int callInitiationType) {
204             PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
205             Bundle bundle = new Bundle();
206             bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
207             bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
208             bundle.putInt(ARG_CALL_INITIATION_TYPE, callInitiationType);
209             bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
210             fragment.setArguments(bundle);
211             fragment.show(fragmentManager, TAG);
212         }
213 
PhoneDisambiguationDialogFragment()214         public PhoneDisambiguationDialogFragment() {
215             super();
216         }
217 
218         @Override
onCreateDialog(Bundle savedInstanceState)219         public Dialog onCreateDialog(Bundle savedInstanceState) {
220             final Activity activity = getActivity();
221             mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
222             mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE);
223             mCallInitiationType = getArguments().getInt(ARG_CALL_INITIATION_TYPE);
224             mIsVideoCall = getArguments().getBoolean(ARG_IS_VIDEO_CALL);
225 
226             mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
227             final LayoutInflater inflater = activity.getLayoutInflater();
228             final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
229             return new AlertDialog.Builder(activity)
230                     .setAdapter(mPhonesAdapter, this)
231                     .setTitle(mInteractionType == ContactDisplayUtils.INTERACTION_SMS
232                             ? R.string.sms_disambig_title : R.string.call_disambig_title)
233                     .setView(setPrimaryView)
234                     .create();
235         }
236 
237         @Override
onClick(DialogInterface dialog, int which)238         public void onClick(DialogInterface dialog, int which) {
239             final Activity activity = getActivity();
240             if (activity == null) return;
241             final AlertDialog alertDialog = (AlertDialog)dialog;
242             if (mPhoneList.size() > which && which >= 0) {
243                 final PhoneItem phoneItem = mPhoneList.get(which);
244                 final CheckBox checkBox = (CheckBox)alertDialog.findViewById(R.id.setPrimary);
245                 if (checkBox.isChecked()) {
246                     // Request to mark the data as primary in the background.
247                     final Intent serviceIntent = ContactUpdateService.createSetSuperPrimaryIntent(
248                             activity, phoneItem.id);
249                     activity.startService(serviceIntent);
250                 }
251 
252                 PhoneNumberInteraction.performAction(activity, phoneItem.phoneNumber,
253                         mInteractionType, mIsVideoCall, mCallInitiationType);
254             } else {
255                 dialog.dismiss();
256             }
257         }
258     }
259 
260     private static final String[] PHONE_NUMBER_PROJECTION = new String[] {
261             Phone._ID,                      // 0
262             Phone.NUMBER,                   // 1
263             Phone.IS_SUPER_PRIMARY,         // 2
264             RawContacts.ACCOUNT_TYPE,       // 3
265             RawContacts.DATA_SET,           // 4
266             Phone.TYPE,                     // 5
267             Phone.LABEL,                    // 6
268             Phone.MIMETYPE,                 // 7
269             Phone.CONTACT_ID                // 8
270     };
271 
272     private static final int _ID = 0;
273     private static final int NUMBER = 1;
274     private static final int IS_SUPER_PRIMARY = 2;
275     private static final int ACCOUNT_TYPE = 3;
276     private static final int DATA_SET = 4;
277     private static final int TYPE = 5;
278     private static final int LABEL = 6;
279     private static final int MIMETYPE = 7;
280     private static final int CONTACT_ID = 8;
281 
282     private static final String PHONE_NUMBER_SELECTION =
283             Data.MIMETYPE + " IN ('"
284                 + Phone.CONTENT_ITEM_TYPE + "', "
285                 + "'" + SipAddress.CONTENT_ITEM_TYPE + "') AND "
286                 + Data.DATA1 + " NOT NULL";
287 
288     private final Context mContext;
289     private final OnDismissListener mDismissListener;
290     private final int mInteractionType;
291 
292     private final int mCallInitiationType;
293     private boolean mUseDefault;
294 
295     private static final int UNKNOWN_CONTACT_ID = -1;
296     private long mContactId = UNKNOWN_CONTACT_ID;
297 
298     private CursorLoader mLoader;
299     private boolean mIsVideoCall;
300 
301     /**
302      * Constructs a new {@link PhoneNumberInteraction}. The constructor takes in a {@link Context}
303      * instead of a {@link TransactionSafeActivity} for testing purposes to verify the functionality
304      * of this class. However, all factory methods for creating {@link PhoneNumberInteraction}s
305      * require a {@link TransactionSafeActivity} (i.e. see {@link #startInteractionForPhoneCall}).
306      */
307     @VisibleForTesting
PhoneNumberInteraction(Context context, int interactionType, DialogInterface.OnDismissListener dismissListener)308     /* package */ PhoneNumberInteraction(Context context, int interactionType,
309             DialogInterface.OnDismissListener dismissListener) {
310         this(context, interactionType, dismissListener, false /*isVideoCall*/,
311                 LogState.INITIATION_UNKNOWN);
312     }
313 
PhoneNumberInteraction(Context context, int interactionType, DialogInterface.OnDismissListener dismissListener, boolean isVideoCall, int callInitiationType)314     private PhoneNumberInteraction(Context context, int interactionType,
315             DialogInterface.OnDismissListener dismissListener, boolean isVideoCall,
316             int callInitiationType) {
317         mContext = context;
318         mInteractionType = interactionType;
319         mDismissListener = dismissListener;
320         mCallInitiationType = callInitiationType;
321         mIsVideoCall = isVideoCall;
322     }
323 
performAction(String phoneNumber)324     private void performAction(String phoneNumber) {
325         PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mIsVideoCall,
326                 mCallInitiationType);
327     }
328 
performAction( Context context, String phoneNumber, int interactionType, boolean isVideoCall, int callInitiationType)329     private static void performAction(
330             Context context, String phoneNumber, int interactionType,
331             boolean isVideoCall, int callInitiationType) {
332         Intent intent;
333         switch (interactionType) {
334             case ContactDisplayUtils.INTERACTION_SMS:
335                 intent = new Intent(
336                         Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
337                 break;
338             default:
339                 intent = new CallIntentBuilder(phoneNumber)
340                         .setCallInitiationType(callInitiationType)
341                         .setIsVideoCall(isVideoCall)
342                         .build();
343                 break;
344         }
345         DialerUtils.startActivityWithErrorToast(context, intent);
346     }
347 
348     /**
349      * Initiates the interaction. This may result in a phone call or sms message started
350      * or a disambiguation dialog to determine which phone number should be used. If there
351      * is a primary phone number, it will be automatically used and a disambiguation dialog
352      * will no be shown.
353      */
354     @VisibleForTesting
startInteraction(Uri uri)355     /* package */ void startInteraction(Uri uri) {
356         startInteraction(uri, true);
357     }
358 
359     /**
360      * Initiates the interaction to result in either a phone call or sms message for a contact.
361      * @param uri Contact Uri
362      * @param useDefault Whether or not to use the primary(default) phone number. If true, the
363      * primary phone number will always be used by default if one is available. If false, a
364      * disambiguation dialog will be shown regardless of whether or not a primary phone number
365      * is available.
366      */
367     @VisibleForTesting
startInteraction(Uri uri, boolean useDefault)368     /* package */ void startInteraction(Uri uri, boolean useDefault) {
369         if (mLoader != null) {
370             mLoader.reset();
371         }
372         mUseDefault = useDefault;
373         final Uri queryUri;
374         final String inputUriAsString = uri.toString();
375         if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
376             if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
377                 queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
378             } else {
379                 queryUri = uri;
380             }
381         } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
382             queryUri = uri;
383         } else {
384             throw new UnsupportedOperationException(
385                     "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
386         }
387 
388         mLoader = new CursorLoader(mContext,
389                 queryUri,
390                 PHONE_NUMBER_PROJECTION,
391                 PHONE_NUMBER_SELECTION,
392                 null,
393                 null);
394         mLoader.registerListener(0, this);
395         mLoader.startLoading();
396     }
397 
398     @Override
onLoadComplete(Loader<Cursor> loader, Cursor cursor)399     public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
400         if (cursor == null) {
401             onDismiss();
402             return;
403         }
404         try {
405             ArrayList<PhoneItem> phoneList = new ArrayList<PhoneItem>();
406             String primaryPhone = null;
407             if (!isSafeToCommitTransactions()) {
408                 onDismiss();
409                 return;
410             }
411             while (cursor.moveToNext()) {
412                 if (mContactId == UNKNOWN_CONTACT_ID) {
413                     mContactId = cursor.getLong(CONTACT_ID);
414                 }
415 
416                 if (mUseDefault && cursor.getInt(IS_SUPER_PRIMARY) != 0) {
417                     // Found super primary, call it.
418                     primaryPhone = cursor.getString(NUMBER);
419                 }
420 
421                 PhoneItem item = new PhoneItem();
422                 item.id = cursor.getLong(_ID);
423                 item.phoneNumber = cursor.getString(NUMBER);
424                 item.accountType = cursor.getString(ACCOUNT_TYPE);
425                 item.dataSet = cursor.getString(DATA_SET);
426                 item.type = cursor.getInt(TYPE);
427                 item.label = cursor.getString(LABEL);
428                 item.mimeType = cursor.getString(MIMETYPE);
429 
430                 phoneList.add(item);
431             }
432 
433             if (mUseDefault && primaryPhone != null) {
434                 performAction(primaryPhone);
435                 onDismiss();
436                 return;
437             }
438 
439             Collapser.collapseList(phoneList, mContext);
440             if (phoneList.size() == 0) {
441                 onDismiss();
442             } else if (phoneList.size() == 1) {
443                 PhoneItem item = phoneList.get(0);
444                 onDismiss();
445                 performAction(item.phoneNumber);
446             } else {
447                 // There are multiple candidates. Let the user choose one.
448                 showDisambiguationDialog(phoneList);
449             }
450         } finally {
451             cursor.close();
452         }
453     }
454 
isSafeToCommitTransactions()455     private boolean isSafeToCommitTransactions() {
456         return mContext instanceof TransactionSafeActivity ?
457                 ((TransactionSafeActivity) mContext).isSafeToCommitTransactions() : true;
458     }
459 
onDismiss()460     private void onDismiss() {
461         if (mDismissListener != null) {
462             mDismissListener.onDismiss(null);
463         }
464     }
465 
466     /**
467      * @param activity that is calling this interaction. This must be of type
468      * {@link TransactionSafeActivity} because we need to check on the activity state after the
469      * phone numbers have been queried for.
470      * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
471      * @param callInitiationType Indicates the UI affordance that was used to initiate the call.
472      */
startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri, boolean isVideoCall, int callInitiationType)473     public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
474             boolean isVideoCall, int callInitiationType) {
475         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null,
476                 isVideoCall, callInitiationType)).startInteraction(uri, true);
477     }
478 
479     /**
480      * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple
481      * candidates for the phone call, dialog is automatically shown and the user is asked to choose
482      * one.
483      *
484      * @param activity that is calling this interaction. This must be of type
485      * {@link TransactionSafeActivity} because we need to check on the activity state after the
486      * phone numbers have been queried for.
487      * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
488      * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
489      * data Uri won't.
490      */
startInteractionForTextMessage(TransactionSafeActivity activity, Uri uri)491     public static void startInteractionForTextMessage(TransactionSafeActivity activity, Uri uri) {
492         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_SMS, null))
493                 .startInteraction(uri, true);
494     }
495 
496     @VisibleForTesting
getLoader()497     /* package */ CursorLoader getLoader() {
498         return mLoader;
499     }
500 
501     @VisibleForTesting
showDisambiguationDialog(ArrayList<PhoneItem> phoneList)502     /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
503         final Activity activity = (Activity) mContext;
504         if (activity.isDestroyed()) {
505             // Check whether the activity is still running
506             return;
507         }
508         try {
509             PhoneDisambiguationDialogFragment.show(activity.getFragmentManager(),
510                     phoneList, mInteractionType, mIsVideoCall, mCallInitiationType);
511         } catch (IllegalStateException e) {
512             // ignore to be safe. Shouldn't happen because we checked the
513             // activity wasn't destroyed, but to be safe.
514         }
515     }
516 }
517