/* * 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 android.util.ArraySet; 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 auxiliaryColumn; public final String[] translateColumns; public Config(Uri baseUri, String auxiliaryColumn, String... translateColumns) { this.baseUri = baseUri; this.auxiliaryColumn = auxiliaryColumn; this.translateColumns = translateColumns; } } public interface Translator { String translate(String data, int auxiliaryColumnIndex, String matchingColumn, Cursor cursor); } private final @NonNull Config mConfig; private final @NonNull Translator mTranslator; private final boolean mDropLast; private final int mAuxiliaryColumnIndex; private final ArraySet mTranslateColumnIndices; public TranslatingCursor(@NonNull Cursor cursor, @NonNull Config config, @NonNull Translator translator, boolean dropLast) { super(cursor); mConfig = Objects.requireNonNull(config); mTranslator = Objects.requireNonNull(translator); mDropLast = dropLast; mAuxiliaryColumnIndex = cursor.getColumnIndexOrThrow(config.auxiliaryColumn); mTranslateColumnIndices = new ArraySet<>(); for (int i = 0; i < cursor.getColumnCount(); ++i) { String columnName = cursor.getColumnName(i); if (ArrayUtils.contains(config.translateColumns, columnName)) { mTranslateColumnIndices.add(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 requestedAuxiliaryColumn = ArrayUtils.isEmpty(projectionIn) || ArrayUtils.contains(projectionIn, config.auxiliaryColumn); final boolean requestedTranslateColumns = ArrayUtils.isEmpty(projectionIn) || ArrayUtils.containsAny(projectionIn, config.translateColumns); // If caller didn't request any columns that need to be translated, // we have nothing to redirect if (!requestedTranslateColumns) { return qb.query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, limit, signal); } // If caller didn't request auxiliary column, we need to splice it in if (!requestedAuxiliaryColumn) { projectionIn = ArrayUtils.appendElement(String.class, projectionIn, config.auxiliaryColumn); } final Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder); return new TranslatingCursor(c, config, translator, !requestedAuxiliaryColumn); } @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(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getDouble(columnIndex); } } @Override public float getFloat(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getFloat(columnIndex); } } @Override public int getInt(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getInt(columnIndex); } } @Override public long getLong(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getLong(columnIndex); } } @Override public short getShort(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getShort(columnIndex); } } @Override public String getString(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { return mTranslator.translate(super.getString(columnIndex), mAuxiliaryColumnIndex, getColumnName(columnIndex), this); } else { return super.getString(columnIndex); } } @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { super.copyStringToBuffer(columnIndex, buffer); } } @Override public byte[] getBlob(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { throw new IllegalArgumentException(); } else { return super.getBlob(columnIndex); } } @Override public int getType(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { return Cursor.FIELD_TYPE_STRING; } else { return super.getType(columnIndex); } } @Override public boolean isNull(int columnIndex) { if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { return getString(columnIndex) == null; } else { return super.isNull(columnIndex); } } }