1 /* 2 * Copyright (C) 2020 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 package android.scopedstorage.cts; 17 18 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY; 19 import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata; 20 import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_READ_QUERY; 21 import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_WRITE_QUERY; 22 import static android.scopedstorage.cts.lib.TestUtils.CAN_READ_WRITE_QUERY; 23 import static android.scopedstorage.cts.lib.TestUtils.CHECK_DATABASE_ROW_EXISTS_QUERY; 24 import static android.scopedstorage.cts.lib.TestUtils.CREATE_FILE_QUERY; 25 import static android.scopedstorage.cts.lib.TestUtils.CREATE_IMAGE_ENTRY_QUERY; 26 import static android.scopedstorage.cts.lib.TestUtils.DELETE_FILE_QUERY; 27 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXCEPTION; 28 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_CALLING_PKG; 29 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_PATH; 30 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_URI; 31 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILEPATH; 32 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ; 33 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE; 34 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY; 35 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_WRITE_QUERY; 36 import static android.scopedstorage.cts.lib.TestUtils.QUERY_TYPE; 37 import static android.scopedstorage.cts.lib.TestUtils.QUERY_URI; 38 import static android.scopedstorage.cts.lib.TestUtils.READDIR_QUERY; 39 import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_PARAMS_SEPARATOR; 40 import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_QUERY; 41 import static android.scopedstorage.cts.lib.TestUtils.SETATTR_QUERY; 42 import static android.scopedstorage.cts.lib.TestUtils.canOpen; 43 import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase; 44 import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri; 45 46 import android.app.Activity; 47 import android.content.ContentValues; 48 import android.content.Intent; 49 import android.database.Cursor; 50 import android.media.ExifInterface; 51 import android.net.Uri; 52 import android.os.Bundle; 53 import android.provider.MediaStore; 54 55 import androidx.annotation.Nullable; 56 import androidx.core.content.FileProvider; 57 58 import java.io.File; 59 import java.io.FileDescriptor; 60 import java.io.IOException; 61 import java.util.ArrayList; 62 import java.util.Collections; 63 import java.util.regex.Matcher; 64 import java.util.regex.Pattern; 65 66 /** 67 * Helper app for ScopedStorageTest. 68 * 69 * <p>Used to perform ScopedStorageTest functions as a different app. Based on the Query type 70 * app can perform different functions and send the result back to host app. 71 */ 72 public class ScopedStorageTestHelper extends Activity { 73 private static final String TAG = "ScopedStorageTestHelper"; 74 /** 75 * Regex that matches paths in all well-known package-specific directories, 76 * and which captures the directory type as the first group (data|media|obb) and the 77 * package name as the 2nd group. 78 */ 79 private static final Pattern PATTERN_OWNED_PATH = Pattern.compile( 80 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(data|media|obb)/([^/]+)(/?.*)?"); 81 82 @Override onCreate(Bundle savedInstanceState)83 public void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 String queryType = getIntent().getStringExtra(QUERY_TYPE); 86 queryType = queryType == null ? "null" : queryType; 87 Intent returnIntent; 88 try { 89 switch (queryType) { 90 case READDIR_QUERY: 91 returnIntent = sendDirectoryEntries(queryType); 92 break; 93 case CAN_READ_WRITE_QUERY: 94 case CREATE_FILE_QUERY: 95 case DELETE_FILE_QUERY: 96 case CAN_OPEN_FILE_FOR_READ_QUERY: 97 case CAN_OPEN_FILE_FOR_WRITE_QUERY: 98 case OPEN_FILE_FOR_READ_QUERY: 99 case OPEN_FILE_FOR_WRITE_QUERY: 100 case SETATTR_QUERY: 101 returnIntent = accessFile(queryType); 102 break; 103 case EXIF_METADATA_QUERY: 104 returnIntent = sendMetadata(queryType); 105 break; 106 case CREATE_IMAGE_ENTRY_QUERY: 107 returnIntent = createImageEntry(queryType); 108 break; 109 case RENAME_FILE_QUERY: 110 returnIntent = renameFile(queryType); 111 break; 112 case CHECK_DATABASE_ROW_EXISTS_QUERY: 113 returnIntent = checkDatabaseRowExists(queryType); 114 break; 115 case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE: 116 case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ: 117 returnIntent = isFileDescriptorRedactedForUri(queryType); 118 break; 119 case IS_URI_REDACTED_VIA_FILEPATH: 120 returnIntent = isFilePathForUriRedacted(queryType); 121 break; 122 case QUERY_URI: 123 returnIntent = queryForUri(queryType); 124 break; 125 case "null": 126 default: 127 throw new IllegalStateException( 128 "Unknown query received from launcher app: " + queryType); 129 } 130 } catch (Exception e) { 131 returnIntent = new Intent(queryType); 132 returnIntent.putExtra(INTENT_EXCEPTION, e); 133 } 134 sendBroadcast(returnIntent); 135 } 136 queryForUri(String queryType)137 private Intent queryForUri(String queryType) { 138 final Intent intent = new Intent(queryType); 139 final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI); 140 141 try { 142 final Cursor c = getContentResolver().query(uri, null, null, null); 143 intent.putExtra(queryType, c != null && c.moveToFirst()); 144 } catch (Exception e) { 145 intent.putExtra(INTENT_EXCEPTION, e); 146 } 147 148 return intent; 149 } 150 isFileDescriptorRedactedForUri(String queryType)151 private Intent isFileDescriptorRedactedForUri(String queryType) { 152 final Intent intent = new Intent(queryType); 153 final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI); 154 155 try { 156 final String mode = queryType.equals(IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE) 157 ? "w" : "r"; 158 FileDescriptor fd = getContentResolver().openFileDescriptor(uri, 159 mode).getFileDescriptor(); 160 ExifInterface exifInterface = new ExifInterface(fd); 161 intent.putExtra(queryType, exifInterface.getGpsDateTime() == -1); 162 } catch (Exception e) { 163 intent.putExtra(INTENT_EXCEPTION, e); 164 } 165 166 return intent; 167 } 168 isFilePathForUriRedacted(String queryType)169 private Intent isFilePathForUriRedacted(String queryType) { 170 final Intent intent = new Intent(queryType); 171 final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI); 172 173 try { 174 final Cursor c = getContentResolver().query(uri, null, null, null); 175 if (!c.moveToFirst()) { 176 intent.putExtra(INTENT_EXCEPTION, new IOException("")); 177 return intent; 178 } 179 final String path = c.getString(c.getColumnIndex(MediaStore.MediaColumns.DATA)); 180 ExifInterface redactedExifInf = new ExifInterface(path); 181 intent.putExtra(queryType, redactedExifInf.getGpsDateTime() == -1); 182 } catch (Exception e) { 183 intent.putExtra(INTENT_EXCEPTION, e); 184 } 185 186 return intent; 187 } 188 sendMetadata(String queryType)189 private Intent sendMetadata(String queryType) throws IOException { 190 final Intent intent = new Intent(queryType); 191 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 192 final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH); 193 if (EXIF_METADATA_QUERY.equals(queryType)) { 194 intent.putExtra(queryType, getExifMetadata(new File(filePath))); 195 } 196 } else { 197 throw new IllegalStateException( 198 EXIF_METADATA_QUERY + ": File path not set from launcher app"); 199 } 200 return intent; 201 } 202 sendDirectoryEntries(String queryType)203 private Intent sendDirectoryEntries(String queryType) throws IOException { 204 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 205 final String directoryPath = getIntent().getStringExtra(INTENT_EXTRA_PATH); 206 ArrayList<String> directoryEntriesList = new ArrayList<>(); 207 if (queryType.equals(READDIR_QUERY)) { 208 final String[] directoryEntries = new File(directoryPath).list(); 209 if (directoryEntries == null) { 210 throw new IOException( 211 "I/O exception while listing entries for " + directoryPath); 212 } 213 Collections.addAll(directoryEntriesList, directoryEntries); 214 } 215 final Intent intent = new Intent(queryType); 216 intent.putStringArrayListExtra(queryType, directoryEntriesList); 217 return intent; 218 } else { 219 throw new IllegalStateException( 220 READDIR_QUERY + ": Directory path not set from launcher app"); 221 } 222 } 223 createImageEntry(String queryType)224 private Intent createImageEntry(String queryType) throws Exception { 225 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 226 final String path = getIntent().getStringExtra(INTENT_EXTRA_PATH); 227 final String relativePath = path.substring(0, path.lastIndexOf('/')); 228 final String name = path.substring(path.lastIndexOf('/') + 1); 229 230 ContentValues values = new ContentValues(); 231 values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); 232 values.put(MediaStore.Images.Media.RELATIVE_PATH, relativePath); 233 values.put(MediaStore.Images.Media.DISPLAY_NAME, name); 234 235 getContentResolver().insert(getImageContentUri(), values); 236 237 final Intent intent = new Intent(queryType); 238 intent.putExtra(queryType, true); 239 return intent; 240 } else { 241 throw new IllegalStateException( 242 CREATE_IMAGE_ENTRY_QUERY + ": File path not set from launcher app"); 243 } 244 } 245 accessFile(String queryType)246 private Intent accessFile(String queryType) throws IOException { 247 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 248 final String packageName = getIntent().getStringExtra(INTENT_EXTRA_CALLING_PKG); 249 final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH); 250 final File file = new File(filePath); 251 final Intent intent = new Intent(queryType); 252 switch (queryType) { 253 case CAN_READ_WRITE_QUERY: 254 intent.putExtra(queryType, file.exists() && file.canRead() && file.canWrite()); 255 return intent; 256 case CREATE_FILE_QUERY: 257 maybeCreateParentDirInAndroid(file); 258 if (!file.getParentFile().exists()) { 259 file.getParentFile().mkdirs(); 260 } 261 intent.putExtra(queryType, file.createNewFile()); 262 return intent; 263 case DELETE_FILE_QUERY: 264 intent.putExtra(queryType, file.delete()); 265 return intent; 266 case SETATTR_QUERY: 267 int newTimeMillis = 12345000; 268 intent.putExtra(queryType, file.setLastModified(newTimeMillis)); 269 return intent; 270 case CAN_OPEN_FILE_FOR_READ_QUERY: 271 intent.putExtra(queryType, canOpen(file, false)); 272 return intent; 273 case CAN_OPEN_FILE_FOR_WRITE_QUERY: 274 intent.putExtra(queryType, canOpen(file, true)); 275 return intent; 276 case OPEN_FILE_FOR_READ_QUERY: 277 case OPEN_FILE_FOR_WRITE_QUERY: 278 Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), 279 getApplicationContext().getPackageName(), file); 280 intent.putExtra(queryType, contentUri); 281 // Grant permission to the possible instrumenting test apps 282 if (packageName != null) { 283 getApplicationContext().grantUriPermission(packageName, 284 contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION 285 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 286 } 287 return intent; 288 default: 289 throw new IllegalStateException( 290 "Unknown query received from launcher app: " + queryType); 291 } 292 } else { 293 throw new IllegalStateException(queryType + ": File path not set from launcher app"); 294 } 295 } 296 renameFile(String queryType)297 private Intent renameFile(String queryType) { 298 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 299 String[] paths = getIntent().getStringExtra(INTENT_EXTRA_PATH) 300 .split(RENAME_FILE_PARAMS_SEPARATOR); 301 File src = new File(paths[0]); 302 File dst = new File(paths[1]); 303 boolean result = src.renameTo(dst); 304 final Intent intent = new Intent(queryType); 305 intent.putExtra(queryType, result); 306 return intent; 307 } else { 308 throw new IllegalStateException( 309 queryType + ": File paths not set from launcher app"); 310 } 311 } 312 checkDatabaseRowExists(String queryType)313 private Intent checkDatabaseRowExists(String queryType) { 314 if (getIntent().hasExtra(INTENT_EXTRA_PATH)) { 315 final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH); 316 boolean result = 317 getFileRowIdFromDatabase(getContentResolver(), new File(filePath)) != -1; 318 final Intent intent = new Intent(queryType); 319 intent.putExtra(queryType, result); 320 return intent; 321 } else { 322 throw new IllegalStateException( 323 queryType + ": File path not set from launcher app"); 324 } 325 } 326 maybeCreateParentDirInAndroid(File file)327 private void maybeCreateParentDirInAndroid(File file) { 328 final String ownedPathType = getOwnedDirectoryType(file); 329 if (ownedPathType == null) { 330 return; 331 } 332 // Create the external app dir first. 333 if (createExternalAppDir(ownedPathType)) { 334 // Then create everything along the path. 335 file.getParentFile().mkdirs(); 336 } 337 } 338 createExternalAppDir(String name)339 private boolean createExternalAppDir(String name) { 340 // Apps are not allowed to create data/cache/obb etc under Android directly and are 341 // expected to call one of the following methods. 342 switch (name) { 343 case "data": 344 getApplicationContext().getExternalFilesDirs(null); 345 getApplicationContext().getExternalCacheDirs(); 346 return true; 347 case "obb": 348 getApplicationContext().getObbDirs(); 349 return true; 350 case "media": 351 getApplicationContext().getExternalMediaDirs(); 352 return true; 353 default: 354 return false; 355 } 356 } 357 358 /** 359 * Returns null if given path is not an owned path. 360 */ 361 @Nullable getOwnedDirectoryType(File path)362 private static String getOwnedDirectoryType(File path) { 363 final Matcher m = PATTERN_OWNED_PATH.matcher(path.getAbsolutePath()); 364 if (m.matches()) { 365 return m.group(1); 366 } 367 return null; 368 } 369 } 370