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