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