Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e919fba5 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

Merge "Don't load provider status on the ui thread" into jb-dev

parents c8310442 c2bd6138
Loading
Loading
Loading
Loading
+11 −9
Original line number Diff line number Diff line
@@ -48,8 +48,8 @@ import com.android.contacts.list.DefaultContactBrowseListFragment;
import com.android.contacts.list.DirectoryListLoader;
import com.android.contacts.list.OnContactBrowserActionListener;
import com.android.contacts.list.OnContactsUnavailableActionListener;
import com.android.contacts.list.ProviderStatusLoader;
import com.android.contacts.list.ProviderStatusLoader.ProviderStatusListener;
import com.android.contacts.list.ProviderStatusWatcher;
import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.AccountWithDataSet;
import com.android.contacts.preference.ContactsPreferenceActivity;
@@ -149,7 +149,7 @@ public class PeopleActivity extends ContactsActivity
    private ContactListFilterController mContactListFilterController;

    private ContactsUnavailableFragment mContactsUnavailableFragment;
    private ProviderStatusLoader mProviderStatusLoader;
    private ProviderStatusWatcher mProviderStatusWatcher;
    private int mProviderStatus = -1;

    private boolean mOptionsMenuContactsAvailable;
@@ -207,7 +207,7 @@ public class PeopleActivity extends ContactsActivity
    public PeopleActivity() {
        mInstanceId = sNextInstanceId.getAndIncrement();
        mIntentResolver = new ContactsIntentResolver(this);
        mProviderStatusLoader = new ProviderStatusLoader(this);
        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this);
    }

    @Override
@@ -247,7 +247,6 @@ public class PeopleActivity extends ContactsActivity
            mContactDetailFragment = (ContactDetailFragment) fragment;
        } else if (fragment instanceof ContactsUnavailableFragment) {
            mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
            mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
            mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
                    new ContactsUnavailableFragmentListener());
        }
@@ -269,6 +268,8 @@ public class PeopleActivity extends ContactsActivity
        mContactListFilterController.checkFilterValidity(false);
        mContactListFilterController.addListener(this);

        mProviderStatusWatcher.addListener(this);

        mIsRecreatedInstance = (savedState != null);
        createViewsAndFragments(savedState);
        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -486,14 +487,14 @@ public class PeopleActivity extends ContactsActivity
        mOptionsMenuContactsAvailable = false;

        mProviderStatus = -1;
        mProviderStatusLoader.setProviderStatusListener(null);
        mProviderStatusWatcher.stop();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mProviderStatusLoader.setProviderStatusListener(this);
        mProviderStatusWatcher.start();
        showContactsUnavailableFragmentIfNecessary();

        // Re-register the listener, which may have been cleared when onSaveInstanceState was
@@ -515,6 +516,8 @@ public class PeopleActivity extends ContactsActivity

    @Override
    protected void onDestroy() {
        mProviderStatusWatcher.removeListener(this);

        // Some of variables will be null if this Activity redirects Intent.
        // See also onCreate() or other methods called during the Activity's initialization.
        if (mActionBarAdapter != null) {
@@ -975,7 +978,7 @@ public class PeopleActivity extends ContactsActivity
    }

    private void showContactsUnavailableFragmentIfNecessary() {
        int providerStatus = mProviderStatusLoader.getProviderStatus();
        int providerStatus = mProviderStatusWatcher.getProviderStatus();
        if (providerStatus == mProviderStatus) {
            return;
        }
@@ -1015,7 +1018,6 @@ public class PeopleActivity extends ContactsActivity
            }
            if (mContactsUnavailableFragment == null) {
                mContactsUnavailableFragment = new ContactsUnavailableFragment();
                mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
                mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
                        new ContactsUnavailableFragmentListener());
                getFragmentManager().beginTransaction()
+0 −5
Original line number Diff line number Diff line
@@ -128,7 +128,6 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter
    private ContextMenuAdapter mContextMenuAdapter;
    private ContactPhotoManager mPhotoManager;
    private ContactListEmptyView mEmptyView;
    private ProviderStatusLoader mProviderStatusLoader;
    private ContactsPreferences mContactsPrefs;

    private boolean mForceLoad;
@@ -291,10 +290,6 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter

        mContactsPrefs.registerChangeListener(mPreferencesChangeListener);

        if (mProviderStatusLoader == null) {
            mProviderStatusLoader = new ProviderStatusLoader(mContext);
        }

        mForceLoad = loadPreferences();

        mDirectoryListStatus = STATUS_NOT_LOADED;
+11 −9
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ import android.widget.TextView;
 */
public class ContactsUnavailableFragment extends Fragment implements OnClickListener {

    private ProviderStatusLoader mProviderStatusLoader;
    private ProviderStatusWatcher mProviderStatusWatcher;

    private View mView;
    private TextView mMessageView;
@@ -51,6 +51,12 @@ public class ContactsUnavailableFragment extends Fragment implements OnClickList

    private OnContactsUnavailableActionListener mListener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(getActivity());
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -77,12 +83,8 @@ public class ContactsUnavailableFragment extends Fragment implements OnClickList
        mListener = listener;
    }

    public void setProviderStatusLoader(ProviderStatusLoader loader) {
        mProviderStatusLoader = loader;
    }

    public void update() {
        int providerStatus = mProviderStatusLoader.getProviderStatus();
        int providerStatus = mProviderStatusWatcher.getProviderStatus();
        switch (providerStatus) {
            case ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS:
                setMessageText(mNoContactsMsgResId, mNSecNoContactsMsgResId);
@@ -120,7 +122,7 @@ public class ContactsUnavailableFragment extends Fragment implements OnClickList

            case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
                String message = getResources().getString(R.string.upgrade_out_of_memory,
                        new Object[] { mProviderStatusLoader.getProviderStatusData() });
                        new Object[] { mProviderStatusWatcher.getProviderStatusData() });
                mMessageView.setText(message);
                mMessageView.setGravity(Gravity.LEFT);
                mMessageView.setVisibility(View.VISIBLE);
@@ -153,7 +155,7 @@ public class ContactsUnavailableFragment extends Fragment implements OnClickList
                mListener.onFreeInternalStorageAction();
                break;
            case R.id.import_failure_retry_button:
                mProviderStatusLoader.retryUpgrade();
                mProviderStatusWatcher.retryUpgrade();
                break;
        }
    }
@@ -166,7 +168,7 @@ public class ContactsUnavailableFragment extends Fragment implements OnClickList
        mNoContactsMsgResId = resId;
        mNSecNoContactsMsgResId = secResId;
        if (mMessageView != null &&
                mProviderStatusLoader.getProviderStatus() ==
                mProviderStatusWatcher.getProviderStatus() ==
                    ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
            if (resId != -1) {
                mMessageView.setText(mNoContactsMsgResId);
+0 −127
Original line number Diff line number Diff line
/*
 * Copyright (C) 2010 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.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Handler;
import android.provider.ContactsContract.ProviderStatus;

/**
 * Checks provider status and configures a list adapter accordingly.
 */
public class ProviderStatusLoader extends ContentObserver {

    /**
     * Callback interface invoked when the provider status changes.
     */
    public interface ProviderStatusListener {
        public void onProviderStatusChange();
    }

    private static final String[] PROJECTION = new String[] {
        ProviderStatus.STATUS,
        ProviderStatus.DATA1
    };

    private static final int UNKNOWN = -1;

    private final Context mContext;
    private int mProviderStatus = UNKNOWN;
    private String mProviderData;
    private ProviderStatusListener mListener;
    private Handler mHandler = new Handler();

    public ProviderStatusLoader(Context context) {
        super(null);
        this.mContext = context;
    }

    public int getProviderStatus() {
        if (mProviderStatus == UNKNOWN) {
            loadProviderStatus();
        }

        return mProviderStatus;
    }

    public String getProviderStatusData() {
        if (mProviderStatus == UNKNOWN) {
            loadProviderStatus();
        }

        return mProviderData;
    }

    protected void loadProviderStatus() {

        // Default to normal status
        mProviderStatus = ProviderStatus.STATUS_NORMAL;

        // This query can be performed on the UI thread because
        // the API explicitly allows such use.
        Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
                PROJECTION, null, null, null);
        if (cursor != null) {
            try {
                if (cursor.moveToFirst()) {
                    mProviderStatus = cursor.getInt(0);
                    mProviderData = cursor.getString(1);
                }
            } finally {
                cursor.close();
            }
        }
    }

    public void setProviderStatusListener(ProviderStatusListener listener) {
        mListener = listener;

        ContentResolver resolver = mContext.getContentResolver();
        if (listener != null) {
            mProviderStatus = UNKNOWN;
            resolver.registerContentObserver(ProviderStatus.CONTENT_URI, false, this);
        } else {
            resolver.unregisterContentObserver(this);
        }
    }

    @Override
    public void onChange(boolean selfChange) {
        // Deliver a notification on the UI thread
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mListener != null) {
                    mProviderStatus = UNKNOWN;
                    mListener.onProviderStatusChange();
                }
            }
        });
    }

    /**
     * Sends a provider status update, which will trigger a retry of database upgrade
     */
    public void retryUpgrade() {
        ContentValues values = new ContentValues();
        values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
        mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
    }
}
+308 −0
Original line number Diff line number Diff line
/*
 * 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 com.google.common.collect.Lists;

import android.content.ContentValues;
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 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,
        ProviderStatus.DATA1
    };

    /**
     * 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 final int STATUS_UNKNOWN = -1;

    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. */
    private int mProviderStatus = STATUS_UNKNOWN;

    /** Last known provider status data.  This can be changed on a worker thread. */
    private String mProviderData;

    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 ProviderStatus#STATUS_UPGRADING} in order to unblock
     * the UI thread.  The actual result will be delivered later via {@link ProviderStatusListener}.
     * (If {@link ProviderStatus#STATUS_UPGRADING} is returned, the app (should) shows an according
     * message, like "contacts are being updated".)
     */
    public int getProviderStatus() {
        waitForLoaded();

        if (mProviderStatus == STATUS_UNKNOWN) {
            return ProviderStatus.STATUS_UPGRADING;
        }

        return mProviderStatus;
    }

    /**
     * @return last known provider status data.  See also {@link #getProviderStatus()}.
     */
    public String getProviderStatusData() {
        waitForLoaded();

        if (mProviderStatus == STATUS_UNKNOWN) {
            // STATUS_UPGRADING has no data.
            return "";
        }

        return mProviderData;
    }

    private void waitForLoaded() {
        if (mProviderStatus == STATUS_UNKNOWN) {
            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()) {
                            mProviderStatus = cursor.getInt(0);
                            mProviderData = cursor.getString(1);
                            return true;
                        }
                    } finally {
                        cursor.close();
                    }
                }
                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);
    }

    /**
     * Sends a provider status update, which will trigger a retry of database upgrade
     */
    public void retryUpgrade() {
        Log.i(TAG, "retryUpgrade");
        final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                ContentValues values = new ContentValues();
                values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
                mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values,
                        null, null);
                return null;
            }
        };
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }
}