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