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