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