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