Home | History | Annotate | Download | only in interactions
      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.CallUtil;
     49 import com.android.contacts.common.Collapser;
     50 import com.android.contacts.common.Collapser.Collapsible;
     51 import com.android.contacts.common.MoreContactUtils;
     52 import com.android.contacts.common.util.ContactDisplayUtils;
     53 import com.android.dialer.R;
     54 import com.android.dialer.activity.TransactionSafeActivity;
     55 import com.android.dialer.contact.ContactUpdateService;
     56 import com.android.dialer.util.DialerUtils;
     57 
     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 
     90         public PhoneItem() {
     91         }
     92 
     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
    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
    115         public int describeContents() {
    116             return 0;
    117         }
    118 
    119         @Override
    120         public void collapseWith(PhoneItem phoneItem) {
    121             // Just keep the number and id we already have.
    122         }
    123 
    124         @Override
    125         public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) {
    126             return MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE, phoneNumber,
    127                     Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
    128         }
    129 
    130         @Override
    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 
    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
    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 
    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
    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
    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
    298     /* package */ PhoneNumberInteraction(Context context, int interactionType,
    299             DialogInterface.OnDismissListener dismissListener) {
    300         this(context, interactionType, dismissListener, null);
    301     }
    302 
    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 
    311     private void performAction(String phoneNumber) {
    312         PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mCallOrigin);
    313     }
    314 
    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         DialerUtils.startActivityWithErrorToast(context, 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
    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
    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
    382     public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
    383         if (cursor == null) {
    384             onDismiss();
    385             return;
    386         }
    387         try {
    388             ArrayList<PhoneItem> phoneList = new ArrayList<PhoneItem>();
    389             String primaryPhone = null;
    390             if (!isSafeToCommitTransactions()) {
    391                 onDismiss();
    392                 return;
    393             }
    394             while (cursor.moveToNext()) {
    395                 if (mContactId == UNKNOWN_CONTACT_ID) {
    396                     mContactId = cursor.getLong(CONTACT_ID);
    397                 }
    398 
    399                 if (mUseDefault && cursor.getInt(IS_SUPER_PRIMARY) != 0) {
    400                     // Found super primary, call it.
    401                     primaryPhone = cursor.getString(NUMBER);
    402                 }
    403 
    404                 PhoneItem item = new PhoneItem();
    405                 item.id = cursor.getLong(_ID);
    406                 item.phoneNumber = cursor.getString(NUMBER);
    407                 item.accountType = cursor.getString(ACCOUNT_TYPE);
    408                 item.dataSet = cursor.getString(DATA_SET);
    409                 item.type = cursor.getInt(TYPE);
    410                 item.label = cursor.getString(LABEL);
    411                 item.mimeType = cursor.getString(MIMETYPE);
    412 
    413                 phoneList.add(item);
    414             }
    415 
    416             if (mUseDefault && primaryPhone != null) {
    417                 performAction(primaryPhone);
    418                 onDismiss();
    419                 return;
    420             }
    421 
    422             Collapser.collapseList(phoneList, mContext);
    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         } finally {
    434             cursor.close();
    435         }
    436     }
    437 
    438     private boolean isSafeToCommitTransactions() {
    439         return mContext instanceof TransactionSafeActivity ?
    440                 ((TransactionSafeActivity) mContext).isSafeToCommitTransactions() : true;
    441     }
    442 
    443     private void onDismiss() {
    444         if (mDismissListener != null) {
    445             mDismissListener.onDismiss(null);
    446         }
    447     }
    448 
    449     /**
    450      * Start call action using given contact Uri. If there are multiple candidates for the phone
    451      * call, dialog is automatically shown and the user is asked to choose one.
    452      *
    453      * @param activity that is calling this interaction. This must be of type
    454      * {@link TransactionSafeActivity} because we need to check on the activity state after the
    455      * phone numbers have been queried for.
    456      * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
    457      * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
    458      * data Uri won't.
    459      */
    460     public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri) {
    461         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null))
    462                 .startInteraction(uri, true);
    463     }
    464 
    465     /**
    466      * Start call action using given contact Uri. If there are multiple candidates for the phone
    467      * call, dialog is automatically shown and the user is asked to choose one.
    468      *
    469      * @param activity that is calling this interaction. This must be of type
    470      * {@link TransactionSafeActivity} because we need to check on the activity state after the
    471      * phone numbers have been queried for.
    472      * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
    473      * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
    474      * data Uri won't.
    475      * @param useDefault Whether or not to use the primary(default) phone number. If true, the
    476      * primary phone number will always be used by default if one is available. If false, a
    477      * disambiguation dialog will be shown regardless of whether or not a primary phone number
    478      * is available.
    479      */
    480     public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
    481             boolean useDefault) {
    482         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null))
    483                 .startInteraction(uri, useDefault);
    484     }
    485 
    486     /**
    487      * @param activity that is calling this interaction. This must be of type
    488      * {@link TransactionSafeActivity} because we need to check on the activity state after the
    489      * phone numbers have been queried for.
    490      * @param callOrigin If non null, {@link PhoneConstants#EXTRA_CALL_ORIGIN} will be
    491      * appended to the Intent initiating phone call. See comments in Phone package (PhoneApp)
    492      * for more detail.
    493      */
    494     public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
    495             String callOrigin) {
    496         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null, callOrigin))
    497                 .startInteraction(uri, true);
    498     }
    499 
    500     /**
    501      * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple
    502      * candidates for the phone call, dialog is automatically shown and the user is asked to choose
    503      * one.
    504      *
    505      * @param activity that is calling this interaction. This must be of type
    506      * {@link TransactionSafeActivity} because we need to check on the activity state after the
    507      * phone numbers have been queried for.
    508      * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
    509      * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
    510      * data Uri won't.
    511      */
    512     public static void startInteractionForTextMessage(TransactionSafeActivity activity, Uri uri) {
    513         (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_SMS, null))
    514                 .startInteraction(uri, true);
    515     }
    516 
    517     @VisibleForTesting
    518     /* package */ CursorLoader getLoader() {
    519         return mLoader;
    520     }
    521 
    522     @VisibleForTesting
    523     /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
    524         PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(),
    525                 phoneList, mInteractionType, mCallOrigin);
    526     }
    527 }
    528