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