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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.app.Activity; 22import android.app.Dialog; 23import android.content.Context; 24import android.content.DialogInterface; 25import android.os.Bundle; 26import android.support.annotation.IntDef; 27import android.support.annotation.NonNull; 28import android.support.annotation.Nullable; 29import android.support.annotation.RestrictTo; 30import android.support.annotation.StyleRes; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.Window; 35import android.view.WindowManager; 36 37import java.lang.annotation.Retention; 38import java.lang.annotation.RetentionPolicy; 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(LIBRARY_GROUP) 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 @Override 305 public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) { 306 if (!mShowsDialog) { 307 return super.onGetLayoutInflater(savedInstanceState); 308 } 309 310 mDialog = onCreateDialog(savedInstanceState); 311 312 if (mDialog != null) { 313 setupDialog(mDialog, mStyle); 314 315 return (LayoutInflater) mDialog.getContext().getSystemService( 316 Context.LAYOUT_INFLATER_SERVICE); 317 } 318 return (LayoutInflater) mHost.getContext().getSystemService( 319 Context.LAYOUT_INFLATER_SERVICE); 320 } 321 322 /** @hide */ 323 @RestrictTo(LIBRARY_GROUP) 324 public void setupDialog(Dialog dialog, int style) { 325 switch (style) { 326 case STYLE_NO_INPUT: 327 dialog.getWindow().addFlags( 328 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 329 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); 330 // fall through... 331 case STYLE_NO_FRAME: 332 case STYLE_NO_TITLE: 333 dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 334 } 335 } 336 337 /** 338 * Override to build your own custom Dialog container. This is typically 339 * used to show an AlertDialog instead of a generic Dialog; when doing so, 340 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need 341 * to be implemented since the AlertDialog takes care of its own content. 342 * 343 * <p>This method will be called after {@link #onCreate(Bundle)} and 344 * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The 345 * default implementation simply instantiates and returns a {@link Dialog} 346 * class. 347 * 348 * <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener 349 * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener 350 * Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em> 351 * To find out about these events, override {@link #onCancel(DialogInterface)} 352 * and {@link #onDismiss(DialogInterface)}.</p> 353 * 354 * @param savedInstanceState The last saved instance state of the Fragment, 355 * or null if this is a freshly created Fragment. 356 * 357 * @return Return a new Dialog instance to be displayed by the Fragment. 358 */ 359 @NonNull 360 public Dialog onCreateDialog(Bundle savedInstanceState) { 361 return new Dialog(getActivity(), getTheme()); 362 } 363 364 @Override 365 public void onCancel(DialogInterface dialog) { 366 } 367 368 @Override 369 public void onDismiss(DialogInterface dialog) { 370 if (!mViewDestroyed) { 371 // Note: we need to use allowStateLoss, because the dialog 372 // dispatches this asynchronously so we can receive the call 373 // after the activity is paused. Worst case, when the user comes 374 // back to the activity they see the dialog again. 375 dismissInternal(true); 376 } 377 } 378 379 @Override 380 public void onActivityCreated(Bundle savedInstanceState) { 381 super.onActivityCreated(savedInstanceState); 382 383 if (!mShowsDialog) { 384 return; 385 } 386 387 View view = getView(); 388 if (view != null) { 389 if (view.getParent() != null) { 390 throw new IllegalStateException( 391 "DialogFragment can not be attached to a container view"); 392 } 393 mDialog.setContentView(view); 394 } 395 final Activity activity = getActivity(); 396 if (activity != null) { 397 mDialog.setOwnerActivity(activity); 398 } 399 mDialog.setCancelable(mCancelable); 400 mDialog.setOnCancelListener(this); 401 mDialog.setOnDismissListener(this); 402 if (savedInstanceState != null) { 403 Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); 404 if (dialogState != null) { 405 mDialog.onRestoreInstanceState(dialogState); 406 } 407 } 408 } 409 410 @Override 411 public void onStart() { 412 super.onStart(); 413 414 if (mDialog != null) { 415 mViewDestroyed = false; 416 mDialog.show(); 417 } 418 } 419 420 @Override 421 public void onSaveInstanceState(Bundle outState) { 422 super.onSaveInstanceState(outState); 423 if (mDialog != null) { 424 Bundle dialogState = mDialog.onSaveInstanceState(); 425 if (dialogState != null) { 426 outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); 427 } 428 } 429 if (mStyle != STYLE_NORMAL) { 430 outState.putInt(SAVED_STYLE, mStyle); 431 } 432 if (mTheme != 0) { 433 outState.putInt(SAVED_THEME, mTheme); 434 } 435 if (!mCancelable) { 436 outState.putBoolean(SAVED_CANCELABLE, mCancelable); 437 } 438 if (!mShowsDialog) { 439 outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); 440 } 441 if (mBackStackId != -1) { 442 outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); 443 } 444 } 445 446 @Override 447 public void onStop() { 448 super.onStop(); 449 if (mDialog != null) { 450 mDialog.hide(); 451 } 452 } 453 454 /** 455 * Remove dialog. 456 */ 457 @Override 458 public void onDestroyView() { 459 super.onDestroyView(); 460 if (mDialog != null) { 461 // Set removed here because this dismissal is just to hide 462 // the dialog -- we don't want this to cause the fragment to 463 // actually be removed. 464 mViewDestroyed = true; 465 mDialog.dismiss(); 466 mDialog = null; 467 } 468 } 469} 470