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 * <service android:name=".ExternalStorageServiceImpl" 50 * android:exported="true" 51 * android:priority="100" 52 * android:permission="android.permission.BIND_EXTERNAL_STORAGE_SERVICE"> 53 * <intent-filter> 54 * <action android:name="android.service.storage.ExternalStorageService" /> 55 * </intent-filter> 56 * </service> 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