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