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