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