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