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 
17 package com.android.contacts.dialog;
18 
19 import android.app.Dialog;
20 import android.app.DialogFragment;
21 import android.app.FragmentManager;
22 import android.app.ProgressDialog;
23 import android.content.DialogInterface;
24 import android.os.Bundle;
25 import 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  */
44 public class IndeterminateProgressDialog extends DialogFragment {
45     private static final String TAG = "IndeterminateProgress";
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      */
show(FragmentManager fragmentManager, CharSequence title, CharSequence message, long minDisplayTime)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
onCreate(Bundle savedInstanceState)82     public void onCreate(Bundle savedInstanceState) {
83         super.onCreate(savedInstanceState);
84         setRetainInstance(true);
85     }
86 
87     @Override
onCreateDialog(Bundle savedInstanceState)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
onStart()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
onStop()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
onDismiss(DialogInterface dialog)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
onDestroyView()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
dismiss()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
dismissAllowingStateLoss()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      */
dismissWhenReady()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      */
superDismiss()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