| /* |
| * 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<Integer> 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); |
| } |
| } |
| } |