1 /* 2 * Copyright 2018 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 androidx.fragment.app; 17 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 18 import android.annotation.SuppressLint; 19 import android.app.Activity; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.os.Bundle; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.Window; 30 import android.view.WindowManager; 31 import androidx.annotation.IntDef; 32 import androidx.annotation.MainThread; 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.RestrictTo; 36 import androidx.annotation.StyleRes; 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 /** 40 * Static library support version of the framework's {@link android.app.DialogFragment}. 41 * Used to write apps that run on platforms prior to Android 3.0. When running 42 * on Android 3.0 or above, this implementation is still used; it does not try 43 * to switch to the framework's implementation. See the framework SDK 44 * documentation for a class overview. 45 */ 46 public class DialogFragment extends Fragment 47 implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { 48 /** @hide */ 49 @RestrictTo(LIBRARY_GROUP_PREFIX) 50 @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) 51 @Retention(RetentionPolicy.SOURCE) 52 private @interface DialogStyle {} 53 /** 54 * Style for {@link #setStyle(int, int)}: a basic, 55 * normal dialog. 56 */ 57 public static final int STYLE_NORMAL = 0; 58 /** 59 * Style for {@link #setStyle(int, int)}: don't include 60 * a title area. 61 */ 62 public static final int STYLE_NO_TITLE = 1; 63 /** 64 * Style for {@link #setStyle(int, int)}: don't draw 65 * any frame at all; the view hierarchy returned by {@link #onCreateView} 66 * is entirely responsible for drawing the dialog. 67 */ 68 public static final int STYLE_NO_FRAME = 2; 69 /** 70 * Style for {@link #setStyle(int, int)}: like 71 * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. 72 * The user can not touch it, and its window will not receive input focus. 73 */ 74 public static final int STYLE_NO_INPUT = 3; 75 private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; 76 private static final String SAVED_STYLE = "android:style"; 77 private static final String SAVED_THEME = "android:theme"; 78 private static final String SAVED_CANCELABLE = "android:cancelable"; 79 private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; 80 private static final String SAVED_BACK_STACK_ID = "android:backStackId"; 81 private Handler mHandler; 82 private Runnable mDismissRunnable = new Runnable() { 83 @SuppressLint("SyntheticAccessor") 84 @Override 85 public void run() { 86 mOnDismissListener.onDismiss(mDialog); 87 } 88 }; 89 private DialogInterface.OnCancelListener mOnCancelListener = 90 new DialogInterface.OnCancelListener() { 91 @SuppressLint("SyntheticAccessor") 92 @Override 93 public void onCancel(@Nullable DialogInterface dialog) { 94 if (mDialog != null) { 95 DialogFragment.this.onCancel(mDialog); 96 } 97 } 98 }; 99 private DialogInterface.OnDismissListener mOnDismissListener = 100 new DialogInterface.OnDismissListener() { 101 @SuppressLint("SyntheticAccessor") 102 @Override 103 public void onDismiss(@Nullable DialogInterface dialog) { 104 if (mDialog != null) { 105 DialogFragment.this.onDismiss(mDialog); 106 } 107 } 108 }; 109 private int mStyle = STYLE_NORMAL; 110 private int mTheme = 0; 111 private boolean mCancelable = true; 112 private boolean mShowsDialog = true; 113 private int mBackStackId = -1; 114 private boolean mCreatingDialog; 115 @Nullable 116 private Dialog mDialog; 117 private boolean mViewDestroyed; 118 private boolean mDismissed; 119 private boolean mShownByMe; DialogFragment()120 public DialogFragment() { 121 } 122 /** 123 * Call to customize the basic appearance and behavior of the 124 * fragment's dialog. This can be used for some common dialog behaviors, 125 * taking care of selecting flags, theme, and other options for you. The 126 * same effect can be achieve by manually setting Dialog and Window 127 * attributes yourself. Calling this after the fragment's Dialog is 128 * created will have no effect. 129 * 130 * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, 131 * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or 132 * {@link #STYLE_NO_INPUT}. 133 * @param theme Optional custom theme. If 0, an appropriate theme (based 134 * on the style) will be selected for you. 135 */ setStyle(@ialogStyle int style, @StyleRes int theme)136 public void setStyle(@DialogStyle int style, @StyleRes int theme) { 137 mStyle = style; 138 if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { 139 mTheme = android.R.style.Theme_Panel; 140 } 141 if (theme != 0) { 142 mTheme = theme; 143 } 144 } 145 /** 146 * Display the dialog, adding the fragment to the given FragmentManager. This 147 * is a convenience for explicitly creating a transaction, adding the 148 * fragment to it with the given tag, and {@link FragmentTransaction#commit() committing} it. 149 * This does <em>not</em> add the transaction to the fragment back stack. When the fragment 150 * is dismissed, a new transaction will be executed to remove it from 151 * the activity. 152 * @param manager The FragmentManager this fragment will be added to. 153 * @param tag The tag for this fragment, as per 154 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. 155 */ show(@onNull FragmentManager manager, @Nullable String tag)156 public void show(@NonNull FragmentManager manager, @Nullable String tag) { 157 mDismissed = false; 158 mShownByMe = true; 159 FragmentTransaction ft = manager.beginTransaction(); 160 ft.add(this, tag); 161 ft.commit(); 162 } 163 /** 164 * Display the dialog, adding the fragment using an existing transaction 165 * and then {@link FragmentTransaction#commit() committing} the transaction. 166 * @param transaction An existing transaction in which to add the fragment. 167 * @param tag The tag for this fragment, as per 168 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. 169 * @return Returns the identifier of the committed transaction, as per 170 * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. 171 */ show(@onNull FragmentTransaction transaction, @Nullable String tag)172 public int show(@NonNull FragmentTransaction transaction, @Nullable String tag) { 173 mDismissed = false; 174 mShownByMe = true; 175 transaction.add(this, tag); 176 mViewDestroyed = false; 177 mBackStackId = transaction.commit(); 178 return mBackStackId; 179 } 180 /** 181 * Display the dialog, immediately adding the fragment to the given FragmentManager. This 182 * is a convenience for explicitly creating a transaction, adding the 183 * fragment to it with the given tag, and calling {@link FragmentTransaction#commitNow()}. 184 * This does <em>not</em> add the transaction to the fragment back stack. When the fragment 185 * is dismissed, a new transaction will be executed to remove it from 186 * the activity. 187 * @param manager The FragmentManager this fragment will be added to. 188 * @param tag The tag for this fragment, as per 189 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. 190 */ showNow(@onNull FragmentManager manager, @Nullable String tag)191 public void showNow(@NonNull FragmentManager manager, @Nullable String tag) { 192 mDismissed = false; 193 mShownByMe = true; 194 FragmentTransaction ft = manager.beginTransaction(); 195 ft.add(this, tag); 196 ft.commitNow(); 197 } 198 /** 199 * Dismiss the fragment and its dialog. If the fragment was added to the 200 * back stack, all back stack state up to and including this entry will 201 * be popped. Otherwise, a new transaction will be committed to remove 202 * the fragment. 203 */ dismiss()204 public void dismiss() { 205 dismissInternal(false, false); 206 } 207 /** 208 * Version of {@link #dismiss()} that uses 209 * {@link FragmentTransaction#commitAllowingStateLoss() 210 * FragmentTransaction.commitAllowingStateLoss()}. See linked 211 * documentation for further details. 212 */ dismissAllowingStateLoss()213 public void dismissAllowingStateLoss() { 214 dismissInternal(true, false); 215 } dismissInternal(boolean allowStateLoss, boolean fromOnDismiss)216 private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) { 217 if (mDismissed) { 218 return; 219 } 220 mDismissed = true; 221 mShownByMe = false; 222 if (mDialog != null) { 223 // Instead of waiting for a posted onDismiss(), null out 224 // the listener and call onDismiss() manually to ensure 225 // that the callback happens before onDestroy() 226 mDialog.setOnDismissListener(null); 227 mDialog.dismiss(); 228 if (!fromOnDismiss) { 229 // onDismiss() is always called on the main thread, so 230 // we mimic that behavior here. The difference here is that 231 // we don't post the message to ensure that the onDismiss() 232 // callback still happens before onDestroy() 233 if (Looper.myLooper() == mHandler.getLooper()) { 234 onDismiss(mDialog); 235 } else { 236 mHandler.post(mDismissRunnable); 237 } 238 } 239 } 240 mViewDestroyed = true; 241 if (mBackStackId >= 0) { 242 getParentFragmentManager().popBackStack(mBackStackId, 243 FragmentManager.POP_BACK_STACK_INCLUSIVE); 244 mBackStackId = -1; 245 } else { 246 FragmentTransaction ft = getParentFragmentManager().beginTransaction(); 247 ft.remove(this); 248 if (allowStateLoss) { 249 ft.commitAllowingStateLoss(); 250 } else { 251 ft.commit(); 252 } 253 } 254 } 255 /** 256 * Return the {@link Dialog} this fragment is currently controlling. 257 * 258 * @see #requireDialog() 259 */ 260 @Nullable getDialog()261 public Dialog getDialog() { 262 return mDialog; 263 } 264 /** 265 * Return the {@link Dialog} this fragment is currently controlling. 266 * 267 * @throws IllegalStateException if the Dialog has not yet been created (before 268 * {@link #onCreateDialog(Bundle)}) or has been destroyed (after {@link #onDestroyView()}. 269 * @see #getDialog() 270 */ 271 @NonNull requireDialog()272 public final Dialog requireDialog() { 273 Dialog dialog = getDialog(); 274 if (dialog == null) { 275 throw new IllegalStateException("DialogFragment " + this + " does not have a Dialog."); 276 } 277 return dialog; 278 } 279 @StyleRes getTheme()280 public int getTheme() { 281 return mTheme; 282 } 283 /** 284 * Control whether the shown Dialog is cancelable. Use this instead of 285 * directly calling {@link Dialog#setCancelable(boolean) 286 * Dialog.setCancelable(boolean)}, because DialogFragment needs to change 287 * its behavior based on this. 288 * 289 * @param cancelable If true, the dialog is cancelable. The default 290 * is true. 291 */ setCancelable(boolean cancelable)292 public void setCancelable(boolean cancelable) { 293 mCancelable = cancelable; 294 if (mDialog != null) mDialog.setCancelable(cancelable); 295 } 296 /** 297 * Return the current value of {@link #setCancelable(boolean)}. 298 */ isCancelable()299 public boolean isCancelable() { 300 return mCancelable; 301 } 302 /** 303 * Controls whether this fragment should be shown in a dialog. If not 304 * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, 305 * and the fragment's view hierarchy will thus not be added to it. This 306 * allows you to instead use it as a normal fragment (embedded inside of 307 * its activity). 308 * 309 * <p>This is normally set for you based on whether the fragment is 310 * associated with a container view ID passed to 311 * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. 312 * If the fragment was added with a container, setShowsDialog will be 313 * initialized to false; otherwise, it will be true. 314 * 315 * @param showsDialog If true, the fragment will be displayed in a Dialog. 316 * If false, no Dialog will be created and the fragment's view hierarchy 317 * left undisturbed. 318 */ setShowsDialog(boolean showsDialog)319 public void setShowsDialog(boolean showsDialog) { 320 mShowsDialog = showsDialog; 321 } 322 /** 323 * Return the current value of {@link #setShowsDialog(boolean)}. 324 */ getShowsDialog()325 public boolean getShowsDialog() { 326 return mShowsDialog; 327 } 328 @MainThread 329 @Override onAttach(@onNull Context context)330 public void onAttach(@NonNull Context context) { 331 super.onAttach(context); 332 if (!mShownByMe) { 333 // If not explicitly shown through our API, take this as an 334 // indication that the dialog is no longer dismissed. 335 mDismissed = false; 336 } 337 } 338 @MainThread 339 @Override onDetach()340 public void onDetach() { 341 super.onDetach(); 342 if (!mShownByMe && !mDismissed) { 343 // The fragment was not shown by a direct call here, it is not 344 // dismissed, and now it is being detached... well, okay, thou 345 // art now dismissed. Have fun. 346 mDismissed = true; 347 } 348 } 349 @MainThread 350 @Override onCreate(@ullable Bundle savedInstanceState)351 public void onCreate(@Nullable Bundle savedInstanceState) { 352 super.onCreate(savedInstanceState); 353 // This assumes that onCreate() is being called on the main thread 354 mHandler = new Handler(); 355 mShowsDialog = mContainerId == 0; 356 if (savedInstanceState != null) { 357 mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); 358 mTheme = savedInstanceState.getInt(SAVED_THEME, 0); 359 mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); 360 mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); 361 mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); 362 } 363 } 364 /** 365 * {@inheritDoc} 366 * 367 * <p> 368 * If this is called from within {@link #onCreateDialog(Bundle)}, the layout inflater from 369 * {@link Fragment#onGetLayoutInflater(Bundle)}, without the dialog theme, will be returned. 370 */ 371 @Override 372 @NonNull onGetLayoutInflater(@ullable Bundle savedInstanceState)373 public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) { 374 LayoutInflater layoutInflater = super.onGetLayoutInflater(savedInstanceState); 375 if (!mShowsDialog || mCreatingDialog) { 376 return layoutInflater; 377 } 378 try { 379 mCreatingDialog = true; 380 mDialog = onCreateDialog(savedInstanceState); 381 setupDialog(mDialog, mStyle); 382 } finally { 383 mCreatingDialog = false; 384 } 385 return layoutInflater.cloneInContext(requireDialog().getContext()); 386 } 387 /** @hide */ 388 @RestrictTo(LIBRARY_GROUP_PREFIX) setupDialog(@onNull Dialog dialog, int style)389 public void setupDialog(@NonNull Dialog dialog, int style) { 390 switch (style) { 391 case STYLE_NO_INPUT: 392 Window window = dialog.getWindow(); 393 if (window != null) { 394 window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 395 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); 396 } 397 // fall through... 398 case STYLE_NO_FRAME: 399 case STYLE_NO_TITLE: 400 dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 401 } 402 } 403 /** 404 * Override to build your own custom Dialog container. This is typically 405 * used to show an AlertDialog instead of a generic Dialog; when doing so, 406 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need 407 * to be implemented since the AlertDialog takes care of its own content. 408 * 409 * <p>This method will be called after {@link #onCreate(Bundle)} and 410 * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The 411 * default implementation simply instantiates and returns a {@link Dialog} 412 * class. 413 * 414 * <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener 415 * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener 416 * Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em> 417 * To find out about these events, override {@link #onCancel(DialogInterface)} 418 * and {@link #onDismiss(DialogInterface)}.</p> 419 * 420 * @param savedInstanceState The last saved instance state of the Fragment, 421 * or null if this is a freshly created Fragment. 422 * 423 * @return Return a new Dialog instance to be displayed by the Fragment. 424 */ 425 @MainThread 426 @NonNull onCreateDialog(@ullable Bundle savedInstanceState)427 public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 428 return new Dialog(requireContext(), getTheme()); 429 } 430 @Override onCancel(@onNull DialogInterface dialog)431 public void onCancel(@NonNull DialogInterface dialog) { 432 } 433 @Override onDismiss(@onNull DialogInterface dialog)434 public void onDismiss(@NonNull DialogInterface dialog) { 435 if (!mViewDestroyed) { 436 // Note: we need to use allowStateLoss, because the dialog 437 // dispatches this asynchronously so we can receive the call 438 // after the activity is paused. Worst case, when the user comes 439 // back to the activity they see the dialog again. 440 dismissInternal(true, true); 441 } 442 } 443 @MainThread 444 @Override onActivityCreated(@ullable Bundle savedInstanceState)445 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 446 super.onActivityCreated(savedInstanceState); 447 if (!mShowsDialog) { 448 return; 449 } 450 View view = getView(); 451 if (mDialog != null) { 452 if (view != null) { 453 if (view.getParent() != null) { 454 throw new IllegalStateException( 455 "DialogFragment can not be attached to a container view"); 456 } 457 mDialog.setContentView(view); 458 } 459 final Context context = getContext(); 460 if (context instanceof Activity) { 461 mDialog.setOwnerActivity((Activity) context); 462 } 463 mDialog.setCancelable(mCancelable); 464 mDialog.setOnCancelListener(mOnCancelListener); 465 mDialog.setOnDismissListener(mOnDismissListener); 466 if (savedInstanceState != null) { 467 Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); 468 if (dialogState != null) { 469 mDialog.onRestoreInstanceState(dialogState); 470 } 471 } 472 } 473 } 474 @MainThread 475 @Override onStart()476 public void onStart() { 477 super.onStart(); 478 if (mDialog != null) { 479 mViewDestroyed = false; 480 mDialog.show(); 481 } 482 } 483 @MainThread 484 @Override onSaveInstanceState(@onNull Bundle outState)485 public void onSaveInstanceState(@NonNull Bundle outState) { 486 super.onSaveInstanceState(outState); 487 if (mDialog != null) { 488 Bundle dialogState = mDialog.onSaveInstanceState(); 489 outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); 490 } 491 if (mStyle != STYLE_NORMAL) { 492 outState.putInt(SAVED_STYLE, mStyle); 493 } 494 if (mTheme != 0) { 495 outState.putInt(SAVED_THEME, mTheme); 496 } 497 if (!mCancelable) { 498 outState.putBoolean(SAVED_CANCELABLE, mCancelable); 499 } 500 if (!mShowsDialog) { 501 outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); 502 } 503 if (mBackStackId != -1) { 504 outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); 505 } 506 } 507 @MainThread 508 @Override onStop()509 public void onStop() { 510 super.onStop(); 511 if (mDialog != null) { 512 mDialog.hide(); 513 } 514 } 515 /** 516 * Remove dialog. 517 */ 518 @MainThread 519 @Override onDestroyView()520 public void onDestroyView() { 521 super.onDestroyView(); 522 if (mDialog != null) { 523 // Set removed here because this dismissal is just to hide 524 // the dialog -- we don't want this to cause the fragment to 525 // actually be removed. 526 mViewDestroyed = true; 527 // Instead of waiting for a posted onDismiss(), null out 528 // the listener and call onDismiss() manually to ensure 529 // that the callback happens before onDestroy() 530 mDialog.setOnDismissListener(null); 531 mDialog.dismiss(); 532 if (!mDismissed) { 533 // Don't send a second onDismiss() callback if we've already 534 // dismissed the dialog manually in dismissInternal() 535 onDismiss(mDialog); 536 } 537 mDialog = null; 538 } 539 } 540 } 541