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

Commit 0b28e794 authored by Steve McKay's avatar Steve McKay Committed by android-build-merger
Browse files

Merge "Remove support for auto-paging of cursors." into oc-dev

am: 7f0d8c80

Change-Id: Ice100c419ce6d53721d6bbec1147bb1cc1bb7b4d
parents 8bf30a78 7f0d8c80
Loading
Loading
Loading
Loading
+0 −9
Original line number Diff line number Diff line
@@ -11775,15 +11775,6 @@ package android.database {
    field protected final java.util.ArrayList<T> mObservers;
  }
  public final class PageViewCursor extends android.database.CursorWrapper implements android.database.CrossProcessCursor {
    ctor public PageViewCursor(android.database.Cursor, android.os.Bundle);
    method public void fillWindow(int, android.database.CursorWindow);
    method public android.database.CursorWindow getWindow();
    method public boolean onMove(int, int);
    method public static android.database.Cursor wrap(android.database.Cursor, android.os.Bundle);
    field public static final java.lang.String EXTRA_AUTO_PAGED = "android.content.extra.AUTO_PAGED";
  }
  public class SQLException extends java.lang.RuntimeException {
    ctor public SQLException();
    ctor public SQLException(java.lang.String);
+0 −2
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ 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;
@@ -104,7 +103,6 @@ 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());
+0 −308
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.ArrayUtils.contains;
import static com.android.internal.util.Preconditions.checkArgument;

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

import java.util.Arrays;

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

    /** 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[] EMPTY_ARGS = new String[0];
    private static final String TAG = "PageViewCursor";
    private static final boolean DEBUG = Build.IS_DEBUGGABLE;
    private static final boolean VERBOSE = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);

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

    private @Nullable CursorWindow mWindow;
    private int mPos = -1;
    private int mWindowFillCount = 0;

    /**
     * @see PageViewCursor#wrap(Cursor, Bundle)
     */
    public PageViewCursor(Cursor cursor, Bundle queryArgs) {
        super(cursor);

        int offset = queryArgs.getInt(ContentResolver.QUERY_ARG_OFFSET, 0);
        int limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, Integer.MAX_VALUE);

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

        int count = mCursor.getCount();

        mOffset = offset;

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

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

        mExtras.putBoolean(EXTRA_AUTO_PAGED, true);
        mExtras.putInt(ContentResolver.EXTRA_TOTAL_SIZE, count);

        // Ensure we retain any extra args supplied in cursor extras, and add
        // offset and/or limit.
        String[] existingArgs = mExtras.getStringArray(ContentResolver.EXTRA_HONORED_ARGS);
        existingArgs = existingArgs != null ? existingArgs : EMPTY_ARGS;

        int size = existingArgs.length;

        // copy the array with space for the extra query args we'll be adding.
        String[] newArgs = Arrays.copyOf(existingArgs, size + 2);

        if (queryArgs.containsKey(ContentResolver.QUERY_ARG_OFFSET)) {
            newArgs[size++] = ContentResolver.QUERY_ARG_OFFSET;
        }
        if (queryArgs.containsKey(ContentResolver.QUERY_ARG_LIMIT)) {
            newArgs[size++] = ContentResolver.QUERY_ARG_LIMIT;
        }

        assert(size > existingArgs.length);  // must add at least one arg.

        // At this point there may be a null element at the end of
        // the array because our pre-sizing didn't match the actualy
        // number of args we added. So we trim.
        if (size == newArgs.length - 1) {
            newArgs = Arrays.copyOf(newArgs, size);
        }
        mExtras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, newArgs);

        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;
    }

    @Override
    public boolean getWantsAllOnMoveCalls() {
        return false; // we want bulk cursor adapter to lift data into a CursorWindow.
    }

    @Override
    public CursorWindow getWindow() {
        assert(mPos == -1 || mPos == 0);
        if (mWindow == null) {
            mWindow = new CursorWindow("PageViewCursorWindow");
            fillWindow(0, mWindow);
        }

        return mWindow;
    }

    @Override
    public void fillWindow(int position, CursorWindow window) {
        assert(window == mWindow);

        if (mWindowFillCount++ > 0) {
            Log.w(TAG, "Re-filling window on paged cursor! Reduce ContentResolver.QUERY_ARG_LIMIT");
        }

        DatabaseUtils.cursorFillWindow(this, position, window);
    }

    /**
     * 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.v(TAG, "No-wrap: No paging args in request.");
            return cursor;
        }

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

        // Cursors that want all calls aren't compatible with our way
        // of doing business. TODO: Cover this case in CTS.
        if (cursor.getWantsAllOnMoveCalls()) {
            Log.w(TAG, "Unable to wrap cursor that wants to hear about move calls.");
            return cursor;
        }

        return new PageViewCursor(cursor, queryArgs);
    }

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

        if (extras.containsKey(ContentResolver.EXTRA_TOTAL_SIZE)) {
            return true;
        }

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

        return false;
    }
}
+0 −387
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.ArrayUtils.contains;
import static com.android.internal.util.Preconditions.checkArgument;
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.Build;
import android.os.Bundle;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import android.util.MathUtils;

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

import java.util.Arrays;
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, createArgs(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, createArgs(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, createArgs(0, 0));
        assertEquals(0, mCursor.getCount());
    }

    @Test
    public void testIsBeforeFirst_TrueForEmptyCursor() {
        mCursor = new PageViewCursor(mDelegate, createArgs(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, createArgs(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 testLimitOutOfBounds() {
        mCursor = new PageViewCursor(mDelegate, createArgs(5, 100));
        assertEquals(15, mCursor.getCount());
    }

    @Test
    public void testOffsetOutOfBounds_EmptyResult() {
        mCursor = new PageViewCursor(mDelegate, createArgs(100000, 100));
        assertEquals(0, mCursor.getCount());
    }

    @Test
    public void testAddsExtras() {
        mCursor = new PageViewCursor(mDelegate, createArgs(5, 100));
        assertTrue(mCursor.getExtras().getBoolean(PageViewCursor.EXTRA_AUTO_PAGED));
        String[] honoredArgs = mCursor.getExtras()
                .getStringArray(ContentResolver.EXTRA_HONORED_ARGS);
        assertTrue(contains(honoredArgs, ContentResolver.QUERY_ARG_OFFSET));
        assertTrue(contains(honoredArgs, ContentResolver.QUERY_ARG_LIMIT));
    }

    @Test
    public void testAddsExtras_OnlyOffset() {
        Bundle args = new Bundle();
        args.putInt(ContentResolver.QUERY_ARG_OFFSET, 5);
        mCursor = new PageViewCursor(mDelegate, args);
        String[] honoredArgs = mCursor.getExtras()
                .getStringArray(ContentResolver.EXTRA_HONORED_ARGS);
        assertTrue(contains(honoredArgs, ContentResolver.QUERY_ARG_OFFSET));
        assertFalse(contains(honoredArgs, ContentResolver.QUERY_ARG_LIMIT));
    }

    @Test
    public void testAddsExtras_OnlyLimit() {
        Bundle args = new Bundle();
        args.putInt(ContentResolver.QUERY_ARG_LIMIT, 5);
        mCursor = new PageViewCursor(mDelegate, args);
        String[] honoredArgs = mCursor.getExtras()
                .getStringArray(ContentResolver.EXTRA_HONORED_ARGS);
        assertFalse(contains(honoredArgs, ContentResolver.QUERY_ARG_OFFSET));
        assertTrue(contains(honoredArgs, ContentResolver.QUERY_ARG_LIMIT));
    }

    @Test
    public void testGetWindow() {
        mCursor = new PageViewCursor(mDelegate, createArgs(5, 5));
        CursorWindow window = mCursor.getWindow();
        assertEquals(5, window.getNumRows());
    }

    @Test
    public void testWraps() {
        Bundle args = createArgs(5, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, args);
        assertTrue(wrapped instanceof PageViewCursor);
        assertEquals(5, wrapped.getCount());
    }

    @Test
    public void testWraps_NullExtras() {
        Bundle args = createArgs(5, 5);
        mDelegate.setExtras(null);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, args);
        assertTrue(wrapped instanceof PageViewCursor);
        assertEquals(5, wrapped.getCount());
    }

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

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

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

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

        Bundle args = createArgs(5, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, args);
        assertTrue(mDelegate == wrapped);
    }

    @Test
    public void testNoWrap_CursorsHasExistingPaging_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 args = createArgs(5, 5);
        Cursor wrapped = PageViewCursor.wrap(mDelegate, args);
        assertTrue(mDelegate == wrapped);
    }

    private static Bundle createArgs(int offset, int limit) {
        Bundle args = new Bundle();
        args.putInt(ContentResolver.QUERY_ARG_OFFSET, offset);
        args.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
        return args;
    }

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