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