blob: 64dc298203e13daf55140360a2d746b40e78edb4 [file] [log] [blame]
Wenyi Wang14cb7fa2016-06-09 16:50:20 -07001/*
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 */
Gary Mai69c182a2016-12-05 13:07:03 -080016package com.android.contacts.list;
Wenyi Wang14cb7fa2016-06-09 16:50:20 -070017
Wenyi Wang14cb7fa2016-06-09 16:50:20 -070018import android.content.Context;
19import android.database.ContentObserver;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.AsyncTask;
23import android.os.Handler;
24import android.provider.ContactsContract.ProviderStatus;
25import android.util.Log;
26
Gary Mai69c182a2016-12-05 13:07:03 -080027import com.android.contacts.compat.ProviderStatusCompat;
Wenyi Wangc3857fa2016-11-07 17:10:07 -080028import com.android.contactsbind.FeedbackHelper;
29
Wenyi Wang14cb7fa2016-06-09 16:50:20 -070030import com.google.common.collect.Lists;
31
32import 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 */
41public 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 {
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 */
88 public synchronized static ProviderStatusWatcher getInstance(Context context) {
89 if (sInstance == null) {
90 sInstance = new ProviderStatusWatcher(context);
91 }
92 return sInstance;
93 }
94
95 private ProviderStatusWatcher(Context context) {
96 super(null);
97 mContext = context;
98 }
99
100 /** Add a listener. */
101 public void addListener(ProviderStatusListener listener) {
102 mListeners.add(listener);
103 }
104
105 /** Remove a listener */
106 public void removeListener(ProviderStatusListener listener) {
107 mListeners.remove(listener);
108 }
109
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
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 */
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 */
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 */
177 public int getProviderStatus() {
178 waitForLoaded();
179
180 if (mProviderStatus == null) {
181 return ProviderStatusCompat.STATUS_BUSY;
182 }
183
184 return mProviderStatus;
185 }
186
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
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
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;
Wenyi Wangc3857fa2016-11-07 17:10:07 -0800234 } catch (SecurityException e) {
235 FeedbackHelper.sendFeedback(mContext, TAG,
236 "Security exception when querying provider status", e);
237 return false;
Wenyi Wang14cb7fa2016-06-09 16:50:20 -0700238 } finally {
239 synchronized (mSignal) {
240 mSignal.notifyAll();
241 }
242 }
243 }
244
245 @Override
246 protected void onCancelled(Boolean result) {
247 cleanUp();
248 }
249
250 @Override
251 protected void onPostExecute(Boolean loaded) {
252 cleanUp();
253 if (loaded != null && loaded) {
254 notifyListeners();
255 }
256 }
257
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
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}