ProviderStatusWatcher.java revision c2bd6138e19fdcf734843eb55c83d6ffe00e91da
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 */ 16package com.android.contacts.list; 17 18import com.google.common.collect.Lists; 19 20import android.content.ContentValues; 21import android.content.Context; 22import android.database.ContentObserver; 23import android.database.Cursor; 24import android.net.Uri; 25import android.os.AsyncTask; 26import android.os.Handler; 27import android.provider.ContactsContract.ProviderStatus; 28import android.util.Log; 29 30import java.util.ArrayList; 31 32/** 33 * A singleton that keeps track of the last known provider status. 34 * 35 * All methods must be called on the UI thread unless noted otherwise. 36 * 37 * All members must be set on the UI thread unless noted otherwise. 38 */ 39public class ProviderStatusWatcher extends ContentObserver { 40 private static final String TAG = "ProviderStatusWatcher"; 41 private static final boolean DEBUG = false; 42 43 /** 44 * Callback interface invoked when the provider status changes. 45 */ 46 public interface ProviderStatusListener { 47 public void onProviderStatusChange(); 48 } 49 50 private static final String[] PROJECTION = new String[] { 51 ProviderStatus.STATUS, 52 ProviderStatus.DATA1 53 }; 54 55 /** 56 * We'll wait for this amount of time on the UI thread if the load hasn't finished. 57 */ 58 private static final int LOAD_WAIT_TIMEOUT_MS = 1000; 59 60 private static final int STATUS_UNKNOWN = -1; 61 62 private static ProviderStatusWatcher sInstance; 63 64 private final Context mContext; 65 private final Handler mHandler = new Handler(); 66 67 private final Object mSignal = new Object(); 68 69 private int mStartRequestedCount; 70 71 private LoaderTask mLoaderTask; 72 73 /** Last known provider status. This can be changed on a worker thread. */ 74 private int mProviderStatus = STATUS_UNKNOWN; 75 76 /** Last known provider status data. This can be changed on a worker thread. */ 77 private String mProviderData; 78 79 private final ArrayList<ProviderStatusListener> mListeners = Lists.newArrayList(); 80 81 private final Runnable mStartLoadingRunnable = new Runnable() { 82 @Override 83 public void run() { 84 startLoading(); 85 } 86 }; 87 88 /** 89 * Returns the singleton instance. 90 */ 91 public synchronized static ProviderStatusWatcher getInstance(Context context) { 92 if (sInstance == null) { 93 sInstance = new ProviderStatusWatcher(context); 94 } 95 return sInstance; 96 } 97 98 private ProviderStatusWatcher(Context context) { 99 super(null); 100 mContext = context; 101 } 102 103 /** Add a listener. */ 104 public void addListener(ProviderStatusListener listener) { 105 mListeners.add(listener); 106 } 107 108 /** Remove a listener */ 109 public void removeListener(ProviderStatusListener listener) { 110 mListeners.remove(listener); 111 } 112 113 private void notifyListeners() { 114 if (DEBUG) { 115 Log.d(TAG, "notifyListeners: " + mListeners.size()); 116 } 117 if (isStarted()) { 118 for (ProviderStatusListener listener : mListeners) { 119 listener.onProviderStatusChange(); 120 } 121 } 122 } 123 124 private boolean isStarted() { 125 return mStartRequestedCount > 0; 126 } 127 128 /** 129 * Starts watching the provider status. {@link #start()} and {@link #stop()} calls can be 130 * nested. 131 */ 132 public void start() { 133 if (++mStartRequestedCount == 1) { 134 mContext.getContentResolver() 135 .registerContentObserver(ProviderStatus.CONTENT_URI, false, this); 136 startLoading(); 137 138 if (DEBUG) { 139 Log.d(TAG, "Start observing"); 140 } 141 } 142 } 143 144 /** 145 * Stops watching the provider status. 146 */ 147 public void stop() { 148 if (!isStarted()) { 149 Log.e(TAG, "Already stopped"); 150 return; 151 } 152 if (--mStartRequestedCount == 0) { 153 154 mHandler.removeCallbacks(mStartLoadingRunnable); 155 156 mContext.getContentResolver().unregisterContentObserver(this); 157 if (DEBUG) { 158 Log.d(TAG, "Stop observing"); 159 } 160 } 161 } 162 163 /** 164 * @return last known provider status. 165 * 166 * If this method is called when we haven't started the status query or the query is still in 167 * progress, it will start a query in a worker thread if necessary, and *wait for the result*. 168 * 169 * This means this method is essentially a blocking {@link ProviderStatus#CONTENT_URI} query. 170 * This URI is not backed by the file system, so is usually fast enough to perform on the main 171 * thread, but in extreme cases (when the system takes a while to bring up the contacts 172 * provider?) this may still cause ANRs. 173 * 174 * In order to avoid that, if we can't load the status within {@link #LOAD_WAIT_TIMEOUT_MS}, 175 * we'll give up and just returns {@link ProviderStatus#STATUS_UPGRADING} in order to unblock 176 * the UI thread. The actual result will be delivered later via {@link ProviderStatusListener}. 177 * (If {@link ProviderStatus#STATUS_UPGRADING} is returned, the app (should) shows an according 178 * message, like "contacts are being updated".) 179 */ 180 public int getProviderStatus() { 181 waitForLoaded(); 182 183 if (mProviderStatus == STATUS_UNKNOWN) { 184 return ProviderStatus.STATUS_UPGRADING; 185 } 186 187 return mProviderStatus; 188 } 189 190 /** 191 * @return last known provider status data. See also {@link #getProviderStatus()}. 192 */ 193 public String getProviderStatusData() { 194 waitForLoaded(); 195 196 if (mProviderStatus == STATUS_UNKNOWN) { 197 // STATUS_UPGRADING has no data. 198 return ""; 199 } 200 201 return mProviderData; 202 } 203 204 private void waitForLoaded() { 205 if (mProviderStatus == STATUS_UNKNOWN) { 206 if (mLoaderTask == null) { 207 // For some reason the loader couldn't load the status. Let's start it again. 208 startLoading(); 209 } 210 synchronized (mSignal) { 211 try { 212 mSignal.wait(LOAD_WAIT_TIMEOUT_MS); 213 } catch (InterruptedException ignore) { 214 } 215 } 216 } 217 } 218 219 private void startLoading() { 220 if (mLoaderTask != null) { 221 return; // Task already running. 222 } 223 224 if (DEBUG) { 225 Log.d(TAG, "Start loading"); 226 } 227 228 mLoaderTask = new LoaderTask(); 229 mLoaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 230 } 231 232 private class LoaderTask extends AsyncTask<Void, Void, Boolean> { 233 @Override 234 protected Boolean doInBackground(Void... params) { 235 try { 236 Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI, 237 PROJECTION, null, null, null); 238 if (cursor != null) { 239 try { 240 if (cursor.moveToFirst()) { 241 mProviderStatus = cursor.getInt(0); 242 mProviderData = cursor.getString(1); 243 return true; 244 } 245 } finally { 246 cursor.close(); 247 } 248 } 249 return false; 250 } finally { 251 synchronized (mSignal) { 252 mSignal.notifyAll(); 253 } 254 } 255 } 256 257 @Override 258 protected void onCancelled(Boolean result) { 259 cleanUp(); 260 } 261 262 @Override 263 protected void onPostExecute(Boolean loaded) { 264 cleanUp(); 265 if (loaded != null && loaded) { 266 notifyListeners(); 267 } 268 } 269 270 private void cleanUp() { 271 mLoaderTask = null; 272 } 273 } 274 275 /** 276 * Called when provider status may has changed. 277 * 278 * This method will be called on a worker thread by the framework. 279 */ 280 @Override 281 public void onChange(boolean selfChange, Uri uri) { 282 if (!ProviderStatus.CONTENT_URI.equals(uri)) return; 283 284 // Provider status change is rare, so okay to log. 285 Log.i(TAG, "Provider status changed."); 286 287 mHandler.removeCallbacks(mStartLoadingRunnable); // Remove one in the queue, if any. 288 mHandler.post(mStartLoadingRunnable); 289 } 290 291 /** 292 * Sends a provider status update, which will trigger a retry of database upgrade 293 */ 294 public void retryUpgrade() { 295 Log.i(TAG, "retryUpgrade"); 296 final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { 297 @Override 298 protected Void doInBackground(Void... params) { 299 ContentValues values = new ContentValues(); 300 values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING); 301 mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values, 302 null, null); 303 return null; 304 } 305 }; 306 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 307 } 308} 309