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