diff options
| -rw-r--r-- | core/java/android/database/TranslatingCursor.java | 229 | 
1 files changed, 229 insertions, 0 deletions
diff --git a/core/java/android/database/TranslatingCursor.java b/core/java/android/database/TranslatingCursor.java new file mode 100644 index 000000000000..58e65b28f0e0 --- /dev/null +++ b/core/java/android/database/TranslatingCursor.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2018 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 android.annotation.NonNull; +import android.content.ContentResolver; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.CancellationSignal; + +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Cursor that supports deprecation of {@code _data} like columns which represent raw filepaths, + * typically by replacing values with fake paths that the OS then offers to redirect to + * {@link ContentResolver#openFileDescriptor(Uri, String)}, which developers + * should be using directly. + * + * @hide + */ +public class TranslatingCursor extends CrossProcessCursorWrapper { +    public static class Config { +        public final Uri baseUri; +        public final String idColumn; +        public final String[] filePathColumns; + +        public Config(Uri baseUri, String idColumn, String... filePathColumns) { +            this.baseUri = baseUri; +            this.idColumn = idColumn; +            this.filePathColumns = filePathColumns; +        } +    } + +    public interface Translator { +        String translate(String data, long id); +    } + +    private final @NonNull Config mConfig; +    private final @NonNull Translator mTranslator; +    private final boolean mDropLast; + +    private final int mIdIndex; +    private final int[] mFilePathColIndices; + +    private TranslatingCursor(@NonNull Cursor cursor, @NonNull Config config, +            @NonNull Translator translator, boolean dropLast) { +        super(cursor); + +        mConfig = Objects.requireNonNull(config); +        mTranslator = Objects.requireNonNull(translator); +        mDropLast = dropLast; + +        mIdIndex = cursor.getColumnIndexOrThrow(config.idColumn); +        mFilePathColIndices = new int[config.filePathColumns.length]; +        for (int i = mFilePathColIndices.length - 1; i >= 0; --i) { +            mFilePathColIndices[i] = cursor.getColumnIndex(config.filePathColumns[i]); +        } +    } + +    @Override +    public int getColumnCount() { +        if (mDropLast) { +            return super.getColumnCount() - 1; +        } else { +            return super.getColumnCount(); +        } +    } + +    @Override +    public String[] getColumnNames() { +        if (mDropLast) { +            return Arrays.copyOfRange(super.getColumnNames(), 0, super.getColumnCount() - 1); +        } else { +            return super.getColumnNames(); +        } +    } + +    public static Cursor query(@NonNull Config config, @NonNull Translator translator, +            SQLiteQueryBuilder qb, SQLiteDatabase db, String[] projectionIn, String selection, +            String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, +            CancellationSignal signal) { +        final boolean requestedId = ArrayUtils.isEmpty(projectionIn) +                || ArrayUtils.contains(projectionIn, config.idColumn); +        final boolean requestedData = ArrayUtils.isEmpty(projectionIn) +                || ArrayUtils.containsAny(projectionIn, config.filePathColumns); + +        // If caller didn't request data, we have nothing to redirect +        if (!requestedData || !ContentResolver.DEPRECATE_DATA_COLUMNS) { +            return qb.query(db, projectionIn, selection, selectionArgs, +                    groupBy, having, sortOrder, limit, signal); +        } + +        // If caller didn't request id, we need to splice it in +        if (!requestedId) { +            projectionIn = ArrayUtils.appendElement(String.class, projectionIn, +                    config.idColumn); +        } + +        final Cursor c = qb.query(db, projectionIn, selection, selectionArgs, +                groupBy, having, sortOrder); +        return new TranslatingCursor(c, config, translator, !requestedId); +    } + +    @Override +    public void fillWindow(int position, CursorWindow window) { +        // Fill window directly to ensure data is rewritten +        DatabaseUtils.cursorFillWindow(this, position, window); +    } + +    @Override +    public CursorWindow getWindow() { +        // Returning underlying window risks leaking data +        return null; +    } + +    @Override +    public Cursor getWrappedCursor() { +        throw new UnsupportedOperationException( +                "Returning underlying cursor risks leaking data"); +    } + +    @Override +    public double getDouble(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getDouble(columnIndex); +        } +    } + +    @Override +    public float getFloat(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getFloat(columnIndex); +        } +    } + +    @Override +    public int getInt(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getInt(columnIndex); +        } +    } + +    @Override +    public long getLong(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getLong(columnIndex); +        } +    } + +    @Override +    public short getShort(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getShort(columnIndex); +        } +    } + +    @Override +    public String getString(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            return mTranslator.translate(super.getString(columnIndex), super.getLong(mIdIndex)); +        } else { +            return super.getString(columnIndex); +        } +    } + +    @Override +    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            super.copyStringToBuffer(columnIndex, buffer); +        } +    } + +    @Override +    public byte[] getBlob(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            throw new IllegalArgumentException(); +        } else { +            return super.getBlob(columnIndex); +        } +    } + +    @Override +    public int getType(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            return Cursor.FIELD_TYPE_STRING; +        } else { +            return super.getType(columnIndex); +        } +    } + +    @Override +    public boolean isNull(int columnIndex) { +        if (ArrayUtils.contains(mFilePathColIndices, columnIndex)) { +            return getString(columnIndex) == null; +        } else { +            return super.isNull(columnIndex); +        } +    } +}  |