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