1 /* 2 * Copyright (C) 2024 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 com.android.providers.media; 18 19 import static android.provider.BaseColumns._ID; 20 import static android.provider.MediaStore.Files.FileColumns._USER_ID; 21 import static android.provider.MediaStore.MediaColumns.GENERATION_MODIFIED; 22 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME; 23 24 import android.content.ContentUris; 25 import android.content.ContentValues; 26 import android.net.Uri; 27 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Locale; 36 37 /** 38 * Utility class for revoking owner_grants when user deselects images that were created by the app 39 * in picker choice mode 40 */ 41 public class FilesOwnershipUtils { 42 43 private static final String FILES_TABLE_NAME = "files"; 44 private static final String TEMP_TABLE_NAME = "temp_file_ids_table"; 45 private static final String FILE_ID_COLUMN_NAME = "file_id"; 46 private static final int CHUNK_SIZE = 50; 47 private static final String TAG = FilesOwnershipUtils.class.getSimpleName(); 48 49 private final DatabaseHelper mExternalDatabase; 50 FilesOwnershipUtils(DatabaseHelper databaseHelper)51 public FilesOwnershipUtils(DatabaseHelper databaseHelper) { 52 mExternalDatabase = databaseHelper; 53 } 54 55 /** 56 * Revokes the access of the file by setting the owner_package_name as null in the files table. 57 * <p> 58 * Images or videos can be preselected because the app owns the file and has access to it. 59 * If the user deselects such a image/video, we revoke the access of the file by setting the 60 * owner_package_name as null in the files table. 61 * </p> 62 */ removeOwnerPackageNameForUris(@onNull String[] packages, @NonNull List<Uri> uris, int packageUserId)63 public void removeOwnerPackageNameForUris(@NonNull String[] packages, @NonNull List<Uri> uris, 64 int packageUserId) { 65 mExternalDatabase.runWithTransaction(db -> { 66 db.execSQL("CREATE TEMPORARY TABLE " + TEMP_TABLE_NAME + " (" + FILE_ID_COLUMN_NAME 67 + " INTEGER)"); 68 69 /* 70 * Insert all ids in temporary tables in batches. 71 * This will be used in update query below for setting owner_package_name to null 72 * if this file is currently owned by the app 73 */ 74 List<List<Uri>> uriChunks = splitArrayList(uris, CHUNK_SIZE); 75 for (List<Uri> chunk : uriChunks) { 76 String sqlQuery = String.format( 77 Locale.ROOT, 78 "INSERT INTO %s (%s) VALUES %s", 79 TEMP_TABLE_NAME, 80 FILE_ID_COLUMN_NAME, 81 getPlaceholderString(chunk.size()) 82 ); 83 84 db.execSQL(sqlQuery, chunk.stream().map(ContentUris::parseId).toArray()); 85 } 86 87 long generationNumber = DatabaseHelper.getGeneration(db); 88 89 /* 90 * sample query for setting owner_package_name as null : 91 * UPDATE files SET generation_modified = (SELECT generation from local_metadata), 92 * owner_package_name = NULL WHERE (EXISTS (SELECT file_id FROM temp_file_ids_table 93 * WHERE files_id = files._id) AND owner_package_name IN (com.example.package1, 94 * com.example.package2) AND _user_id = example_user_id) 95 */ 96 97 String whereClause = "(EXISTS (SELECT " + FILE_ID_COLUMN_NAME + " FROM " 98 + TEMP_TABLE_NAME + " WHERE " + FILE_ID_COLUMN_NAME + " = " + FILES_TABLE_NAME 99 + "." + _ID + ") " + "AND " + OWNER_PACKAGE_NAME + " IN (" 100 + getPlaceholderString(packages.length) + ") " + "AND " + _USER_ID + " = ?)"; 101 102 List<String> whereArgs = new ArrayList<>(Arrays.asList(packages)); 103 whereArgs.add(String.valueOf(packageUserId)); 104 105 ContentValues contentValues = new ContentValues(); 106 contentValues.put(GENERATION_MODIFIED, generationNumber); 107 contentValues.putNull(OWNER_PACKAGE_NAME); 108 109 int rowsAffected = db.update(FILES_TABLE_NAME, contentValues, whereClause, 110 whereArgs.toArray(String[]::new)); 111 112 Log.i(TAG, "Set owner package name to null for " + rowsAffected + " items for " 113 + "packages " + Arrays.toString(packages)); 114 115 db.execSQL("DROP TABLE " + TEMP_TABLE_NAME); 116 117 return null; 118 }); 119 } 120 getPlaceholderString(int length)121 private static String getPlaceholderString(int length) { 122 return String.join(", ", Collections.nCopies(length, "(?)")); 123 } 124 splitArrayList(List<T> list, int chunkSize)125 private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) { 126 List<List<T>> subLists = new ArrayList<>(); 127 for (int i = 0; i < list.size(); i += chunkSize) { 128 subLists.add(list.subList(i, Math.min(i + chunkSize, list.size()))); 129 } 130 return subLists; 131 } 132 } 133