blob: 64dc298203e13daf55140360a2d746b40e78edb4 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.list;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.provider.ContactsContract.ProviderStatus;
import android.util.Log;
import com.android.contacts.compat.ProviderStatusCompat;
import com.android.contactsbind.FeedbackHelper;
import com.google.common.collect.Lists;
import java.util.ArrayList;
/**
* A singleton that keeps track of the last known provider status.
*
* All methods must be called on the UI thread unless noted otherwise.
*
* All members must be set on the UI thread unless noted otherwise.
*/
public class ProviderStatusWatcher extends ContentObserver {
private static final String TAG = "ProviderStatusWatcher";
private static final boolean DEBUG = false;
/**
* Callback interface invoked when the provider status changes.
*/
public interface ProviderStatusListener {
public void onProviderStatusChange();
}
private static final String[] PROJECTION = new String[] {
ProviderStatus.STATUS
};
/**
* We'll wait for this amount of time on the UI thread if the load hasn't finished.
*/
private static final int LOAD_WAIT_TIMEOUT_MS = 1000;
private static ProviderStatusWatcher sInstance;
private final Context mContext;
private final Handler mHandler = new Handler();
private final Object mSignal = new Object();
private int mStartRequestedCount;
private LoaderTask mLoaderTask;
/** Last known provider status. This can be changed on a worker thread.
* See {@link ProviderStatus#STATUS} */
private Integer mProviderStatus;
private final ArrayList<ProviderStatusListener> mListeners = Lists.newArrayList();
private final Runnable mStartLoadingRunnable = new Runnable() {
@Override
public void run() {
startLoading();
}
};
/**
* Returns the singleton instance.
*/
public synchronized static ProviderStatusWatcher getInstance(Context context) {
if (sInstance == null) {
sInstance = new ProviderStatusWatcher(context);
}
return sInstance;
}
private ProviderStatusWatcher(Context context) {
super(null);
mContext = context;
}
/** Add a listener. */
public void addListener(ProviderStatusListener listener) {
mListeners.add(listener);
}
/** Remove a listener */
public void removeListener(ProviderStatusListener listener) {
mListeners.remove(listener);
}
private void notifyListeners() {
if (DEBUG) {
Log.d(TAG, "notifyListeners: " + mListeners.size());
}
if (isStarted()) {
for (ProviderStatusListener listener : mListeners) {
listener.onProviderStatusChange();
}
}
}
private boolean isStarted() {
return mStartRequestedCount > 0;
}
/**
* Starts watching the provider status. {@link #start()} and {@link #stop()} calls can be
* nested.
*/
public void start() {
if (++mStartRequestedCount == 1) {
mContext.getContentResolver()
.registerContentObserver(ProviderStatus.CONTENT_URI, false, this);
startLoading();
if (DEBUG) {
Log.d(TAG, "Start observing");
}
}
}
/**
* Stops watching the provider status.
*/
public void stop() {
if (!isStarted()) {
Log.e(TAG, "Already stopped");
return;
}
if (--mStartRequestedCount == 0) {
mHandler.removeCallbacks(mStartLoadingRunnable);
mContext.getContentResolver().unregisterContentObserver(this);
if (DEBUG) {
Log.d(TAG, "Stop observing");
}
}
}
/**
* @return last known provider status.
*
* If this method is called when we haven't started the status query or the query is still in
* progress, it will start a query in a worker thread if necessary, and *wait for the result*.
*
* This means this method is essentially a blocking {@link ProviderStatus#CONTENT_URI} query.
* This URI is not backed by the file system, so is usually fast enough to perform on the main
* thread, but in extreme cases (when the system takes a while to bring up the contacts
* provider?) this may still cause ANRs.
*
* In order to avoid that, if we can't load the status within {@link #LOAD_WAIT_TIMEOUT_MS},
* we'll give up and just returns {@link ProviderStatusCompat#STATUS_BUSY} in order to unblock
* the UI thread. The actual result will be delivered later via {@link ProviderStatusListener}.
* (If {@link ProviderStatusCompat#STATUS_BUSY} is returned, the app (should) shows an according
* message, like "contacts are being updated".)
*/
public int getProviderStatus() {
waitForLoaded();
if (mProviderStatus == null) {
return ProviderStatusCompat.STATUS_BUSY;
}
return mProviderStatus;
}
private void waitForLoaded() {
if (mProviderStatus == null) {
if (mLoaderTask == null) {
// For some reason the loader couldn't load the status. Let's start it again.
startLoading();
}
synchronized (mSignal) {
try {
mSignal.wait(LOAD_WAIT_TIMEOUT_MS);
} catch (InterruptedException ignore) {
}
}
}
}
private void startLoading() {
if (mLoaderTask != null) {
return; // Task already running.
}
if (DEBUG) {
Log.d(TAG, "Start loading");
}
mLoaderTask = new LoaderTask();
mLoaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private class LoaderTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
PROJECTION, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Note here we can't just say "Status", as AsyncTask has the "Status"
// enum too.
mProviderStatus = cursor.getInt(0);
return true;
}
} finally {
cursor.close();
}
}
return false;
} catch (SecurityException e) {
FeedbackHelper.sendFeedback(mContext, TAG,
"Security exception when querying provider status", e);
return false;
} finally {
synchronized (mSignal) {
mSignal.notifyAll();
}
}
}
@Override
protected void onCancelled(Boolean result) {
cleanUp();
}
@Override
protected void onPostExecute(Boolean loaded) {
cleanUp();
if (loaded != null && loaded) {
notifyListeners();
}
}
private void cleanUp() {
mLoaderTask = null;
}
}
/**
* Called when provider status may has changed.
*
* This method will be called on a worker thread by the framework.
*/
@Override
public void onChange(boolean selfChange, Uri uri) {
if (!ProviderStatus.CONTENT_URI.equals(uri)) return;
// Provider status change is rare, so okay to log.
Log.i(TAG, "Provider status changed.");
mHandler.removeCallbacks(mStartLoadingRunnable); // Remove one in the queue, if any.
mHandler.post(mStartLoadingRunnable);
}
}