Home | History | Annotate | Download | only in app
      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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    200     public void dismissAllowingStateLoss() {
    201         dismissInternal(true);
    202     }
    203 
    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 
    229     public Dialog getDialog() {
    230         return mDialog;
    231     }
    232 
    233     @StyleRes
    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      */
    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      */
    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      */
    276     public void setShowsDialog(boolean showsDialog) {
    277         mShowsDialog = showsDialog;
    278     }
    279 
    280     /**
    281      * Return the current value of {@link #setShowsDialog(boolean)}.
    282      */
    283     public boolean getShowsDialog() {
    284         return mShowsDialog;
    285     }
    286 
    287     @Override
    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
    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
    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
    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)
    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
    380     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    381         return new Dialog(getActivity(), getTheme());
    382     }
    383 
    384     @Override
    385     public void onCancel(DialogInterface dialog) {
    386     }
    387 
    388     @Override
    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
    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
    431     public void onStart() {
    432         super.onStart();
    433 
    434         if (mDialog != null) {
    435             mViewDestroyed = false;
    436             mDialog.show();
    437         }
    438     }
    439 
    440     @Override
    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
    467     public void onStop() {
    468         super.onStop();
    469         if (mDialog != null) {
    470             mDialog.hide();
    471         }
    472     }
    473 
    474     /**
    475      * Remove dialog.
    476      */
    477     @Override
    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