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.getExifMetadataFromFile;
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.DELETE_MEDIA_BY_URI_QUERY;
28 import static android.scopedstorage.cts.lib.TestUtils.DELETE_RECURSIVE_QUERY;
29 import static android.scopedstorage.cts.lib.TestUtils.FILE_EXISTS_QUERY;
30 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXCEPTION;
31 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_ARGS;
32 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_CALLING_PKG;
33 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_CONTENT;
34 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_PATH;
35 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_URI;
36 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILEPATH;
37 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ;
38 import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE;
39 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY;
40 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_WRITE_QUERY;
41 import static android.scopedstorage.cts.lib.TestUtils.QUERY_MAX_ROW_ID;
42 import static android.scopedstorage.cts.lib.TestUtils.QUERY_MEDIA_BY_URI_QUERY;
43 import static android.scopedstorage.cts.lib.TestUtils.QUERY_MIN_ROW_ID;
44 import static android.scopedstorage.cts.lib.TestUtils.QUERY_OWNER_PACKAGE_NAMES;
45 import static android.scopedstorage.cts.lib.TestUtils.QUERY_TYPE;
46 import static android.scopedstorage.cts.lib.TestUtils.QUERY_URI;
47 import static android.scopedstorage.cts.lib.TestUtils.QUERY_WITH_ARGS;
48 import static android.scopedstorage.cts.lib.TestUtils.READDIR_QUERY;
49 import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_PARAMS_SEPARATOR;
50 import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_QUERY;
51 import static android.scopedstorage.cts.lib.TestUtils.SETATTR_QUERY;
52 import static android.scopedstorage.cts.lib.TestUtils.UPDATE_MEDIA_BY_URI_QUERY;
53 import static android.scopedstorage.cts.lib.TestUtils.canOpen;
54 import static android.scopedstorage.cts.lib.TestUtils.deleteRecursively;
55 import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
56 import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri;
57 
58 import static com.google.common.truth.Truth.assertThat;
59 
60 import android.app.Activity;
61 import android.content.ContentResolver;
62 import android.content.ContentValues;
63 import android.content.Intent;
64 import android.database.Cursor;
65 import android.media.ExifInterface;
66 import android.net.Uri;
67 import android.os.Bundle;
68 import android.os.Environment;
69 import android.os.FileUtils;
70 import android.os.IBinder;
71 import android.os.Parcel;
72 import android.os.ParcelFileDescriptor;
73 import android.os.RemoteException;
74 import android.provider.MediaStore;
75 import android.util.Log;
76 
77 import androidx.annotation.Nullable;
78 import androidx.core.content.FileProvider;
79 
80 import com.google.common.base.Strings;
81 
82 import java.io.File;
83 import java.io.FileDescriptor;
84 import java.io.FileInputStream;
85 import java.io.FileOutputStream;
86 import java.io.IOException;
87 import java.util.ArrayList;
88 import java.util.Collections;
89 import java.util.HashSet;
90 import java.util.Set;
91 import java.util.regex.Matcher;
92 import java.util.regex.Pattern;
93 
94 /**
95  * Helper app for ScopedStorageTest.
96  *
97  * <p>Used to perform ScopedStorageTest functions as a different app. Based on the Query type
98  * app can perform different functions and send the result back to host app.
99  */
100 public class ScopedStorageTestHelper extends Activity {
101     private static final String TAG = "ScopedStorageTestHelper";
102     /**
103      * Regex that matches paths in all well-known package-specific directories,
104      * and which captures the directory type as the first group (data|media|obb) and the
105      * package name as the 2nd group.
106      */
107     private static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
108             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(data|media|obb)/([^/]+)(/?.*)?");
109 
110     @Override
onCreate(Bundle savedInstanceState)111     public void onCreate(Bundle savedInstanceState) {
112         super.onCreate(savedInstanceState);
113         String queryType = getIntent().getStringExtra(QUERY_TYPE);
114         queryType = queryType == null ? "null" : queryType;
115         Intent returnIntent;
116         try {
117             switch (queryType) {
118                 case READDIR_QUERY:
119                     returnIntent = sendDirectoryEntries(queryType);
120                     break;
121                 case FILE_EXISTS_QUERY:
122                 case CAN_READ_WRITE_QUERY:
123                 case CREATE_FILE_QUERY:
124                 case DELETE_FILE_QUERY:
125                 case DELETE_RECURSIVE_QUERY:
126                 case CAN_OPEN_FILE_FOR_READ_QUERY:
127                 case CAN_OPEN_FILE_FOR_WRITE_QUERY:
128                 case OPEN_FILE_FOR_READ_QUERY:
129                 case OPEN_FILE_FOR_WRITE_QUERY:
130                 case SETATTR_QUERY:
131                     returnIntent = accessFile(queryType);
132                     break;
133                 case DELETE_MEDIA_BY_URI_QUERY:
134                     returnIntent = deleteMediaByUri(queryType);
135                     break;
136                 case UPDATE_MEDIA_BY_URI_QUERY:
137                     returnIntent = updateMediaByUri(queryType);
138                     break;
139                 case QUERY_MEDIA_BY_URI_QUERY:
140                     returnIntent = queryMediaByUri(queryType);
141                     break;
142                 case EXIF_METADATA_QUERY:
143                     returnIntent = sendMetadata(queryType);
144                     break;
145                 case CREATE_IMAGE_ENTRY_QUERY:
146                     returnIntent = createImageEntry(queryType);
147                     break;
148                 case RENAME_FILE_QUERY:
149                     returnIntent = renameFile(queryType);
150                     break;
151                 case CHECK_DATABASE_ROW_EXISTS_QUERY:
152                     returnIntent = checkDatabaseRowExists(queryType);
153                     break;
154                 case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE:
155                 case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ:
156                     returnIntent = isFileDescriptorRedactedForUri(queryType);
157                     break;
158                 case IS_URI_REDACTED_VIA_FILEPATH:
159                     returnIntent = isFilePathForUriRedacted(queryType);
160                     break;
161                 case QUERY_URI:
162                     returnIntent = queryForUri(queryType);
163                     break;
164                 case QUERY_MAX_ROW_ID:
165                 case QUERY_MIN_ROW_ID:
166                     returnIntent = queryRowId(queryType);
167                     break;
168                 case QUERY_OWNER_PACKAGE_NAMES:
169                     returnIntent = queryOwnerPackageNames(queryType);
170                     break;
171                 case QUERY_WITH_ARGS:
172                     returnIntent = queryWithArgs(queryType);
173                     break;
174                 case "null":
175                 default:
176                     throw new IllegalStateException(
177                             "Unknown query received from launcher app: " + queryType);
178             }
179         } catch (Exception e) {
180             returnIntent = new Intent(queryType);
181             returnIntent.putExtra(INTENT_EXCEPTION, e);
182         }
183         sendBroadcast(returnIntent);
184     }
185 
queryForUri(String queryType)186     private Intent queryForUri(String queryType) {
187         final Intent intent = new Intent(queryType);
188         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
189 
190         try {
191             final Cursor c = getContentResolver().query(uri, null, null, null);
192             intent.putExtra(queryType, c != null && c.moveToFirst());
193         } catch (Exception e) {
194             intent.putExtra(INTENT_EXCEPTION, e);
195         }
196 
197         return intent;
198     }
199 
queryRowId(String queryType)200     private Intent queryRowId(String queryType) {
201         // Ensure M_E_S permission has been granted.
202         assertThat(Environment.isExternalStorageManager()).isTrue();
203         final Intent intent = new Intent(queryType);
204         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
205         final Bundle bundle = createQueryArgs(queryType);
206         try {
207             final Cursor c = getContentResolver().query(uri,
208                     new String[]{MediaStore.Files.FileColumns._ID}, bundle, null);
209             if (c != null && c.moveToFirst()) {
210                 intent.putExtra(queryType, c.getLong(0));
211             }
212         } catch (Exception e) {
213             intent.putExtra(INTENT_EXCEPTION, e);
214         }
215 
216         return intent;
217     }
218 
queryOwnerPackageNames(String queryType)219     private Intent queryOwnerPackageNames(String queryType) {
220         final Intent intent = new Intent(queryType);
221         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
222 
223         try {
224             final Cursor c = getContentResolver().query(uri,
225                     new String[]{MediaStore.MediaColumns.OWNER_PACKAGE_NAME}, null, null);
226             final Set<String> ownerPackageNames = new HashSet<>();
227             while (c.moveToNext()) {
228                 final String ownerPackageName = c.getString(0);
229                 if (!Strings.isNullOrEmpty(ownerPackageName)) {
230                     ownerPackageNames.add(ownerPackageName);
231                 }
232             }
233             intent.putExtra(queryType, ownerPackageNames.toArray(new String[0]));
234         } catch (Exception e) {
235             intent.putExtra(INTENT_EXCEPTION, e);
236         }
237 
238         return intent;
239     }
240 
deleteMediaByUri(String queryType)241     private Intent deleteMediaByUri(String queryType) {
242         final Intent intent = new Intent(queryType);
243         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
244 
245         try {
246             int rowsDeleted = getContentResolver().delete(uri, null);
247             intent.putExtra(queryType, rowsDeleted);
248         } catch (Exception e) {
249             intent.putExtra(INTENT_EXCEPTION, e);
250         }
251 
252         return intent;
253     }
254 
updateMediaByUri(String queryType)255     private Intent updateMediaByUri(String queryType) {
256         final Intent intent = new Intent(queryType);
257         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
258         final Bundle attributes = getIntent().getBundleExtra(INTENT_EXTRA_ARGS);
259 
260         final ContentValues values = new ContentValues();
261         for (String key : attributes.keySet()) {
262             values.put(key, attributes.getString(key));
263         }
264 
265         try {
266             getContentResolver().update(uri, values, null, null);
267             intent.putExtra(queryType, true);
268         } catch (Exception e) {
269             intent.putExtra(INTENT_EXCEPTION, e);
270         }
271 
272         return intent;
273     }
274 
queryMediaByUri(String queryType)275     private Intent queryMediaByUri(String queryType) {
276         final Intent intent = new Intent(queryType);
277         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
278         final Bundle projectionBundle = getIntent().getBundleExtra(INTENT_EXTRA_ARGS);
279 
280         String[] projection = null;
281         if (projectionBundle != null && !projectionBundle.isEmpty()) {
282             projection = projectionBundle.keySet().toArray(new String[0]);
283         }
284 
285         try (Cursor c = getContentResolver()
286                 .query(uri, projection, null, null)) {
287             final Bundle result = new Bundle();
288             if (c.getCount() == 1) {
289                 c.moveToFirst();
290                 for (String column : c.getColumnNames()) {
291                     result.putString(column, c.getString(c.getColumnIndex(column)));
292                 }
293             } else {
294                 Log.d(TAG, String.format("Uri in QUERY_MEDIA_BY_URI_QUERY query points "
295                         + "to %d media files", c.getCount()));
296             }
297             intent.putExtra(queryType, result);
298         } catch (Exception e) {
299             intent.putExtra(INTENT_EXCEPTION, e);
300         }
301 
302         return intent;
303     }
304 
queryWithArgs(String queryType)305     private Intent queryWithArgs(String queryType) {
306         final Intent intent = new Intent(queryType);
307         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
308         final Bundle args = getIntent().getBundleExtra(INTENT_EXTRA_ARGS);
309         try {
310             final Cursor c = getContentResolver().query(uri,
311                     new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, args, null);
312             intent.putExtra(queryType, c.getCount());
313         } catch (Exception e) {
314             intent.putExtra(INTENT_EXCEPTION, e);
315         }
316 
317         return intent;
318     }
319 
createQueryArgs(String queryType)320     private Bundle createQueryArgs(String queryType) {
321         switch (queryType){
322             case QUERY_MAX_ROW_ID:
323                 return createQueryArgToRetrieveMaximumRowId();
324             case QUERY_MIN_ROW_ID:
325                 return createQueryArgToRetrieveMinimumRowId();
326             default:
327                 throw new IllegalStateException(
328                         "Unknown query type received from launcher app: " + queryType);
329         }
330     }
331 
createQueryArgToRetrieveMinimumRowId()332     private Bundle createQueryArgToRetrieveMinimumRowId() {
333         final Bundle queryArgs = new Bundle();
334         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
335                 MediaStore.Files.FileColumns._ID + " ASC");
336         queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
337         return queryArgs;
338     }
339 
createQueryArgToRetrieveMaximumRowId()340     private Bundle createQueryArgToRetrieveMaximumRowId() {
341         final Bundle queryArgs = new Bundle();
342         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
343                 MediaStore.Files.FileColumns._ID + " DESC");
344         queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
345         return queryArgs;
346     }
347 
isFileDescriptorRedactedForUri(String queryType)348     private Intent isFileDescriptorRedactedForUri(String queryType) {
349         final Intent intent = new Intent(queryType);
350         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
351 
352         try {
353             final String mode = queryType.equals(IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE)
354                     ? "w" : "r";
355             try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, mode)) {
356                 FileDescriptor fd = pfd.getFileDescriptor();
357                 ExifInterface exifInterface = new ExifInterface(fd);
358                 intent.putExtra(queryType, exifInterface.getGpsDateTime() == -1);
359             }
360         } catch (Exception e) {
361             intent.putExtra(INTENT_EXCEPTION, e);
362         }
363 
364         return intent;
365     }
366 
isFilePathForUriRedacted(String queryType)367     private Intent isFilePathForUriRedacted(String queryType) {
368         final Intent intent = new Intent(queryType);
369         final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
370 
371         try {
372             final Cursor c = getContentResolver().query(uri, null, null, null);
373             if (!c.moveToFirst()) {
374                 intent.putExtra(INTENT_EXCEPTION, new IOException(""));
375                 return intent;
376             }
377             final String path = c.getString(c.getColumnIndex(MediaStore.MediaColumns.DATA));
378             ExifInterface redactedExifInf = new ExifInterface(path);
379             intent.putExtra(queryType, redactedExifInf.getGpsDateTime() == -1);
380         } catch (Exception e) {
381             intent.putExtra(INTENT_EXCEPTION, e);
382         }
383 
384         return intent;
385     }
386 
sendMetadata(String queryType)387     private Intent sendMetadata(String queryType) throws IOException {
388         final Intent intent = new Intent(queryType);
389         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
390             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
391             if (EXIF_METADATA_QUERY.equals(queryType)) {
392                 intent.putExtra(queryType, getExifMetadataFromFile(new File(filePath)));
393             }
394         } else {
395             throw new IllegalStateException(
396                     EXIF_METADATA_QUERY + ": File path not set from launcher app");
397         }
398         return intent;
399     }
400 
sendDirectoryEntries(String queryType)401     private Intent sendDirectoryEntries(String queryType) throws IOException {
402         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
403             final String directoryPath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
404             ArrayList<String> directoryEntriesList = new ArrayList<>();
405             if (queryType.equals(READDIR_QUERY)) {
406                 final String[] directoryEntries = new File(directoryPath).list();
407                 if (directoryEntries == null) {
408                     throw new IOException(
409                             "I/O exception while listing entries for " + directoryPath);
410                 }
411                 Collections.addAll(directoryEntriesList, directoryEntries);
412             }
413             final Intent intent = new Intent(queryType);
414             intent.putStringArrayListExtra(queryType, directoryEntriesList);
415             return intent;
416         } else {
417             throw new IllegalStateException(
418                     READDIR_QUERY + ": Directory path not set from launcher app");
419         }
420     }
421 
createImageEntry(String queryType)422     private Intent createImageEntry(String queryType) throws Exception {
423         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
424             final String path = getIntent().getStringExtra(INTENT_EXTRA_PATH);
425             final String relativePath = path.substring(0, path.lastIndexOf('/'));
426             final String name = path.substring(path.lastIndexOf('/') + 1);
427 
428             final ContentValues values = new ContentValues();
429             values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
430             values.put(MediaStore.Images.Media.RELATIVE_PATH, relativePath);
431             values.put(MediaStore.Images.Media.DISPLAY_NAME, name);
432 
433             final Uri imageUri = getContentResolver().insert(getImageContentUri(), values);
434 
435             final Intent intent = new Intent(queryType);
436             intent.putExtra(queryType, imageUri.toString());
437             return intent;
438         } else {
439             throw new IllegalStateException(
440                     CREATE_IMAGE_ENTRY_QUERY + ": File path not set from launcher app");
441         }
442     }
443 
accessFile(String queryType)444     private Intent accessFile(String queryType) throws IOException, RemoteException {
445         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
446             final String packageName = getIntent().getStringExtra(INTENT_EXTRA_CALLING_PKG);
447             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
448             final File file = new File(filePath);
449             final Intent intent = new Intent(queryType);
450             switch (queryType) {
451                 case FILE_EXISTS_QUERY:
452                     intent.putExtra(queryType, file.exists());
453                     return intent;
454                 case CAN_READ_WRITE_QUERY:
455                     intent.putExtra(queryType, file.exists() && file.canRead() && file.canWrite());
456                     return intent;
457                 case CREATE_FILE_QUERY:
458                     maybeCreateParentDirInAndroid(file);
459                     if (!file.getParentFile().exists()) {
460                         file.getParentFile().mkdirs();
461                     }
462                     boolean success = file.createNewFile();
463                     if (success && getIntent().hasExtra(INTENT_EXTRA_CONTENT)) {
464                         success = createFileContent(file);
465                     }
466                     intent.putExtra(queryType, success);
467                     return intent;
468                 case DELETE_FILE_QUERY:
469                     intent.putExtra(queryType, file.delete());
470                     return intent;
471                 case DELETE_RECURSIVE_QUERY:
472                     intent.putExtra(queryType, deleteRecursively(file));
473                     return intent;
474                 case SETATTR_QUERY:
475                     int newTimeMillis = 12345000;
476                     intent.putExtra(queryType, file.setLastModified(newTimeMillis));
477                     return intent;
478                 case CAN_OPEN_FILE_FOR_READ_QUERY:
479                     intent.putExtra(queryType, canOpen(file, false));
480                     return intent;
481                 case CAN_OPEN_FILE_FOR_WRITE_QUERY:
482                     intent.putExtra(queryType, canOpen(file, true));
483                     return intent;
484                 case OPEN_FILE_FOR_READ_QUERY:
485                 case OPEN_FILE_FOR_WRITE_QUERY:
486                     Uri contentUri = FileProvider.getUriForFile(getApplicationContext(),
487                             getApplicationContext().getPackageName(), file);
488                     intent.putExtra(queryType, contentUri);
489                     // Grant permission to the possible instrumenting test apps
490                     if (packageName != null) {
491                         getApplicationContext().grantUriPermission(packageName,
492                                 contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
493                                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
494                     }
495                     return intent;
496                 default:
497                     throw new IllegalStateException(
498                             "Unknown query received from launcher app: " + queryType);
499             }
500         } else {
501             throw new IllegalStateException(queryType + ": File path not set from launcher app");
502         }
503     }
504 
createFileContent(File file)505     private boolean createFileContent(File file) throws RemoteException {
506         final Bundle content = getIntent().getBundleExtra(
507                 INTENT_EXTRA_CONTENT);
508         IBinder binder = content.getBinder(INTENT_EXTRA_CONTENT);
509         Parcel reply = Parcel.obtain();
510         binder.transact(IBinder.FIRST_CALL_TRANSACTION, Parcel.obtain(), reply, 0);
511         try (ParcelFileDescriptor inputPFD = reply.readFileDescriptor();
512              FileInputStream fileInputStream = new FileInputStream(
513                      inputPFD.getFileDescriptor());
514              FileOutputStream outputStream = new FileOutputStream(file)) {
515             long copied = FileUtils.copy(fileInputStream, outputStream);
516             outputStream.getFD().sync();
517             return copied > 0;
518         } catch (Exception e) {
519             Log.e(TAG, e.getMessage(), e);
520             return false;
521         }
522     }
523 
renameFile(String queryType)524     private Intent renameFile(String queryType) {
525         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
526             String[] paths = getIntent().getStringExtra(INTENT_EXTRA_PATH)
527                     .split(RENAME_FILE_PARAMS_SEPARATOR);
528             File src = new File(paths[0]);
529             File dst = new File(paths[1]);
530             boolean result = src.renameTo(dst);
531             final Intent intent = new Intent(queryType);
532             intent.putExtra(queryType, result);
533             return intent;
534         } else {
535             throw new IllegalStateException(
536                     queryType + ": File paths not set from launcher app");
537         }
538     }
539 
checkDatabaseRowExists(String queryType)540     private Intent checkDatabaseRowExists(String queryType) {
541         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
542             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
543             boolean result =
544                     getFileRowIdFromDatabase(getContentResolver(), new File(filePath)) != -1;
545             final Intent intent = new Intent(queryType);
546             intent.putExtra(queryType, result);
547             return intent;
548         } else {
549             throw new IllegalStateException(
550                     queryType + ": File path not set from launcher app");
551         }
552     }
553 
maybeCreateParentDirInAndroid(File file)554     private void maybeCreateParentDirInAndroid(File file) {
555         final String ownedPathType = getOwnedDirectoryType(file);
556         if (ownedPathType == null) {
557             return;
558         }
559         // Create the external app dir first.
560         if (createExternalAppDir(ownedPathType)) {
561             // Then create everything along the path.
562             file.getParentFile().mkdirs();
563         }
564     }
565 
createExternalAppDir(String name)566     private boolean createExternalAppDir(String name) {
567         // Apps are not allowed to create data/cache/obb etc under Android directly and are
568         // expected to call one of the following methods.
569         switch (name) {
570             case "data":
571                 getApplicationContext().getExternalFilesDirs(null);
572                 getApplicationContext().getExternalCacheDirs();
573                 return true;
574             case "obb":
575                 getApplicationContext().getObbDirs();
576                 return true;
577             case "media":
578                 getApplicationContext().getExternalMediaDirs();
579                 return true;
580             default:
581                 return false;
582         }
583     }
584 
585     /**
586      * Returns null if given path is not an owned path.
587      */
588     @Nullable
getOwnedDirectoryType(File path)589     private static String getOwnedDirectoryType(File path) {
590         final Matcher m = PATTERN_OWNED_PATH.matcher(path.getAbsolutePath());
591         if (m.matches()) {
592             return m.group(1);
593         }
594         return null;
595     }
596 }
597