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