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.Collapser;
49import com.android.contacts.common.Collapser.Collapsible;
50import com.android.contacts.common.MoreContactUtils;
51import com.android.contacts.common.activity.TransactionSafeActivity;
52import com.android.contacts.common.util.ContactDisplayUtils;
53import com.android.dialer.R;
54import com.android.dialer.contact.ContactUpdateService;
55import com.android.dialer.util.IntentUtil;
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, 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 = IntentUtil.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