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