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 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.compat.ProviderStatusCompat;
29
30import 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;
234            } finally {
235                synchronized (mSignal) {
236                    mSignal.notifyAll();
237                }
238            }
239        }
240
241        @Override
242        protected void onCancelled(Boolean result) {
243            cleanUp();
244        }
245
246        @Override
247        protected void onPostExecute(Boolean loaded) {
248            cleanUp();
249            if (loaded != null && loaded) {
250                notifyListeners();
251            }
252        }
253
254        private void cleanUp() {
255            mLoaderTask = null;
256        }
257    }
258
259    /**
260     * Called when provider status may has changed.
261     *
262     * This method will be called on a worker thread by the framework.
263     */
264    @Override
265    public void onChange(boolean selfChange, Uri uri) {
266        if (!ProviderStatus.CONTENT_URI.equals(uri)) return;
267
268        // Provider status change is rare, so okay to log.
269        Log.i(TAG, "Provider status changed.");
270
271        mHandler.removeCallbacks(mStartLoadingRunnable); // Remove one in the queue, if any.
272        mHandler.post(mStartLoadingRunnable);
273    }
274}
275