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