1/*
2 * Copyright (C) 2012 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 com.android.contacts.common.dialog;
18
19import android.app.Dialog;
20import android.app.DialogFragment;
21import android.app.FragmentManager;
22import android.app.ProgressDialog;
23import android.content.DialogInterface;
24import android.os.Bundle;
25import android.os.Handler;
26
27/**
28 * Indeterminate progress dialog wrapped up in a DialogFragment to work even when the device
29 * orientation is changed. Currently, only supports adding a title and/or message to the progress
30 * dialog.  There is an additional parameter of the minimum amount of time to display the progress
31 * dialog even after a call to dismiss the dialog {@link #dismiss()} or
32 * {@link #dismissAllowingStateLoss()}.
33 * <p>
34 * To create and show the progress dialog, use
35 * {@link #show(FragmentManager, CharSequence, CharSequence, long)} and retain the reference to the
36 * IndeterminateProgressDialog instance.
37 * <p>
38 * To dismiss the dialog, use {@link #dismiss()} or {@link #dismissAllowingStateLoss()} on the
39 * instance.  The instance returned by
40 * {@link #show(FragmentManager, CharSequence, CharSequence, long)} is guaranteed to be valid
41 * after a device orientation change because the {@link #setRetainInstance(boolean)} is called
42 * internally with true.
43 */
44public class IndeterminateProgressDialog extends DialogFragment {
45    private static final String TAG = IndeterminateProgressDialog.class.getSimpleName();
46
47    private CharSequence mTitle;
48    private CharSequence mMessage;
49    private long mMinDisplayTime;
50    private long mShowTime = 0;
51    private boolean mActivityReady = false;
52    private Dialog mOldDialog;
53    private final Handler mHandler = new Handler();
54    private boolean mCalledSuperDismiss = false;
55    private boolean mAllowStateLoss;
56    private final Runnable mDismisser = new Runnable() {
57        @Override
58        public void run() {
59            superDismiss();
60        }
61    };
62
63    /**
64     * Creates and shows an indeterminate progress dialog.  Once the progress dialog is shown, it
65     * will be shown for at least the minDisplayTime (in milliseconds), so that the progress dialog
66     * does not flash in and out to quickly.
67     */
68    public static IndeterminateProgressDialog show(FragmentManager fragmentManager,
69            CharSequence title, CharSequence message, long minDisplayTime) {
70        IndeterminateProgressDialog dialogFragment = new IndeterminateProgressDialog();
71        dialogFragment.mTitle = title;
72        dialogFragment.mMessage = message;
73        dialogFragment.mMinDisplayTime = minDisplayTime;
74        dialogFragment.show(fragmentManager, TAG);
75        dialogFragment.mShowTime = System.currentTimeMillis();
76        dialogFragment.setCancelable(false);
77
78        return dialogFragment;
79    }
80
81    @Override
82    public void onCreate(Bundle savedInstanceState) {
83        super.onCreate(savedInstanceState);
84        setRetainInstance(true);
85    }
86
87    @Override
88    public Dialog onCreateDialog(Bundle savedInstanceState) {
89        // Create the progress dialog and set its properties
90        final ProgressDialog dialog = new ProgressDialog(getActivity());
91        dialog.setIndeterminate(true);
92        dialog.setIndeterminateDrawable(null);
93        dialog.setTitle(mTitle);
94        dialog.setMessage(mMessage);
95
96        return dialog;
97    }
98
99    @Override
100    public void onStart() {
101        super.onStart();
102        mActivityReady = true;
103
104        // Check if superDismiss() had been called before.  This can happen if in a long
105        // running operation, the user hits the home button and closes this fragment's activity.
106        // Upon returning, we want to dismiss this progress dialog fragment.
107        if (mCalledSuperDismiss) {
108            superDismiss();
109        }
110    }
111
112    @Override
113    public void onStop() {
114        super.onStop();
115        mActivityReady = false;
116    }
117
118    /**
119     * There is a race condition that is not handled properly by the DialogFragment class.
120     * If we don't check that this onDismiss callback isn't for the old progress dialog from before
121     * the device orientation change, then this will cause the newly created dialog after the
122     * orientation change to be dismissed immediately.
123     */
124    @Override
125    public void onDismiss(DialogInterface dialog) {
126        if (mOldDialog != null && mOldDialog == dialog) {
127            // This is the callback from the old progress dialog that was already dismissed before
128            // the device orientation change, so just ignore it.
129            return;
130        }
131        super.onDismiss(dialog);
132    }
133
134    /**
135     * Save the old dialog that is about to get destroyed in case this is due to a change
136     * in device orientation.  This will allow us to intercept the callback to
137     * {@link #onDismiss(DialogInterface)} in case the callback happens after a new progress dialog
138     * instance was created.
139     */
140    @Override
141    public void onDestroyView() {
142        mOldDialog = getDialog();
143        super.onDestroyView();
144    }
145
146    /**
147     * This tells the progress dialog to dismiss itself after guaranteeing to be shown for the
148     * specified time in {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
149     */
150    @Override
151    public void dismiss() {
152        mAllowStateLoss = false;
153        dismissWhenReady();
154    }
155
156    /**
157     * This tells the progress dialog to dismiss itself (with state loss) after guaranteeing to be
158     * shown for the specified time in
159     * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
160     */
161    @Override
162    public void dismissAllowingStateLoss() {
163        mAllowStateLoss = true;
164        dismissWhenReady();
165    }
166
167    /**
168     * Tells the progress dialog to dismiss itself after guaranteeing that the dialog had been
169     * showing for at least the minimum display time as set in
170     * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
171     */
172    private void dismissWhenReady() {
173        // Compute how long the dialog has been showing
174        final long shownTime = System.currentTimeMillis() - mShowTime;
175        if (shownTime >= mMinDisplayTime) {
176            // dismiss immediately
177            mHandler.post(mDismisser);
178        } else {
179            // Need to wait some more, so compute the amount of time to sleep.
180            final long sleepTime = mMinDisplayTime - shownTime;
181            mHandler.postDelayed(mDismisser, sleepTime);
182        }
183    }
184
185    /**
186     * Actually dismiss the dialog fragment.
187     */
188    private void superDismiss() {
189        mCalledSuperDismiss = true;
190        if (mActivityReady) {
191            // The fragment is either in onStart or past it, but has not gotten to onStop yet.
192            // It is safe to dismiss this dialog fragment.
193            if (mAllowStateLoss) {
194                super.dismissAllowingStateLoss();
195            } else {
196                super.dismiss();
197            }
198        }
199        // If mActivityReady is false, then this dialog fragment has already passed the onStop
200        // state. This can happen if the user hit the 'home' button before this dialog fragment was
201        // dismissed or if there is a configuration change.
202        // In the event that this dialog fragment is re-attached and reaches onStart (e.g.,
203        // because the user returns to this fragment's activity or the device configuration change
204        // has re-attached this dialog fragment), because the mCalledSuperDismiss flag was set to
205        // true, this dialog fragment will be dismissed within onStart.  So, there's nothing else
206        // that needs to be done.
207    }
208}
209