Home | History | Annotate | Download | only in list
      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  */
     16 package com.android.contacts.list;
     17 
     18 import android.content.Context;
     19 import android.database.ContentObserver;
     20 import android.database.Cursor;
     21 import android.net.Uri;
     22 import android.os.AsyncTask;
     23 import android.os.Handler;
     24 import android.provider.ContactsContract.ProviderStatus;
     25 import android.util.Log;
     26 
     27 import com.android.contacts.compat.ProviderStatusCompat;
     28 import com.android.contactsbind.FeedbackHelper;
     29 
     30 import com.google.common.collect.Lists;
     31 
     32 import 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  */
     41 public 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             } catch (SecurityException e) {
    235                 FeedbackHelper.sendFeedback(mContext, TAG,
    236                         "Security exception when querying provider status", e);
    237                 return false;
    238             } 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 }
    279