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