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