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 android.service.storage;
18 
19 import android.annotation.BytesLong;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.SdkConstant;
23 import android.annotation.SystemApi;
24 import android.app.Service;
25 import android.content.Intent;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.IBinder;
29 import android.os.ParcelFileDescriptor;
30 import android.os.ParcelableException;
31 import android.os.RemoteCallback;
32 import android.os.RemoteException;
33 import android.os.storage.StorageManager;
34 import android.os.storage.StorageVolume;
35 
36 import com.android.internal.os.BackgroundThread;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.UUID;
43 
44 /**
45  * A service to handle filesystem I/O from other apps.
46  *
47  * <p>To extend this class, you must declare the service in your manifest file with the
48  * {@link android.Manifest.permission#BIND_EXTERNAL_STORAGE_SERVICE} permission,
49  * and include an intent filter with the {@link #SERVICE_INTERFACE} action.
50  * For example:</p>
51  * <pre>
52  *     &lt;service android:name=".ExternalStorageServiceImpl"
53  *             android:exported="true"
54  *             android:priority="100"
55  *             android:permission="android.permission.BIND_EXTERNAL_STORAGE_SERVICE"&gt;
56  *         &lt;intent-filter&gt;
57  *             &lt;action android:name="android.service.storage.ExternalStorageService" /&gt;
58  *         &lt;/intent-filter&gt;
59  *     &lt;/service&gt;
60  * </pre>
61  * @hide
62  */
63 @SystemApi
64 public abstract class ExternalStorageService extends Service {
65     /**
66      * The Intent action that a service must respond to. Add it as an intent filter in the
67      * manifest declaration of the implementing service.
68      */
69     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
70     public static final String SERVICE_INTERFACE = "android.service.storage.ExternalStorageService";
71     /**
72      * Whether the session associated with the device file descriptor when calling
73      * {@link #onStartSession} is a FUSE session.
74      */
75     public static final int FLAG_SESSION_TYPE_FUSE = 1 << 0;
76 
77     /**
78      * Whether the upper file system path specified when calling {@link #onStartSession}
79      * should be indexed.
80      */
81     public static final int FLAG_SESSION_ATTRIBUTE_INDEXABLE = 1 << 1;
82 
83     /**
84      * {@link Bundle} key for a {@link String} value.
85      *
86      * {@hide}
87      */
88     public static final String EXTRA_SESSION_ID =
89             "android.service.storage.extra.session_id";
90     /**
91      * {@link Bundle} key for a {@link ParcelableException} value.
92      *
93      * {@hide}
94      */
95     public static final String EXTRA_ERROR =
96             "android.service.storage.extra.error";
97 
98     /**
99      * {@link Bundle} key for a package name {@link String} value.
100      *
101      * {@hide}
102      */
103     public static final String EXTRA_PACKAGE_NAME = "android.service.storage.extra.package_name";
104 
105     /** @hide */
106     @IntDef(flag = true, prefix = {"FLAG_SESSION_"},
107         value = {FLAG_SESSION_TYPE_FUSE, FLAG_SESSION_ATTRIBUTE_INDEXABLE})
108     @Retention(RetentionPolicy.SOURCE)
109     public @interface SessionFlag {}
110 
111     private final ExternalStorageServiceWrapper mWrapper = new ExternalStorageServiceWrapper();
112     private final Handler mHandler = BackgroundThread.getHandler();
113 
114     /**
115      * Called when the system starts a session associated with {@code deviceFd}
116      * identified by {@code sessionId} to handle filesystem I/O for other apps. The type of
117      * session and other attributes are passed in {@code flag}.
118      *
119      * <p> I/O is received as requests originating from {@code upperFileSystemPath} on
120      * {@code deviceFd}. Implementors should handle the I/O by responding to these requests
121      * using the data on the {@code lowerFileSystemPath}.
122      *
123      * <p> Additional calls to start a session for the same {@code sessionId} while the session
124      * is still starting or already started should have no effect.
125      *
126      * @param sessionId uniquely identifies a running session and used in {@link #onEndSession}
127      * @param flag specifies the type or additional attributes of a session
128      * @param deviceFd for intercepting IO from other apps
129      * @param upperFileSystemPath is the root path on which we are intercepting IO from other apps
130      * @param lowerFileSystemPath is the root path matching {@code upperFileSystemPath} containing
131      * the actual data apps are trying to access
132      */
onStartSession(@onNull String sessionId, @SessionFlag int flag, @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath, @NonNull File lowerFileSystemPath)133     public abstract void onStartSession(@NonNull String sessionId, @SessionFlag int flag,
134             @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath,
135             @NonNull File lowerFileSystemPath) throws IOException;
136 
137     /**
138      * Called when the system ends the session identified by {@code sessionId}. Implementors should
139      * stop handling filesystem I/O and clean up resources from the ended session.
140      *
141      * <p> Additional calls to end a session for the same {@code sessionId} while the session
142      * is still ending or has not started should have no effect.
143      */
onEndSession(@onNull String sessionId)144     public abstract void onEndSession(@NonNull String sessionId) throws IOException;
145 
146     /**
147      * Called when any volume's state changes.
148      *
149      * <p> This is required to communicate volume state changes with the Storage Service before
150      * broadcasting to other apps. The Storage Service needs to process any change in the volume
151      * state (before other apps receive a broadcast for the same) to update the database so that
152      * other apps have the correct view of the volume.
153      *
154      * <p> Blocks until the Storage Service processes/scans the volume or fails in doing so.
155      *
156      * @param vol name of the volume that was changed
157      */
onVolumeStateChanged(@onNull StorageVolume vol)158     public abstract void onVolumeStateChanged(@NonNull StorageVolume vol) throws IOException;
159 
160     /**
161      * Called when any cache held by the ExternalStorageService needs to be freed.
162      *
163      * <p> Blocks until the service frees the cache or fails in doing so.
164      *
165      * @param volumeUuid uuid of the {@link StorageVolume} from which cache needs to be freed
166      * @param bytes number of bytes which need to be freed
167      */
onFreeCache(@onNull UUID volumeUuid, @BytesLong long bytes)168     public void onFreeCache(@NonNull UUID volumeUuid, @BytesLong long bytes) throws IOException {
169         throw new UnsupportedOperationException("onFreeCacheRequested not implemented");
170     }
171 
172     /**
173      * Called when {@code packageName} is about to ANR. The {@link ExternalStorageService} can
174      * show a progress dialog for the {@code reason}.
175      *
176      * @param packageName the package name of the ANR'ing app
177      * @param uid the uid of the ANR'ing app
178      * @param tid the thread id of the ANR'ing app
179      * @param reason the reason the app is ANR'ing
180      */
onAnrDelayStarted(@onNull String packageName, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)181     public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid,
182             @StorageManager.AppIoBlockedReason int reason) {
183         throw new UnsupportedOperationException("onAnrDelayStarted not implemented");
184     }
185 
186     @Override
187     @NonNull
onBind(@onNull Intent intent)188     public final IBinder onBind(@NonNull Intent intent) {
189         return mWrapper;
190     }
191 
192     private class ExternalStorageServiceWrapper extends IExternalStorageService.Stub {
193         @Override
startSession(String sessionId, @SessionFlag int flag, ParcelFileDescriptor deviceFd, String upperPath, String lowerPath, RemoteCallback callback)194         public void startSession(String sessionId, @SessionFlag int flag,
195                 ParcelFileDescriptor deviceFd, String upperPath, String lowerPath,
196                 RemoteCallback callback) throws RemoteException {
197             mHandler.post(() -> {
198                 try {
199                     onStartSession(sessionId, flag, deviceFd, new File(upperPath),
200                             new File(lowerPath));
201                     sendResult(sessionId, null /* throwable */, callback);
202                 } catch (Throwable t) {
203                     sendResult(sessionId, t, callback);
204                 }
205             });
206         }
207 
208         @Override
notifyVolumeStateChanged(String sessionId, StorageVolume vol, RemoteCallback callback)209         public void notifyVolumeStateChanged(String sessionId, StorageVolume vol,
210                 RemoteCallback callback) {
211             mHandler.post(() -> {
212                 try {
213                     onVolumeStateChanged(vol);
214                     sendResult(sessionId, null /* throwable */, callback);
215                 } catch (Throwable t) {
216                     sendResult(sessionId, t, callback);
217                 }
218             });
219         }
220 
221         @Override
freeCache(String sessionId, String volumeUuid, long bytes, RemoteCallback callback)222         public void freeCache(String sessionId, String volumeUuid, long bytes,
223                 RemoteCallback callback) {
224             mHandler.post(() -> {
225                 try {
226                     onFreeCache(StorageManager.convert(volumeUuid), bytes);
227                     sendResult(sessionId, null /* throwable */, callback);
228                 } catch (Throwable t) {
229                     sendResult(sessionId, t, callback);
230                 }
231             });
232         }
233 
234         @Override
endSession(String sessionId, RemoteCallback callback)235         public void endSession(String sessionId, RemoteCallback callback) throws RemoteException {
236             mHandler.post(() -> {
237                 try {
238                     onEndSession(sessionId);
239                     sendResult(sessionId, null /* throwable */, callback);
240                 } catch (Throwable t) {
241                     sendResult(sessionId, t, callback);
242                 }
243             });
244         }
245 
246         @Override
notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)247         public void notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)
248                 throws RemoteException {
249             mHandler.post(() -> {
250                 try {
251                     onAnrDelayStarted(packageName, uid, tid, reason);
252                 } catch (Throwable t) {
253                     // Ignored
254                 }
255             });
256         }
257 
sendResult(String sessionId, Throwable throwable, RemoteCallback callback)258         private void sendResult(String sessionId, Throwable throwable, RemoteCallback callback) {
259             Bundle bundle = new Bundle();
260             bundle.putString(EXTRA_SESSION_ID, sessionId);
261             if (throwable != null) {
262                 bundle.putParcelable(EXTRA_ERROR, new ParcelableException(throwable));
263             }
264             callback.sendResult(bundle);
265         }
266     }
267 }
268