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.Manifest.permission;
19 import android.annotation.SuppressLint;
20 import android.app.Activity;
21 import android.app.AlertDialog;
22 import android.app.Dialog;
23 import android.app.DialogFragment;
24 import android.app.FragmentManager;
25 import android.content.Context;
26 import android.content.CursorLoader;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.Loader;
30 import android.content.Loader.OnLoadCompleteListener;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Parcel;
35 import android.os.Parcelable;
36 import android.provider.ContactsContract.CommonDataKinds.Phone;
37 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
38 import android.provider.ContactsContract.Contacts;
39 import android.provider.ContactsContract.Data;
40 import android.provider.ContactsContract.RawContacts;
41 import android.support.annotation.IntDef;
42 import android.support.annotation.VisibleForTesting;
43 import android.support.v4.app.ActivityCompat;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.widget.ArrayAdapter;
48 import android.widget.CheckBox;
49 import android.widget.ListAdapter;
50 import android.widget.TextView;
51 import com.android.contacts.common.Collapser;
52 import com.android.contacts.common.Collapser.Collapsible;
53 import com.android.contacts.common.MoreContactUtils;
54 import com.android.contacts.common.util.ContactDisplayUtils;
55 import com.android.dialer.callintent.CallInitiationType;
56 import com.android.dialer.callintent.CallIntentBuilder;
57 import com.android.dialer.callintent.CallIntentParser;
58 import com.android.dialer.callintent.CallSpecificAppData;
59 import com.android.dialer.common.Assert;
60 import com.android.dialer.common.LogUtil;
61 import com.android.dialer.logging.InteractionEvent;
62 import com.android.dialer.logging.Logger;
63 import com.android.dialer.precall.PreCall;
64 import com.android.dialer.util.DialerUtils;
65 import com.android.dialer.util.PermissionsUtil;
66 import com.android.dialer.util.TransactionSafeActivity;
67 import java.lang.annotation.Retention;
68 import java.lang.annotation.RetentionPolicy;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.List;
72 
73 /**
74  * Initiates phone calls or a text message. If there are multiple candidates, this class shows a
75  * dialog to pick one. Creating one of these interactions should be done through the static factory
76  * methods.
77  *
78  * <p>Note that this class initiates not only usual *phone* calls but also *SIP* calls.
79  *
80  * <p>TODO: clean up code and documents since it is quite confusing to use "phone numbers" or "phone
81  * calls" here while they can be SIP addresses or SIP calls (See also issue 5039627).
82  */
83 public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
84 
85   static final String TAG = PhoneNumberInteraction.class.getSimpleName();
86   /** The identifier for a permissions request if one is generated. */
87   public static final int REQUEST_READ_CONTACTS = 1;
88 
89   public static final int REQUEST_CALL_PHONE = 2;
90 
91   @VisibleForTesting
92   public static final String[] PHONE_NUMBER_PROJECTION =
93       new String[] {
94         Phone._ID,
95         Phone.NUMBER,
96         Phone.IS_SUPER_PRIMARY,
97         RawContacts.ACCOUNT_TYPE,
98         RawContacts.DATA_SET,
99         Phone.TYPE,
100         Phone.LABEL,
101         Phone.MIMETYPE,
102         Phone.CONTACT_ID,
103       };
104 
105   private static final String PHONE_NUMBER_SELECTION =
106       Data.MIMETYPE
107           + " IN ('"
108           + Phone.CONTENT_ITEM_TYPE
109           + "', "
110           + "'"
111           + SipAddress.CONTENT_ITEM_TYPE
112           + "') AND "
113           + Data.DATA1
114           + " NOT NULL";
115   private static final int UNKNOWN_CONTACT_ID = -1;
116   private final Context context;
117   private final int interactionType;
118   private final CallSpecificAppData callSpecificAppData;
119   private long contactId = UNKNOWN_CONTACT_ID;
120   private CursorLoader loader;
121   private boolean isVideoCall;
122 
123   /** Error codes for interactions. */
124   @Retention(RetentionPolicy.SOURCE)
125   @IntDef(
126     value = {
127       InteractionErrorCode.CONTACT_NOT_FOUND,
128       InteractionErrorCode.CONTACT_HAS_NO_NUMBER,
129       InteractionErrorCode.USER_LEAVING_ACTIVITY,
130       InteractionErrorCode.OTHER_ERROR
131     }
132   )
133   public @interface InteractionErrorCode {
134 
135     int CONTACT_NOT_FOUND = 1;
136     int CONTACT_HAS_NO_NUMBER = 2;
137     int OTHER_ERROR = 3;
138     int USER_LEAVING_ACTIVITY = 4;
139   }
140 
141   /**
142    * Activities which use this class must implement this. They will be notified if there was an
143    * error performing the interaction. For example, this callback will be invoked on the activity if
144    * the contact URI provided points to a deleted contact, or to a contact without a phone number.
145    */
146   public interface InteractionErrorListener {
147 
interactionError(@nteractionErrorCode int interactionErrorCode)148     void interactionError(@InteractionErrorCode int interactionErrorCode);
149   }
150 
151   /**
152    * Activities which use this class must implement this. They will be notified if the phone number
153    * disambiguation dialog is dismissed.
154    */
155   public interface DisambigDialogDismissedListener {
onDisambigDialogDismissed()156     void onDisambigDialogDismissed();
157   }
158 
PhoneNumberInteraction( Context context, int interactionType, boolean isVideoCall, CallSpecificAppData callSpecificAppData)159   private PhoneNumberInteraction(
160       Context context,
161       int interactionType,
162       boolean isVideoCall,
163       CallSpecificAppData callSpecificAppData) {
164     this.context = context;
165     this.interactionType = interactionType;
166     this.callSpecificAppData = callSpecificAppData;
167     this.isVideoCall = isVideoCall;
168 
169     Assert.checkArgument(context instanceof InteractionErrorListener);
170     Assert.checkArgument(context instanceof DisambigDialogDismissedListener);
171     Assert.checkArgument(context instanceof ActivityCompat.OnRequestPermissionsResultCallback);
172   }
173 
performAction( Context context, String phoneNumber, int interactionType, boolean isVideoCall, CallSpecificAppData callSpecificAppData)174   private static void performAction(
175       Context context,
176       String phoneNumber,
177       int interactionType,
178       boolean isVideoCall,
179       CallSpecificAppData callSpecificAppData) {
180     Intent intent;
181     switch (interactionType) {
182       case ContactDisplayUtils.INTERACTION_SMS:
183         intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
184         break;
185       default:
186         intent =
187             PreCall.getIntent(
188                 context,
189                 new CallIntentBuilder(phoneNumber, callSpecificAppData)
190                     .setIsVideoCall(isVideoCall)
191                     .setAllowAssistedDial(callSpecificAppData.getAllowAssistedDialing()));
192         break;
193     }
194     DialerUtils.startActivityWithErrorToast(context, intent);
195   }
196 
197   /**
198    * @param activity that is calling this interaction. This must be of type {@link
199    *     TransactionSafeActivity} because we need to check on the activity state after the phone
200    *     numbers have been queried for. The activity must implement {@link InteractionErrorListener}
201    *     and {@link DisambigDialogDismissedListener}.
202    * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
203    */
startInteractionForPhoneCall( TransactionSafeActivity activity, Uri uri, boolean isVideoCall, CallSpecificAppData callSpecificAppData)204   public static void startInteractionForPhoneCall(
205       TransactionSafeActivity activity,
206       Uri uri,
207       boolean isVideoCall,
208       CallSpecificAppData callSpecificAppData) {
209     new PhoneNumberInteraction(
210             activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData)
211         .startInteraction(uri);
212   }
213 
performAction(String phoneNumber)214   private void performAction(String phoneNumber) {
215     PhoneNumberInteraction.performAction(
216         context, phoneNumber, interactionType, isVideoCall, callSpecificAppData);
217   }
218 
219   /**
220    * Initiates the interaction to result in either a phone call or sms message for a contact.
221    *
222    * @param uri Contact Uri
223    */
startInteraction(Uri uri)224   private void startInteraction(Uri uri) {
225     // It's possible for a shortcut to have been created, and then permissions revoked. To avoid a
226     // crash when the user tries to use such a shortcut, check for this condition and ask the user
227     // for the permission.
228     if (!PermissionsUtil.hasPhonePermissions(context)) {
229       LogUtil.i("PhoneNumberInteraction.startInteraction", "Need phone permission: CALL_PHONE");
230       ActivityCompat.requestPermissions(
231           (Activity) context, new String[] {permission.CALL_PHONE}, REQUEST_CALL_PHONE);
232       return;
233     }
234 
235     String[] deniedContactsPermissions =
236         PermissionsUtil.getPermissionsCurrentlyDenied(
237             context, PermissionsUtil.allContactsGroupPermissionsUsedInDialer);
238     if (deniedContactsPermissions.length > 0) {
239       LogUtil.i(
240           "PhoneNumberInteraction.startInteraction",
241           "Need contact permissions: " + Arrays.toString(deniedContactsPermissions));
242       ActivityCompat.requestPermissions(
243           (Activity) context, deniedContactsPermissions, REQUEST_READ_CONTACTS);
244       return;
245     }
246 
247     if (loader != null) {
248       loader.reset();
249     }
250     final Uri queryUri;
251     final String inputUriAsString = uri.toString();
252     if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
253       if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
254         queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
255       } else {
256         queryUri = uri;
257       }
258     } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
259       queryUri = uri;
260     } else {
261       throw new UnsupportedOperationException(
262           "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
263     }
264 
265     loader =
266         new CursorLoader(
267             context, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null);
268     loader.registerListener(0, this);
269     loader.startLoading();
270   }
271 
272   @Override
onLoadComplete(Loader<Cursor> loader, Cursor cursor)273   public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
274     if (cursor == null) {
275       LogUtil.i("PhoneNumberInteraction.onLoadComplete", "null cursor");
276       interactionError(InteractionErrorCode.OTHER_ERROR);
277       return;
278     }
279     try {
280       ArrayList<PhoneItem> phoneList = new ArrayList<>();
281       String primaryPhone = null;
282       if (!isSafeToCommitTransactions()) {
283         LogUtil.i("PhoneNumberInteraction.onLoadComplete", "not safe to commit transaction");
284         interactionError(InteractionErrorCode.USER_LEAVING_ACTIVITY);
285         return;
286       }
287       if (cursor.moveToFirst()) {
288         int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID);
289         int isSuperPrimaryColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY);
290         int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER);
291         int phoneIdColumn = cursor.getColumnIndexOrThrow(Phone._ID);
292         int accountTypeColumn = cursor.getColumnIndexOrThrow(RawContacts.ACCOUNT_TYPE);
293         int dataSetColumn = cursor.getColumnIndexOrThrow(RawContacts.DATA_SET);
294         int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE);
295         int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL);
296         int phoneMimeTpeColumn = cursor.getColumnIndexOrThrow(Phone.MIMETYPE);
297         do {
298           if (contactId == UNKNOWN_CONTACT_ID) {
299             contactId = cursor.getLong(contactIdColumn);
300           }
301 
302           if (cursor.getInt(isSuperPrimaryColumn) != 0) {
303             // Found super primary, call it.
304             primaryPhone = cursor.getString(phoneNumberColumn);
305           }
306 
307           PhoneItem item = new PhoneItem();
308           item.id = cursor.getLong(phoneIdColumn);
309           item.phoneNumber = cursor.getString(phoneNumberColumn);
310           item.accountType = cursor.getString(accountTypeColumn);
311           item.dataSet = cursor.getString(dataSetColumn);
312           item.type = cursor.getInt(phoneTypeColumn);
313           item.label = cursor.getString(phoneLabelColumn);
314           item.mimeType = cursor.getString(phoneMimeTpeColumn);
315 
316           phoneList.add(item);
317         } while (cursor.moveToNext());
318       } else {
319         interactionError(InteractionErrorCode.CONTACT_NOT_FOUND);
320         return;
321       }
322 
323       if (primaryPhone != null) {
324         performAction(primaryPhone);
325         return;
326       }
327 
328       Collapser.collapseList(phoneList, context);
329       if (phoneList.size() == 0) {
330         interactionError(InteractionErrorCode.CONTACT_HAS_NO_NUMBER);
331       } else if (phoneList.size() == 1) {
332         PhoneItem item = phoneList.get(0);
333         performAction(item.phoneNumber);
334       } else {
335         // There are multiple candidates. Let the user choose one.
336         showDisambiguationDialog(phoneList);
337       }
338     } finally {
339       cursor.close();
340     }
341   }
342 
interactionError(@nteractionErrorCode int interactionErrorCode)343   private void interactionError(@InteractionErrorCode int interactionErrorCode) {
344     // mContext is really the activity -- see ctor docs.
345     ((InteractionErrorListener) context).interactionError(interactionErrorCode);
346   }
347 
isSafeToCommitTransactions()348   private boolean isSafeToCommitTransactions() {
349     return !(context instanceof TransactionSafeActivity)
350         || ((TransactionSafeActivity) context).isSafeToCommitTransactions();
351   }
352 
353   @VisibleForTesting
getLoader()354   /* package */ CursorLoader getLoader() {
355     return loader;
356   }
357 
showDisambiguationDialog(ArrayList<PhoneItem> phoneList)358   private void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
359     // TODO(a bug): don't leak the activity
360     final Activity activity = (Activity) context;
361     if (activity.isFinishing()) {
362       LogUtil.i("PhoneNumberInteraction.showDisambiguationDialog", "activity finishing");
363       return;
364     }
365 
366     if (activity.isDestroyed()) {
367       // Check whether the activity is still running
368       LogUtil.i("PhoneNumberInteraction.showDisambiguationDialog", "activity destroyed");
369       return;
370     }
371 
372     try {
373       PhoneDisambiguationDialogFragment.show(
374           activity.getFragmentManager(),
375           phoneList,
376           interactionType,
377           isVideoCall,
378           callSpecificAppData);
379     } catch (IllegalStateException e) {
380       // ignore to be safe. Shouldn't happen because we checked the
381       // activity wasn't destroyed, but to be safe.
382       LogUtil.e("PhoneNumberInteraction.showDisambiguationDialog", "caught exception", e);
383     }
384   }
385 
386   /** A model object for capturing a phone number for a given contact. */
387   @VisibleForTesting
388   /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> {
389 
390     public static final Parcelable.Creator<PhoneItem> CREATOR =
391         new Parcelable.Creator<PhoneItem>() {
392           @Override
393           public PhoneItem createFromParcel(Parcel in) {
394             return new PhoneItem(in);
395           }
396 
397           @Override
398           public PhoneItem[] newArray(int size) {
399             return new PhoneItem[size];
400           }
401         };
402     long id;
403     String phoneNumber;
404     String accountType;
405     String dataSet;
406     long type;
407     String label;
408     /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */
409     String mimeType;
410 
PhoneItem()411     private PhoneItem() {}
412 
PhoneItem(Parcel in)413     private PhoneItem(Parcel in) {
414       this.id = in.readLong();
415       this.phoneNumber = in.readString();
416       this.accountType = in.readString();
417       this.dataSet = in.readString();
418       this.type = in.readLong();
419       this.label = in.readString();
420       this.mimeType = in.readString();
421     }
422 
423     @Override
writeToParcel(Parcel dest, int flags)424     public void writeToParcel(Parcel dest, int flags) {
425       dest.writeLong(id);
426       dest.writeString(phoneNumber);
427       dest.writeString(accountType);
428       dest.writeString(dataSet);
429       dest.writeLong(type);
430       dest.writeString(label);
431       dest.writeString(mimeType);
432     }
433 
434     @Override
describeContents()435     public int describeContents() {
436       return 0;
437     }
438 
439     @Override
collapseWith(PhoneItem phoneItem)440     public void collapseWith(PhoneItem phoneItem) {
441       // Just keep the number and id we already have.
442     }
443 
444     @Override
shouldCollapseWith(PhoneItem phoneItem, Context context)445     public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) {
446       return MoreContactUtils.shouldCollapse(
447           Phone.CONTENT_ITEM_TYPE, phoneNumber, Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
448     }
449 
450     @Override
toString()451     public String toString() {
452       return phoneNumber;
453     }
454   }
455 
456   /** A list adapter that populates the list of contact's phone numbers. */
457   private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> {
458 
459     private final int interactionType;
460 
PhoneItemAdapter(Context context, List<PhoneItem> list, int interactionType)461     PhoneItemAdapter(Context context, List<PhoneItem> list, int interactionType) {
462       super(context, R.layout.phone_disambig_item, android.R.id.text2, list);
463       this.interactionType = interactionType;
464     }
465 
466     @Override
getView(int position, View convertView, ViewGroup parent)467     public View getView(int position, View convertView, ViewGroup parent) {
468       final View view = super.getView(position, convertView, parent);
469 
470       final PhoneItem item = getItem(position);
471       Assert.isNotNull(item, "Null item at position: %d", position);
472       final TextView typeView = (TextView) view.findViewById(android.R.id.text1);
473       CharSequence value =
474           ContactDisplayUtils.getLabelForCallOrSms(
475               (int) item.type, item.label, interactionType, getContext());
476 
477       typeView.setText(value);
478       return view;
479     }
480   }
481 
482   /**
483    * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which one
484    * will be chosen to make a call or initiate an sms message.
485    *
486    * <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri,
487    * boolean, CallSpecificAppData)} instead of directly using this class, as those methods handle
488    * one or multiple data cases appropriately.
489    *
490    * <p>This fragment may only be attached to activities which implement {@link
491    * DisambigDialogDismissedListener}.
492    */
493   @SuppressWarnings("WeakerAccess") // Made public to let the system reach this class
494   public static class PhoneDisambiguationDialogFragment extends DialogFragment
495       implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
496 
497     private static final String ARG_PHONE_LIST = "phoneList";
498     private static final String ARG_INTERACTION_TYPE = "interactionType";
499     private static final String ARG_IS_VIDEO_CALL = "is_video_call";
500 
501     private int interactionType;
502     private ListAdapter phonesAdapter;
503     private List<PhoneItem> phoneList;
504     private CallSpecificAppData callSpecificAppData;
505     private boolean isVideoCall;
506 
PhoneDisambiguationDialogFragment()507     public PhoneDisambiguationDialogFragment() {
508       super();
509     }
510 
show( FragmentManager fragmentManager, ArrayList<PhoneItem> phoneList, int interactionType, boolean isVideoCall, CallSpecificAppData callSpecificAppData)511     public static void show(
512         FragmentManager fragmentManager,
513         ArrayList<PhoneItem> phoneList,
514         int interactionType,
515         boolean isVideoCall,
516         CallSpecificAppData callSpecificAppData) {
517       PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
518       Bundle bundle = new Bundle();
519       bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
520       bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
521       bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
522       CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData);
523       fragment.setArguments(bundle);
524       fragment.show(fragmentManager, TAG);
525     }
526 
527     @Override
onCreateDialog(Bundle savedInstanceState)528     public Dialog onCreateDialog(Bundle savedInstanceState) {
529       final Activity activity = getActivity();
530       Assert.checkState(activity instanceof DisambigDialogDismissedListener);
531 
532       phoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
533       interactionType = getArguments().getInt(ARG_INTERACTION_TYPE);
534       isVideoCall = getArguments().getBoolean(ARG_IS_VIDEO_CALL);
535       callSpecificAppData = CallIntentParser.getCallSpecificAppData(getArguments());
536 
537       phonesAdapter = new PhoneItemAdapter(activity, phoneList, interactionType);
538       final LayoutInflater inflater = activity.getLayoutInflater();
539       @SuppressLint("InflateParams") // Allowed since dialog view is not available yet
540       final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
541       return new AlertDialog.Builder(activity)
542           .setAdapter(phonesAdapter, this)
543           .setTitle(
544               interactionType == ContactDisplayUtils.INTERACTION_SMS
545                   ? R.string.sms_disambig_title
546                   : R.string.call_disambig_title)
547           .setView(setPrimaryView)
548           .create();
549     }
550 
551     @Override
onClick(DialogInterface dialog, int which)552     public void onClick(DialogInterface dialog, int which) {
553       final Activity activity = getActivity();
554       if (activity == null) {
555         return;
556       }
557       final AlertDialog alertDialog = (AlertDialog) dialog;
558       if (phoneList.size() > which && which >= 0) {
559         final PhoneItem phoneItem = phoneList.get(which);
560         final CheckBox checkBox = (CheckBox) alertDialog.findViewById(R.id.setPrimary);
561         if (checkBox.isChecked()) {
562           if (callSpecificAppData.getCallInitiationType() == CallInitiationType.Type.SPEED_DIAL) {
563             Logger.get(getContext())
564                 .logInteraction(
565                     InteractionEvent.Type.SPEED_DIAL_SET_DEFAULT_NUMBER_FOR_AMBIGUOUS_CONTACT);
566           }
567 
568           // Request to mark the data as primary in the background.
569           final Intent serviceIntent =
570               ContactUpdateService.createSetSuperPrimaryIntent(activity, phoneItem.id);
571           activity.startService(serviceIntent);
572         }
573 
574         PhoneNumberInteraction.performAction(
575             activity, phoneItem.phoneNumber, interactionType, isVideoCall, callSpecificAppData);
576       } else {
577         dialog.dismiss();
578       }
579     }
580 
581     @Override
onDismiss(DialogInterface dialogInterface)582     public void onDismiss(DialogInterface dialogInterface) {
583       super.onDismiss(dialogInterface);
584       Activity activity = getActivity();
585       if (activity != null) {
586         ((DisambigDialogDismissedListener) activity).onDisambigDialogDismissed();
587       }
588     }
589   }
590 }
591