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