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