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