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.common.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 = 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 */ 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