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