1 /* 2 * Copyright (C) 2019 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.fuse; 18 19 import static com.android.providers.media.scan.MediaScanner.REASON_MOUNTED; 20 21 import android.annotation.BytesLong; 22 import android.content.ContentProviderClient; 23 import android.os.Environment; 24 import android.os.OperationCanceledException; 25 import android.os.ParcelFileDescriptor; 26 import android.os.storage.StorageManager; 27 import android.os.storage.StorageVolume; 28 import android.provider.MediaStore; 29 import android.service.storage.ExternalStorageService; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import com.android.modules.utils.build.SdkLevel; 36 import com.android.providers.media.MediaProvider; 37 import com.android.providers.media.MediaService; 38 import com.android.providers.media.MediaVolume; 39 40 import com.android.modules.utils.BackgroundThread; 41 42 import java.io.File; 43 import java.io.IOException; 44 import java.util.HashMap; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.UUID; 48 49 /** 50 * Handles filesystem I/O from other apps. 51 */ 52 public final class ExternalStorageServiceImpl extends ExternalStorageService { 53 private static final String TAG = "ExternalStorageServiceImpl"; 54 55 private static final Object sLock = new Object(); 56 private static final Map<String, FuseDaemon> sFuseDaemons = new HashMap<>(); 57 58 @Override onStartSession(@onNull String sessionId, int flag, @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath, @NonNull File lowerFileSystemPath)59 public void onStartSession(@NonNull String sessionId, /* @SessionFlag */ int flag, 60 @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath, 61 @NonNull File lowerFileSystemPath) { 62 Objects.requireNonNull(sessionId); 63 Objects.requireNonNull(deviceFd); 64 Objects.requireNonNull(upperFileSystemPath); 65 Objects.requireNonNull(lowerFileSystemPath); 66 67 MediaProvider mediaProvider = getMediaProvider(); 68 69 boolean uncachedMode = false; 70 if (SdkLevel.isAtLeastT()) { 71 StorageVolume vol = 72 getSystemService(StorageManager.class).getStorageVolume(upperFileSystemPath); 73 if (vol != null && vol.isExternallyManaged()) { 74 // Cache should be disabled when the volume is externally managed. 75 Log.i(TAG, "Disabling cache for externally managed volume " + upperFileSystemPath); 76 uncachedMode = true; 77 } 78 } 79 80 synchronized (sLock) { 81 if (sFuseDaemons.containsKey(sessionId)) { 82 Log.w(TAG, "Session already started with id: " + sessionId); 83 } else { 84 Log.i(TAG, "Starting session for id: " + sessionId); 85 // We only use the upperFileSystemPath because the media process is mounted as 86 // REMOUNT_MODE_PASS_THROUGH which guarantees that all /storage paths are bind 87 // mounts of the lower filesystem. 88 final String[] supportedTranscodingRelativePaths = 89 mediaProvider.getSupportedTranscodingRelativePaths().toArray(new String[0]); 90 final String[] supportedUncachedRelativePaths = 91 mediaProvider.getSupportedUncachedRelativePaths().toArray(new String[0]); 92 FuseDaemon daemon = new FuseDaemon(mediaProvider, this, deviceFd, sessionId, 93 upperFileSystemPath.getPath(), uncachedMode, 94 supportedTranscodingRelativePaths, supportedUncachedRelativePaths); 95 daemon.start(); 96 sFuseDaemons.put(sessionId, daemon); 97 } 98 } 99 } 100 101 @Override onVolumeStateChanged(@onNull StorageVolume vol)102 public void onVolumeStateChanged(@NonNull StorageVolume vol) throws IOException { 103 Objects.requireNonNull(vol); 104 105 MediaProvider mediaProvider = getMediaProvider(); 106 107 switch(vol.getState()) { 108 case Environment.MEDIA_MOUNTED: 109 MediaVolume volume = MediaVolume.fromStorageVolume(vol); 110 mediaProvider.attachVolume(volume, /* validate */ false, Environment.MEDIA_MOUNTED); 111 MediaService.queueVolumeScan(mediaProvider.getContext(), volume, REASON_MOUNTED); 112 break; 113 case Environment.MEDIA_UNMOUNTED: 114 case Environment.MEDIA_EJECTING: 115 case Environment.MEDIA_REMOVED: 116 case Environment.MEDIA_BAD_REMOVAL: 117 mediaProvider.detachVolume(MediaVolume.fromStorageVolume(vol)); 118 break; 119 default: 120 Log.i(TAG, "Ignoring volume state for vol:" + vol.getMediaStoreVolumeName() 121 + ". State: " + vol.getState()); 122 } 123 // Check for invalidation of cached volumes 124 mediaProvider.updateVolumes(); 125 } 126 127 @Override onEndSession(@onNull String sessionId)128 public void onEndSession(@NonNull String sessionId) { 129 Objects.requireNonNull(sessionId); 130 131 FuseDaemon daemon = onExitSession(sessionId); 132 133 if (daemon == null) { 134 Log.w(TAG, "Session already ended with id: " + sessionId); 135 } else { 136 Log.i(TAG, "Ending session for id: " + sessionId); 137 // The FUSE daemon cannot end the FUSE session itself, but if the FUSE filesystem 138 // is unmounted, the FUSE thread started in #onStartSession will exit and we can 139 // this allows us wait for confirmation. This blocks the client until the session has 140 // exited for sure 141 daemon.waitForExit(); 142 } 143 } 144 145 @Override onFreeCache(@onNull UUID volumeUuid, @BytesLong long bytes)146 public void onFreeCache(@NonNull UUID volumeUuid, @BytesLong long bytes) throws IOException { 147 Objects.requireNonNull(volumeUuid); 148 149 Log.i(TAG, "Free cache requested for " + bytes + " bytes"); 150 getMediaProvider().freeCache(bytes); 151 } 152 153 @Override onAnrDelayStarted(@onNull String packageName, int uid, int tid, int reason)154 public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid, int reason) { 155 Objects.requireNonNull(packageName); 156 157 getMediaProvider().onAnrDelayStarted(packageName, uid, tid, reason); 158 } 159 onExitSession(@onNull String sessionId)160 public FuseDaemon onExitSession(@NonNull String sessionId) { 161 Objects.requireNonNull(sessionId); 162 163 Log.i(TAG, "Exiting session for id: " + sessionId); 164 synchronized (sLock) { 165 return sFuseDaemons.remove(sessionId); 166 } 167 } 168 169 @Nullable getFuseDaemon(String sessionId)170 public static FuseDaemon getFuseDaemon(String sessionId) { 171 synchronized (sLock) { 172 return sFuseDaemons.get(sessionId); 173 } 174 } 175 getMediaProvider()176 private MediaProvider getMediaProvider() { 177 try (ContentProviderClient cpc = 178 getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY)) { 179 return (MediaProvider) cpc.getLocalContentProvider(); 180 } catch (OperationCanceledException e) { 181 throw new IllegalStateException("Failed to acquire MediaProvider", e); 182 } 183 } 184 } 185