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

Commit c2d761a4 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Adapt unpaged cursors to paged requests."

parents bfd10ebc e5f868fd
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));
    }
}