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

Commit e5f868fd authored by Steve McKay's avatar Steve McKay
Browse files

Adapt unpaged cursors to paged requests.

Allow all client targeting Android O to assume paging
    support for any provider.
Adds a new PageViewCursor that adapts an unpaged cursor
    to a paged request.
Updates ContentProviderNative to perform wrapping on
    unpaged results.

Bug: 30927484
Change-Id: I4e225dc16761793c85ef8a195bf049113c79cd20
Test: Added for new class. Run info @ frameworks/base/core/tests/coretests/README
parent b20f3209
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.database.Cursor;
import android.database.CursorToBulkCursorAdaptor;
import android.database.DatabaseUtils;
import android.database.IContentObserver;
import android.database.PageViewCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -103,6 +104,7 @@ abstract public class ContentProviderNative extends Binder implements IContentPr
                    if (cursor != null) {
                        CursorToBulkCursorAdaptor adaptor = null;

                        cursor = PageViewCursor.wrap(cursor, queryArgs);
                        try {
                            adaptor = new CursorToBulkCursorAdaptor(cursor, observer,
                                    getProviderName());
+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 android.database;

import static com.android.internal.util.Preconditions.checkArgument;

import android.annotation.Nullable;
import android.content.ContentResolver;
import android.os.Bundle;
import android.util.Log;
import android.util.MathUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;

/**
 * Cursor wrapper that provides visibility into a subset of a wrapped cursor.
 *
 * The window is specified by offset and limit.
 *
 * @hide
 */
public final class PageViewCursor extends CrossProcessCursorWrapper {

    /**
     * An extra added to results that are auto-paged using the wrapper.
     */
    public static final String EXTRA_AUTO_PAGED = "android.content.extra.AUTO_PAGED";

    private static final String TAG = "PageViewCursor";
    private static final boolean DEBUG = false;
    private static final boolean VERBOSE = false;

    private final int mOffset;  // aka first index
    private final int mCount;
    private final Bundle mExtras;

    private int mPos = -1;

    /**
     * @see PageViewCursor#wrap(Cursor, Bundle)
     */
    @VisibleForTesting
    public PageViewCursor(Cursor cursor, int offset, int limit) {
        super(cursor);

        checkArgument(offset > -1);
        checkArgument(limit > -1);

        mOffset = offset;

        mExtras = new Bundle();
        Bundle extras = cursor.getExtras();
        if (extras != null) {
            mExtras.putAll(extras);
        }
        mExtras.putBoolean(EXTRA_AUTO_PAGED, true);

        // We need a mutable bundle so we can add QUERY_RESULT_SIZE.
        // Direct equality check is correct here. Bundle.EMPTY is a specific instance
        // of Bundle that is immutable by way of implementation.
        // mExtras = (extras == Bundle.EMPTY) ? new Bundle() : extras;

        // When we're wrapping another cursor, it should not already be "paged".
        checkArgument(!mExtras.containsKey(ContentResolver.EXTRA_TOTAL_SIZE));

        int count = mCursor.getCount();
        mExtras.putInt(ContentResolver.EXTRA_TOTAL_SIZE, count);

        mCount = MathUtils.constrain(count - offset, 0, limit);

        if (DEBUG) Log.d(TAG, "Wrapped cursor"
            + " offset: " + mOffset
            + ", limit: " + limit
            + ", delegate_size: " + count
            + ", paged_count: " + mCount);
    }

    @Override
    public Bundle getExtras() {
        return mExtras;
    }

    @Override
    public int getPosition() {
        return mPos;
    }

    @Override
    public boolean isBeforeFirst() {
        if (mCount == 0) {
            return true;
        }
        return mPos == -1;
    }

    @Override
    public boolean isAfterLast() {
        if (mCount == 0) {
            return true;
        }
        return mPos == mCount;
    }

    @Override
    public boolean isFirst() {
        return mPos == 0;
    }

    @Override
    public boolean isLast() {
        return mPos == mCount - 1;
    }

    @Override
    public boolean moveToFirst() {
        return moveToPosition(0);
    }

    @Override
    public boolean moveToLast() {
        return moveToPosition(mCount - 1);
    }

    @Override
    public boolean moveToNext() {
        return move(1);
    }

    @Override
    public boolean moveToPrevious() {
        return move(-1);
    }

    @Override
    public boolean move(int offset) {
        return moveToPosition(mPos + offset);
    }

    @Override
    public boolean moveToPosition(int position) {
        if (position >= mCount) {
            if (VERBOSE) Log.v(TAG, "Invalid Positon: " + position + " >= count: " + mCount
                    + ". Moving to last record.");
            mPos = mCount;
            super.moveToPosition(mOffset + mPos);  // move into "after last" state.
            return false;
        }

        // Make sure position isn't before the beginning of the cursor
        if (position < 0) {
            if (VERBOSE) Log.v(TAG, "Ignoring invalid move to position: " + position);
            mPos = -1;
            super.moveToPosition(mPos);
            return false;
        }

        if (position == mPos) {
            if (VERBOSE) Log.v(TAG, "Ignoring no-op move to position: " + position);
            return true;
        }

        int delegatePosition = position + mOffset;
        if (VERBOSE) Log.v(TAG, "Moving delegate cursor to position: " + delegatePosition);
        if (super.moveToPosition(delegatePosition)) {
            mPos = position;
            return true;
        } else {
            mPos = -1;
            super.moveToPosition(-1);
            return false;
        }
    }

    @Override
    public boolean onMove(int oldPosition, int newPosition) {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    public int getCount() {
        return mCount;
    }

    /**
     * Wraps the cursor such that it will honor paging args (if present), AND if the cursor
     * does not report paging size.
     *
     * <p>No-op if cursor already contains paging or is less than specified page size.
     */
    public static Cursor wrap(Cursor cursor, @Nullable Bundle queryArgs) {

        boolean hasPagingArgs =
                queryArgs != null
                && (queryArgs.containsKey(ContentResolver.QUERY_ARG_OFFSET)
                || queryArgs.containsKey(ContentResolver.QUERY_ARG_LIMIT));

        if (!hasPagingArgs) {
            if (VERBOSE) Log.d(TAG, "No-wrap: No paging args in request.");
            return cursor;
        }

        if (hasPagedResponseDetails(cursor.getExtras())) {
            if (VERBOSE) Log.d(TAG, "No-wrap. Cursor has paging details.");
            return cursor;
        }

        return new PageViewCursor(
                cursor,
                queryArgs.getInt(ContentResolver.QUERY_ARG_OFFSET, 0),
                queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, Integer.MAX_VALUE));
    }

    /**
     * @return true if the extras contains information indicating the associated
     * cursor is paged.
     */
    private static boolean hasPagedResponseDetails(@Nullable Bundle extras) {
        if (extras != null && extras.containsKey(ContentResolver.EXTRA_TOTAL_SIZE)) {
            return true;
        }

        String[] honoredArgs = extras.getStringArray(ContentResolver.EXTRA_HONORED_ARGS);
        if (honoredArgs != null && (
                ArrayUtils.contains(honoredArgs, ContentResolver.QUERY_ARG_OFFSET)
                || ArrayUtils.contains(honoredArgs, ContentResolver.QUERY_ARG_LIMIT))) {
            return true;
        }

        return false;
    }
}
+318 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 android.database;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.annotation.Nullable;
import android.content.ContentResolver;
import android.os.Bundle;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import android.util.MathUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Random;

@RunWith(AndroidJUnit4.class)
public class PageViewCursorTest {

    private static final int ITEM_COUNT = 20;

    private static final String NAME_COLUMN = "name";
    private static final String NUM_COLUMN = "num";

    private static final String[] COLUMNS = new String[]{
      NAME_COLUMN,
      NUM_COLUMN
    };

    private static final String[] NAMES = new String[] {
            "000",
            "111",
            "222",
            "333",
            "444",
            "555",
            "666",
            "777",
            "888",
            "999",
            "aaa",
            "bbb",
            "ccc",
            "ddd",
            "eee",
            "fff",
            "ggg",
            "hhh",
            "iii",
            "jjj"
    };

    private MatrixCursor mDelegate;
    private PageViewCursor mCursor;

    @Before
    public void setUp() {
        Random rand = new Random();

        mDelegate = new MatrixCursor(COLUMNS);
        for (int i = 0; i < ITEM_COUNT; i++) {
            MatrixCursor.RowBuilder row = mDelegate.newRow();
            row.add(NAME_COLUMN, NAMES[i]);
            row.add(NUM_COLUMN, rand.nextInt());
        }

        mCursor = new PageViewCursor(mDelegate, 10, 5);
    }

    @Test
    public void testPage_Size() {
        assertEquals(5, mCursor.getCount());
    }

    @Test
    public void testPage_TotalSize() {
        assertEquals(ITEM_COUNT, mCursor.getExtras().getInt(ContentResolver.EXTRA_TOTAL_SIZE));
    }

    @Test
    public void testPage_OffsetExceedsCursorCount_EffectivelyEmptyCursor() {
        mCursor = new PageViewCursor(mDelegate, ITEM_COUNT * 2, 5);
        assertEquals(0, mCursor.getCount());
    }

    @Test
    public void testMoveToPosition() {
        assertTrue(mCursor.moveToPosition(0));
        assertEquals(NAMES[10], mCursor.getString(0));
        assertTrue(mCursor.moveToPosition(1));
        assertEquals(NAMES[11], mCursor.getString(0));
        assertTrue(mCursor.moveToPosition(4));
        assertEquals(NAMES[14], mCursor.getString(0));

        // and then back down again for good measure.
        assertTrue(mCursor.moveToPosition(1));
        assertEquals(NAMES[11], mCursor.getString(0));
        assertTrue(mCursor.moveToPosition(0));
        assertEquals(NAMES[10], mCursor.getString(0));
    }

    @Test
    public void testMoveToPosition_MoveToSamePosition_NoOp() {
        assertTrue(mCursor.moveToPosition(1));
        assertEquals(NAMES[11], mCursor.getString(0));
        assertTrue(mCursor.moveToPosition(1));
        assertEquals(NAMES[11], mCursor.getString(0));
    }

    @Test
    public void testMoveToPosition_PositionOutOfBounds_MovesToBeforeFirst() {
        assertTrue(mCursor.moveToPosition(0));
        assertEquals(NAMES[10], mCursor.getString(0));

        // move before
        assertFalse(mCursor.moveToPosition(-12));
        assertTrue(mCursor.isBeforeFirst());
    }

    @Test
    public void testMoveToPosition_PositionOutOfBounds_MovesToAfterLast() {
        assertTrue(mCursor.moveToPosition(0));
        assertEquals(NAMES[10], mCursor.getString(0));

        assertFalse(mCursor.moveToPosition(222));
        assertTrue(mCursor.isAfterLast());
    }

    @Test
    public void testPosition() {
        assertEquals(-1, mCursor.getPosition());
    }

    @Test
    public void testIsBeforeFirst() {
        assertTrue(mCursor.isBeforeFirst());
        mCursor.moveToFirst();
        assertFalse(mCursor.isBeforeFirst());
    }

    @Test
    public void testCount_ZeroForEmptyCursor() {
        mCursor = new PageViewCursor(mDelegate, 0, 0);
        assertEquals(0, mCursor.getCount());
    }

    @Test
    public void testIsBeforeFirst_TrueForEmptyCursor() {
        mCursor = new PageViewCursor(mDelegate, 0, 0);
        assertTrue(mCursor.isBeforeFirst());
    }

    @Test
    public void testIsAfterLast() {
        assertFalse(mCursor.isAfterLast());
        mCursor.moveToLast();
        mCursor.moveToNext();
        assertTrue(mCursor.isAfterLast());
    }

    @Test
    public void testIsAfterLast_TrueForEmptyCursor() {
        mCursor = new PageViewCursor(mDelegate, 0, 0);
        assertTrue(mCursor.isAfterLast());
    }

    @Test
    public void testIsFirst() {
        assertFalse(mCursor.isFirst());
        mCursor.moveToFirst();
        assertTrue(mCursor.isFirst());
    }

    @Test
    public void testIsLast() {
        assertFalse(mCursor.isLast());
        mCursor.moveToLast();
        assertTrue(mCursor.isLast());
    }

    @Test
    public void testMove() {
        // note that initial position is -1, so moving
        // 2 will only put as at 1.
        mCursor.move(2);
        assertEquals(NAMES[11], mCursor.getString(0));
        mCursor.move(-1);
        assertEquals(NAMES[10], mCursor.getString(0));
    }

    @Test
    public void testMoveToFist() {
        mCursor.moveToPosition(3);
        mCursor.moveToFirst();
        assertEquals(NAMES[10], mCursor.getString(0));
    }

    @Test
    public void testMoveToLast() {
        mCursor.moveToLast();
        assertEquals(NAMES[14], mCursor.getString(0));
    }

    @Test
    public void testMoveToNext() {
        // default position is -1, so next is 0.
        mCursor.moveToNext();
        assertEquals(NAMES[10], mCursor.getString(0));
    }

    @Test
    public void testMoveToNext_AfterLastReturnsFalse() {
        mCursor.moveToLast();
        assertFalse(mCursor.moveToNext());
    }

    @Test
    public void testMoveToPrevious() {
        mCursor.moveToPosition(3);
        mCursor.moveToPrevious();
        assertEquals(NAMES[12], mCursor.getString(0));
    }

    @Test
    public void testMoveToPrevious_BeforeFirstReturnsFalse() {
        assertFalse(mCursor.moveToPrevious());
    }

    @Test
    public void testWindow_ReadPastEnd() {
        assertFalse(mCursor.moveToPosition(10));
    }

    @Test
    public void testOffset_LimitOutOfBounds() {
        mCursor = new PageViewCursor(mDelegate, 5, 100);
        assertEquals(15, mCursor.getCount());
    }

    @Test
    public void testPagingMarker() {
        mCursor = new PageViewCursor(mDelegate, 5, 100);
        assertTrue(mCursor.getExtras().getBoolean(PageViewCursor.EXTRA_AUTO_PAGED));
    }

    @Test
    public void testWrap() {
        Bundle queryArgs = new Bundle();
        queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 5);
        queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, queryArgs);
        assertTrue(wrapped instanceof PageViewCursor);
        assertEquals(5, wrapped.getCount());
    }

    @Test
    public void testWrap_NoOpWithoutPagingArgs() {
        Cursor wrapped = PageViewCursor.wrap(mDelegate, Bundle.EMPTY);
        assertTrue(mDelegate == wrapped);
    }

    @Test
    public void testWrap_NoOpCursorsWithExistingPaging_ByTotalSize() {
        Bundle extras = new Bundle();
        extras.putInt(ContentResolver.EXTRA_TOTAL_SIZE, 5);
        mDelegate.setExtras(extras);

        Bundle queryArgs = new Bundle();
        queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 5);
        queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, queryArgs);
        assertTrue(mDelegate == wrapped);
    }

    @Test
    public void testWrap_NoOpCursorsWithExistingPaging_ByHonoredArgs() {
        Bundle extras = new Bundle();
        extras.putStringArray(
                ContentResolver.EXTRA_HONORED_ARGS,
                new String[] {
                    ContentResolver.QUERY_ARG_OFFSET,
                    ContentResolver.QUERY_ARG_LIMIT
                });
        mDelegate.setExtras(extras);

        Bundle queryArgs = new Bundle();
        queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 5);
        queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, queryArgs);
        assertTrue(mDelegate == wrapped);
    }

    private void assertStringAt(int row, int column, String expected) {
        mCursor.moveToPosition(row);
        assertEquals(expected, mCursor.getString(column));
    }
}