1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.database; 18 19 import android.annotation.NonNull; 20 import android.content.ContentResolver; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.database.sqlite.SQLiteQueryBuilder; 23 import android.net.Uri; 24 import android.os.CancellationSignal; 25 import android.util.ArraySet; 26 27 import com.android.internal.util.ArrayUtils; 28 29 import java.util.Arrays; 30 import java.util.Objects; 31 32 /** 33 * Cursor that supports deprecation of {@code _data} like columns which represent raw filepaths, 34 * typically by replacing values with fake paths that the OS then offers to redirect to 35 * {@link ContentResolver#openFileDescriptor(Uri, String)}, which developers 36 * should be using directly. 37 * 38 * @hide 39 */ 40 public class TranslatingCursor extends CrossProcessCursorWrapper { 41 public static class Config { 42 public final Uri baseUri; 43 public final String auxiliaryColumn; 44 public final String[] translateColumns; 45 Config(Uri baseUri, String auxiliaryColumn, String... translateColumns)46 public Config(Uri baseUri, String auxiliaryColumn, String... translateColumns) { 47 this.baseUri = baseUri; 48 this.auxiliaryColumn = auxiliaryColumn; 49 this.translateColumns = translateColumns; 50 } 51 } 52 53 public interface Translator { translate(String data, int auxiliaryColumnIndex, String matchingColumn, Cursor cursor)54 String translate(String data, int auxiliaryColumnIndex, 55 String matchingColumn, Cursor cursor); 56 } 57 58 private final @NonNull Config mConfig; 59 private final @NonNull Translator mTranslator; 60 private final boolean mDropLast; 61 62 private final int mAuxiliaryColumnIndex; 63 private final ArraySet<Integer> mTranslateColumnIndices; 64 TranslatingCursor(@onNull Cursor cursor, @NonNull Config config, @NonNull Translator translator, boolean dropLast)65 public TranslatingCursor(@NonNull Cursor cursor, @NonNull Config config, 66 @NonNull Translator translator, boolean dropLast) { 67 super(cursor); 68 69 mConfig = Objects.requireNonNull(config); 70 mTranslator = Objects.requireNonNull(translator); 71 mDropLast = dropLast; 72 73 mAuxiliaryColumnIndex = cursor.getColumnIndexOrThrow(config.auxiliaryColumn); 74 mTranslateColumnIndices = new ArraySet<>(); 75 for (int i = 0; i < cursor.getColumnCount(); ++i) { 76 String columnName = cursor.getColumnName(i); 77 if (ArrayUtils.contains(config.translateColumns, columnName)) { 78 mTranslateColumnIndices.add(i); 79 } 80 } 81 } 82 83 @Override getColumnCount()84 public int getColumnCount() { 85 if (mDropLast) { 86 return super.getColumnCount() - 1; 87 } else { 88 return super.getColumnCount(); 89 } 90 } 91 92 @Override getColumnNames()93 public String[] getColumnNames() { 94 if (mDropLast) { 95 return Arrays.copyOfRange(super.getColumnNames(), 0, super.getColumnCount() - 1); 96 } else { 97 return super.getColumnNames(); 98 } 99 } 100 query(@onNull 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)101 public static Cursor query(@NonNull Config config, @NonNull Translator translator, 102 SQLiteQueryBuilder qb, SQLiteDatabase db, String[] projectionIn, String selection, 103 String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, 104 CancellationSignal signal) { 105 final boolean requestedAuxiliaryColumn = ArrayUtils.isEmpty(projectionIn) 106 || ArrayUtils.contains(projectionIn, config.auxiliaryColumn); 107 final boolean requestedTranslateColumns = ArrayUtils.isEmpty(projectionIn) 108 || ArrayUtils.containsAny(projectionIn, config.translateColumns); 109 110 // If caller didn't request any columns that need to be translated, 111 // we have nothing to redirect 112 if (!requestedTranslateColumns) { 113 return qb.query(db, projectionIn, selection, selectionArgs, 114 groupBy, having, sortOrder, limit, signal); 115 } 116 117 // If caller didn't request auxiliary column, we need to splice it in 118 if (!requestedAuxiliaryColumn) { 119 projectionIn = ArrayUtils.appendElement(String.class, projectionIn, 120 config.auxiliaryColumn); 121 } 122 123 final Cursor c = qb.query(db, projectionIn, selection, selectionArgs, 124 groupBy, having, sortOrder); 125 return new TranslatingCursor(c, config, translator, !requestedAuxiliaryColumn); 126 } 127 128 @Override fillWindow(int position, CursorWindow window)129 public void fillWindow(int position, CursorWindow window) { 130 // Fill window directly to ensure data is rewritten 131 DatabaseUtils.cursorFillWindow(this, position, window); 132 } 133 134 @Override getWindow()135 public CursorWindow getWindow() { 136 // Returning underlying window risks leaking data 137 return null; 138 } 139 140 @Override getWrappedCursor()141 public Cursor getWrappedCursor() { 142 throw new UnsupportedOperationException( 143 "Returning underlying cursor risks leaking data"); 144 } 145 146 @Override getDouble(int columnIndex)147 public double getDouble(int columnIndex) { 148 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 149 throw new IllegalArgumentException(); 150 } else { 151 return super.getDouble(columnIndex); 152 } 153 } 154 155 @Override getFloat(int columnIndex)156 public float getFloat(int columnIndex) { 157 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 158 throw new IllegalArgumentException(); 159 } else { 160 return super.getFloat(columnIndex); 161 } 162 } 163 164 @Override getInt(int columnIndex)165 public int getInt(int columnIndex) { 166 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 167 throw new IllegalArgumentException(); 168 } else { 169 return super.getInt(columnIndex); 170 } 171 } 172 173 @Override getLong(int columnIndex)174 public long getLong(int columnIndex) { 175 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 176 throw new IllegalArgumentException(); 177 } else { 178 return super.getLong(columnIndex); 179 } 180 } 181 182 @Override getShort(int columnIndex)183 public short getShort(int columnIndex) { 184 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 185 throw new IllegalArgumentException(); 186 } else { 187 return super.getShort(columnIndex); 188 } 189 } 190 191 @Override getString(int columnIndex)192 public String getString(int columnIndex) { 193 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 194 return mTranslator.translate(super.getString(columnIndex), 195 mAuxiliaryColumnIndex, getColumnName(columnIndex), this); 196 } else { 197 return super.getString(columnIndex); 198 } 199 } 200 201 @Override copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)202 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 203 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 204 throw new IllegalArgumentException(); 205 } else { 206 super.copyStringToBuffer(columnIndex, buffer); 207 } 208 } 209 210 @Override getBlob(int columnIndex)211 public byte[] getBlob(int columnIndex) { 212 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 213 throw new IllegalArgumentException(); 214 } else { 215 return super.getBlob(columnIndex); 216 } 217 } 218 219 @Override getType(int columnIndex)220 public int getType(int columnIndex) { 221 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 222 return Cursor.FIELD_TYPE_STRING; 223 } else { 224 return super.getType(columnIndex); 225 } 226 } 227 228 @Override isNull(int columnIndex)229 public boolean isNull(int columnIndex) { 230 if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) { 231 return getString(columnIndex) == null; 232 } else { 233 return super.isNull(columnIndex); 234 } 235 } 236 } 237