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 android.annotation.NonNull; 20 import android.content.ContentResolver; 21 import android.content.res.AssetFileDescriptor; 22 import android.os.Bundle; 23 import android.os.CancellationSignal; 24 import android.os.ParcelFileDescriptor; 25 import android.os.Process; 26 import android.os.RemoteException; 27 import android.provider.IMPCancellationSignal; 28 import android.provider.IOpenAssetFileCallback; 29 import android.provider.IOpenFileCallback; 30 import android.provider.MediaStore; 31 import android.provider.OpenAssetFileRequest; 32 import android.provider.OpenFileRequest; 33 import android.provider.ParcelableException; 34 import android.util.Log; 35 36 import com.android.providers.media.util.FileUtils; 37 import com.android.providers.media.util.StringUtils; 38 39 import java.util.concurrent.Executor; 40 import java.util.concurrent.Executors; 41 import java.util.concurrent.ThreadFactory; 42 import java.util.concurrent.ThreadPoolExecutor; 43 import java.util.concurrent.atomic.AtomicInteger; 44 45 /** 46 * Utility class used to open picker files asynchronously. 47 * It manages a {@link ThreadPoolExecutor} that is being used to schedule 48 * pending open file requests. 49 */ 50 public class AsyncPickerFileOpener { 51 private static final String TAG = "AsyncPickerFileOpener"; 52 private static final int THREAD_POOL_SIZE = 8; 53 54 private static Executor sExecutor; 55 56 private final MediaProvider mMediaProvider; 57 private final PickerUriResolver mPickerUriResolver; 58 AsyncPickerFileOpener(@onNull MediaProvider mediaProvider, @NonNull PickerUriResolver pickerUriResolver)59 public AsyncPickerFileOpener(@NonNull MediaProvider mediaProvider, 60 @NonNull PickerUriResolver pickerUriResolver) { 61 mMediaProvider = mediaProvider; 62 mPickerUriResolver = pickerUriResolver; 63 } 64 65 /** 66 * Schedules a new open file request to open the requested file asynchronously. 67 * It validates that the request is valid and the requester has access before enqueueing 68 * the request in the thread pool 69 */ scheduleOpenFileAsync(@onNull OpenFileRequest request, @NonNull LocalCallingIdentity callingIdentity)70 public void scheduleOpenFileAsync(@NonNull OpenFileRequest request, 71 @NonNull LocalCallingIdentity callingIdentity) { 72 Log.i(TAG, "Async open file request created for " + request.getUri()); 73 74 mPickerUriResolver.checkPermissionForRequireOriginalQueryParam(request.getUri(), 75 callingIdentity); 76 mPickerUriResolver.checkUriPermission(request.getUri(), callingIdentity.pid, 77 callingIdentity.uid); 78 79 ensureExecutor(); 80 sExecutor.execute(() -> openFileAsync(request, callingIdentity)); 81 } 82 openFileAsync(@onNull OpenFileRequest request, @NonNull LocalCallingIdentity callingIdentity)83 private void openFileAsync(@NonNull OpenFileRequest request, 84 @NonNull LocalCallingIdentity callingIdentity) { 85 final IMPCancellationSignal iCancellationSignal = request.getCancellationSignal(); 86 final CancellationSignal cancellationSignal = iCancellationSignal != null 87 ? ((MPCancellationSignal) iCancellationSignal).mCancellationSignal 88 // explicitly create cancellation signal to help in case of caller death 89 : new CancellationSignal(); 90 91 final IOpenFileCallback callback = request.getCallback(); 92 try { 93 // cancel the operation in case the requester has died 94 callback.asBinder().linkToDeath(cancellationSignal::cancel, 0); 95 } catch (RemoteException e) { 96 Log.d(TAG, "Caller with uid " + callingIdentity.uid + " that requested opening " 97 + request.getUri() + " has died already"); 98 return; 99 } 100 101 final int tid = Process.myTid(); 102 mMediaProvider.addToPendingOpenMap(tid, callingIdentity.uid); 103 104 ParcelFileDescriptor pfd = null; 105 try { 106 cancellationSignal.throwIfCanceled(); 107 pfd = mPickerUriResolver.openFile( 108 request.getUri(), "r", cancellationSignal, callingIdentity); 109 callback.onSuccess(pfd); 110 } catch (RemoteException ignore) { 111 // ignore remote Exception as it means that the requester has died 112 } catch (Exception e) { 113 try { 114 Log.e(TAG, "Open file operation failed. Failed to open " + request.getUri(), e); 115 callback.onFailure(new ParcelableException(e)); 116 } catch (RemoteException ignore) { 117 // ignore remote exception as it means the requester has died 118 } 119 } finally { 120 if (pfd != null) { 121 // Closing the file descriptor on this side as Binder will dup it 122 FileUtils.closeQuietly(pfd); 123 } 124 mMediaProvider.removeFromPendingOpenMap(tid); 125 } 126 } 127 128 /** 129 * Schedules a new open asset file request to open the requested file asynchronously. 130 * It validates that the request is valid and the requester has access before enqueueing 131 * the request in the thread pool 132 */ scheduleOpenAssetFileAsync(@onNull OpenAssetFileRequest request, @NonNull LocalCallingIdentity callingIdentity)133 public void scheduleOpenAssetFileAsync(@NonNull OpenAssetFileRequest request, 134 @NonNull LocalCallingIdentity callingIdentity) { 135 Log.i(TAG, "Async open asset file request created for " + request.getUri()); 136 137 mPickerUriResolver.checkPermissionForRequireOriginalQueryParam(request.getUri(), 138 callingIdentity); 139 mPickerUriResolver.checkUriPermission(request.getUri(), callingIdentity.pid, 140 callingIdentity.uid); 141 142 ensureExecutor(); 143 sExecutor.execute(() -> openAssetFileAsync(request, callingIdentity)); 144 } 145 openAssetFileAsync(@onNull OpenAssetFileRequest request, @NonNull LocalCallingIdentity callingIdentity)146 private void openAssetFileAsync(@NonNull OpenAssetFileRequest request, 147 @NonNull LocalCallingIdentity callingIdentity) { 148 final IMPCancellationSignal iCancellationSignal = request.getCancellationSignal(); 149 final CancellationSignal cancellationSignal = iCancellationSignal != null 150 ? ((MPCancellationSignal) iCancellationSignal).mCancellationSignal 151 // explicitly create cancellation signal to help in case of caller death 152 : new CancellationSignal(); 153 154 final IOpenAssetFileCallback callback = request.getCallback(); 155 try { 156 // cancel the operation in case the requester has died 157 callback.asBinder().linkToDeath(cancellationSignal::cancel, 0); 158 } catch (RemoteException e) { 159 Log.d(TAG, "Caller with uid " + request.getUri() + " that requested opening " 160 + request.getUri() + " has died already"); 161 return; 162 } 163 164 final Bundle opts = request.getOpts(); 165 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 166 && StringUtils.startsWithIgnoreCase(request.getMimeType(), "image/"); 167 168 if (opts != null) { 169 opts.remove(MediaStore.EXTRA_MODE); 170 } 171 172 final int tid = Process.myTid(); 173 mMediaProvider.addToPendingOpenMap(tid, callingIdentity.uid); 174 AssetFileDescriptor afd = null; 175 try { 176 cancellationSignal.throwIfCanceled(); 177 afd = mPickerUriResolver.openTypedAssetFile( 178 request.getUri(), request.getMimeType(), opts, cancellationSignal, 179 callingIdentity, wantsThumb); 180 callback.onSuccess(afd); 181 } catch (RemoteException ignore) { 182 // ignore remote Exception as it means that the requester has died 183 } catch (Exception e) { 184 Log.e(TAG, "Open file operation failed. Failed to open " + request.getUri(), e); 185 try { 186 callback.onFailure(new ParcelableException(e)); 187 } catch (RemoteException ignore) { 188 // ignore remote Exception as it means that the requester has died 189 } 190 } finally { 191 if (afd != null) { 192 // Closing the file descriptor on this side as Binder will dup it 193 FileUtils.closeQuietly(afd); 194 } 195 mMediaProvider.removeFromPendingOpenMap(tid); 196 } 197 } 198 ensureExecutor()199 private static void ensureExecutor() { 200 synchronized (AsyncPickerFileOpener.class) { 201 if (sExecutor == null) { 202 sExecutor = Executors.newFixedThreadPool(THREAD_POOL_SIZE, new ThreadFactory() { 203 final AtomicInteger mCount = new AtomicInteger(1); 204 205 @Override 206 public Thread newThread(Runnable r) { 207 return new Thread( 208 r, "AsyncPickerFileOpener#" + mCount.getAndIncrement()); 209 } 210 }); 211 } 212 } 213 } 214 } 215