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

Commit 50254fee authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Offer to cache ContentResolver-related Bundles.

There are a handful of core system services that collect data from
third-party ContentProviders by spinning them up and then caching the
results locally in memory.  However, if those apps are killed due to
low-memory pressure, they lose that cached data and have to collect
it again from scratch.  It's impossible for those apps to maintain a
correct cache when not running, since they'll miss out on Uri change
notifications.

To work around this, this change introducing a narrowly-scoped
caching mechanism that maps from Uris to Bundles.  The cache is
isolated per-user and per-calling-package, and internally it's
optimized to keep the Uri notification flow as fast as possible.
Each Bundle is invalidated whenever a notification event for a Uri
key is sent, or when the package hosting the provider is changed.

This change also wires up DocumentsUI to use this new mechanism,
which improves cold-start performance from 3300ms to 1800ms.  The
more DocumentsProviders a system has, the more pronounced this
benefit is.  Use BOOT_COMPLETED to build the cache at boot.

Add more permission docs, send a missing extra in DATA_CLEARED
broadcast.

Bug: 18406595
Change-Id: If3eae14bb3c69a8b83a65f530e081efc3b34d4bc
parent 2392d997
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@
    <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
    <uses-permission android:name="android.permission.REMOVE_TASKS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.CACHE_CONTENT" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:name=".DocumentsApplication"
@@ -105,6 +107,12 @@
            </intent-filter>
        </receiver>

        <receiver android:name=".BootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

        <service
            android:name=".services.FileOperationService"
            android:exported="false">
+34 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.documentsui;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

/**
 * Prime {@link RootsCache} when the system is booted.
 */
public class BootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // We already spun up our application object before getting here, which
        // kicked off a task to load roots, so this broadcast is finished once
        // that first pass is done.
        DocumentsApplication.getRootsCache(context).setBootCompletedResult(goAsync());
    }
}
+62 −8
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.documentsui;

import static com.android.documentsui.Shared.DEBUG;

import android.content.BroadcastReceiver.PendingResult;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
@@ -30,6 +31,7 @@ import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.provider.DocumentsContract;
@@ -40,11 +42,11 @@ import android.util.Log;
import com.android.documentsui.model.RootInfo;
import com.android.internal.annotations.GuardedBy;

import libcore.io.IoUtils;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

import libcore.io.IoUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -63,6 +65,8 @@ public class RootsCache {

    private static final String TAG = "RootsCache";

    private static final boolean ENABLE_SYSTEM_CACHE = true;

    private final Context mContext;
    private final ContentObserver mObserver;
    private OnCacheUpdateListener mCacheUpdateListener;
@@ -72,6 +76,11 @@ public class RootsCache {
    private final Object mLock = new Object();
    private final CountDownLatch mFirstLoad = new CountDownLatch(1);

    @GuardedBy("mLock")
    private boolean mFirstLoadDone;
    @GuardedBy("mLock")
    private PendingResult mBootCompletedResult;

    @GuardedBy("mLock")
    private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
    @GuardedBy("mLock")
@@ -118,7 +127,7 @@ public class RootsCache {
    public void updateAsync() {

        // NOTE: This method is called when the UI language changes.
        // For that reason we upadte our RecentsRoot to reflect
        // For that reason we update our RecentsRoot to reflect
        // the current language.
        mRecentsRoot.title = mContext.getString(R.string.root_recent);

@@ -152,7 +161,25 @@ public class RootsCache {
        }
    }

    private void waitForFirstLoad() {
    public void setBootCompletedResult(PendingResult result) {
        synchronized (mLock) {
            // Quickly check if we've already finished loading, otherwise hang
            // out until first pass is finished.
            if (mFirstLoadDone) {
                result.finish();
            } else {
                mBootCompletedResult = result;
            }
        }
    }

    /**
     * Block until the first {@link UpdateTask} pass has finished.
     *
     * @return {@code true} if cached roots is ready to roll, otherwise
     *         {@code false} if we timed out while waiting.
     */
    private boolean waitForFirstLoad() {
        boolean success = false;
        try {
            success = mFirstLoad.await(15, TimeUnit.SECONDS);
@@ -161,6 +188,7 @@ public class RootsCache {
        if (!success) {
            Log.w(TAG, "Timeout waiting for first update");
        }
        return success;
    }

    /**
@@ -222,9 +250,11 @@ public class RootsCache {
            final long start = SystemClock.elapsedRealtime();

            if (mFilterPackage != null) {
                // Need at least first load, since we're going to be using
                // previously cached values for non-matching packages.
                waitForFirstLoad();
                // We must have previously cached values to fill in non-matching
                // packages, so wait around for successful first load.
                if (!waitForFirstLoad()) {
                    return null;
                }
            }

            mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
@@ -243,6 +273,11 @@ public class RootsCache {
            if (DEBUG)
                Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
            synchronized (mLock) {
                mFirstLoadDone = true;
                if (mBootCompletedResult != null) {
                    mBootCompletedResult.finish();
                    mBootCompletedResult = null;
                }
                mRoots = mTaskRoots;
                mStoppedAuthorities = mTaskStoppedAuthorities;
            }
@@ -300,9 +335,18 @@ public class RootsCache {
            }
        }

        final List<RootInfo> roots = new ArrayList<>();
        final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
        if (ENABLE_SYSTEM_CACHE) {
            // Look for roots data that we might have cached for ourselves in the
            // long-lived system process.
            final Bundle systemCache = resolver.getCache(rootsUri);
            if (systemCache != null) {
                if (DEBUG) Log.d(TAG, "System cache hit for " + authority);
                return systemCache.getParcelableArrayList(TAG);
            }
        }

        final ArrayList<RootInfo> roots = new ArrayList<>();
        ContentProviderClient client = null;
        Cursor cursor = null;
        try {
@@ -318,6 +362,16 @@ public class RootsCache {
            IoUtils.closeQuietly(cursor);
            ContentProviderClient.releaseQuietly(client);
        }

        if (ENABLE_SYSTEM_CACHE) {
            // Cache these freshly parsed roots over in the long-lived system
            // process, in case our process goes away. The system takes care of
            // invalidating the cache if the package or Uri changes.
            final Bundle systemCache = new Bundle();
            systemCache.putParcelableArrayList(TAG, roots);
            resolver.putCache(rootsUri, systemCache);
        }

        return roots;
    }

+1 −1
Original line number Diff line number Diff line
@@ -55,7 +55,7 @@ public class RootInfo implements Durable, Parcelable, Comparable<RootInfo> {
    private static final int VERSION_DROP_TYPE = 2;

    // The values of these constants determine the sort order of various roots in the RootsFragment.
    @IntDef(flag = true, value = {
    @IntDef(flag = false, value = {
            TYPE_IMAGES,
            TYPE_VIDEO,
            TYPE_AUDIO,