blob: 7c4773033518e2f5885b13561c8288f5ac76935f [file] [log] [blame]
Zim42f1e9f2019-08-15 17:35:00 +01001/*
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
17package com.android.server.storage;
18
19import static android.service.storage.ExternalStorageService.EXTRA_ERROR;
20import static android.service.storage.ExternalStorageService.FLAG_SESSION_ATTRIBUTE_INDEXABLE;
21import static android.service.storage.ExternalStorageService.FLAG_SESSION_TYPE_FUSE;
22
23import static com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
24
25import android.annotation.MainThread;
26import android.annotation.Nullable;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.Intent;
30import android.content.ServiceConnection;
31import android.os.Bundle;
32import android.os.IBinder;
33import android.os.ParcelFileDescriptor;
34import android.os.ParcelableException;
35import android.os.RemoteCallback;
Zim42f1e9f2019-08-15 17:35:00 +010036import android.os.UserHandle;
Zim42f1e9f2019-08-15 17:35:00 +010037import android.service.storage.ExternalStorageService;
38import android.service.storage.IExternalStorageService;
Zim17be6f92019-09-25 14:37:55 +010039import android.text.TextUtils;
Zim42f1e9f2019-08-15 17:35:00 +010040import android.util.Slog;
41
42import com.android.internal.annotations.GuardedBy;
43import com.android.internal.util.Preconditions;
44
Zim42f1e9f2019-08-15 17:35:00 +010045import java.io.IOException;
46import java.util.HashMap;
Zim17be6f92019-09-25 14:37:55 +010047import java.util.HashSet;
Zim42f1e9f2019-08-15 17:35:00 +010048import java.util.Map;
Zim17be6f92019-09-25 14:37:55 +010049import java.util.Set;
Zim42f1e9f2019-08-15 17:35:00 +010050import java.util.concurrent.CountDownLatch;
51import java.util.concurrent.TimeUnit;
Zim17be6f92019-09-25 14:37:55 +010052import java.util.concurrent.TimeoutException;
Zim42f1e9f2019-08-15 17:35:00 +010053
54/**
55 * Controls the lifecycle of the {@link ActiveConnection} to an {@link ExternalStorageService}
Zim17be6f92019-09-25 14:37:55 +010056 * for a user and manages storage sessions associated with mounted volumes.
Zim42f1e9f2019-08-15 17:35:00 +010057 */
58public final class StorageUserConnection {
59 private static final String TAG = "StorageUserConnection";
Zim17be6f92019-09-25 14:37:55 +010060 private static final int REMOTE_TIMEOUT_SECONDS = 15;
Zim42f1e9f2019-08-15 17:35:00 +010061
62 private final Object mLock = new Object();
63 private final Context mContext;
64 private final int mUserId;
65 private final StorageSessionController mSessionController;
66 private final ActiveConnection mActiveConnection = new ActiveConnection();
67 @GuardedBy("mLock") private final Map<String, Session> mSessions = new HashMap<>();
68
69 public StorageUserConnection(Context context, int userId, StorageSessionController controller) {
70 mContext = Preconditions.checkNotNull(context);
71 mUserId = Preconditions.checkArgumentNonnegative(userId);
72 mSessionController = controller;
73 }
74
Zim17be6f92019-09-25 14:37:55 +010075 /**
76 * Creates and stores a storage {@link Session}.
77 *
Zim17be6f92019-09-25 14:37:55 +010078 * They must also be cleaned up with {@link #removeSession}.
79 *
80 * @throws IllegalArgumentException if a {@code Session} with {@code sessionId} already exists
81 */
Zim95eca1d2019-11-15 18:03:00 +000082 public void createSession(String sessionId, ParcelFileDescriptor pfd, String upperPath,
83 String lowerPath) {
Zim17be6f92019-09-25 14:37:55 +010084 Preconditions.checkNotNull(sessionId);
85 Preconditions.checkNotNull(pfd);
Zim95eca1d2019-11-15 18:03:00 +000086 Preconditions.checkNotNull(upperPath);
87 Preconditions.checkNotNull(lowerPath);
Zim17be6f92019-09-25 14:37:55 +010088
Zim42f1e9f2019-08-15 17:35:00 +010089 synchronized (mLock) {
Zim17be6f92019-09-25 14:37:55 +010090 Preconditions.checkArgument(!mSessions.containsKey(sessionId));
Zim95eca1d2019-11-15 18:03:00 +000091 mSessions.put(sessionId, new Session(sessionId, pfd, upperPath, lowerPath));
Zim17be6f92019-09-25 14:37:55 +010092 }
93 }
94
95 /**
96 * Starts an already created storage {@link Session} for {@code sessionId}.
97 *
98 * It is safe to call this multiple times, however if the session is already started,
99 * subsequent calls will be ignored.
100 *
101 * @throws ExternalStorageServiceException if the session failed to start
102 **/
103 public void startSession(String sessionId) throws ExternalStorageServiceException {
104 Session session;
105 synchronized (mLock) {
106 session = mSessions.get(sessionId);
107 }
108
109 prepareRemote();
110 synchronized (mLock) {
Zim42f1e9f2019-08-15 17:35:00 +0100111 mActiveConnection.startSessionLocked(session);
112 }
113 }
114
115 /**
Zim17be6f92019-09-25 14:37:55 +0100116 * Removes a session without ending it or waiting for exit.
Zim42f1e9f2019-08-15 17:35:00 +0100117 *
Zim17be6f92019-09-25 14:37:55 +0100118 * This should only be used if the session has certainly been ended because the volume was
119 * unmounted or the user running the session has been stopped. Otherwise, wait for session
120 * with {@link #waitForExit}.
Zim42f1e9f2019-08-15 17:35:00 +0100121 **/
Zim17be6f92019-09-25 14:37:55 +0100122 public Session removeSession(String sessionId) {
Zim42f1e9f2019-08-15 17:35:00 +0100123 synchronized (mLock) {
Zim17be6f92019-09-25 14:37:55 +0100124 Session session = mSessions.remove(sessionId);
Zim42f1e9f2019-08-15 17:35:00 +0100125 if (session != null) {
Zim17be6f92019-09-25 14:37:55 +0100126 session.close();
127 return session;
Zim42f1e9f2019-08-15 17:35:00 +0100128 }
Zim17be6f92019-09-25 14:37:55 +0100129 return null;
Zim42f1e9f2019-08-15 17:35:00 +0100130 }
131 }
132
Zim17be6f92019-09-25 14:37:55 +0100133
134 /**
135 * Removes a session and waits for exit
136 *
137 * @throws ExternalStorageServiceException if the session may not have exited
138 **/
139 public void removeSessionAndWait(String sessionId) throws ExternalStorageServiceException {
140 Session session = removeSession(sessionId);
141 if (session == null) {
142 Slog.i(TAG, "No session found for id: " + sessionId);
143 return;
144 }
145
146 Slog.i(TAG, "Waiting for session end " + session + " ...");
147 prepareRemote();
Zim42f1e9f2019-08-15 17:35:00 +0100148 synchronized (mLock) {
Zim17be6f92019-09-25 14:37:55 +0100149 mActiveConnection.endSessionLocked(session);
150 }
151 }
152
153 /** Starts all available sessions for a user without blocking. Any failures will be ignored. */
154 public void startAllSessions() {
155 try {
156 prepareRemote();
157 } catch (ExternalStorageServiceException e) {
158 Slog.e(TAG, "Failed to start all sessions for user: " + mUserId, e);
159 return;
160 }
161
162 synchronized (mLock) {
163 Slog.i(TAG, "Starting " + mSessions.size() + " sessions for user: " + mUserId + "...");
Zim42f1e9f2019-08-15 17:35:00 +0100164 for (Session session : mSessions.values()) {
Zim17be6f92019-09-25 14:37:55 +0100165 try {
166 mActiveConnection.startSessionLocked(session);
167 } catch (IllegalStateException | ExternalStorageServiceException e) {
168 // TODO: Don't crash process? We could get into process crash loop
169 Slog.e(TAG, "Failed to start " + session, e);
170 }
Zim42f1e9f2019-08-15 17:35:00 +0100171 }
172 }
173 }
174
Zim17be6f92019-09-25 14:37:55 +0100175 /**
176 * Closes the connection to the {@link ExternalStorageService}. The connection will typically
177 * be restarted after close.
178 */
179 public void close() {
180 mActiveConnection.close();
181 }
182
183 /** Throws an {@link IllegalArgumentException} if {@code path} is not ready for access */
184 public void checkPathReady(String path) {
Zim42f1e9f2019-08-15 17:35:00 +0100185 synchronized (mLock) {
186 for (Session session : mSessions.values()) {
Zim17be6f92019-09-25 14:37:55 +0100187 if (session.upperPath != null && path.startsWith(session.upperPath)) {
188 if (mActiveConnection.isActiveLocked(session)) {
189 return;
190 }
191 }
Zim42f1e9f2019-08-15 17:35:00 +0100192 }
Zim17be6f92019-09-25 14:37:55 +0100193 throw new IllegalStateException("Path not ready " + path);
194 }
195 }
196
197 /** Returns all created sessions. */
198 public Set<String> getAllSessionIds() {
199 synchronized (mLock) {
200 return new HashSet<>(mSessions.keySet());
201 }
202 }
203
204 private void prepareRemote() throws ExternalStorageServiceException {
205 try {
206 waitForLatch(mActiveConnection.bind(), "remote_prepare_user " + mUserId);
207 } catch (IllegalStateException | TimeoutException e) {
208 throw new ExternalStorageServiceException("Failed to prepare remote", e);
209 }
210 }
211
212 private void waitForLatch(CountDownLatch latch, String reason) throws TimeoutException {
213 try {
214 if (!latch.await(REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
215 // TODO(b/140025078): Call ActivityManager ANR API?
216 throw new TimeoutException("Latch wait for " + reason + " elapsed");
217 }
218 } catch (InterruptedException e) {
219 Thread.currentThread().interrupt();
220 throw new IllegalStateException("Latch wait for " + reason + " interrupted");
Zim42f1e9f2019-08-15 17:35:00 +0100221 }
222 }
223
224 private final class ActiveConnection implements AutoCloseable {
225 // Lifecycle connection to the external storage service, needed to unbind.
Zim42f1e9f2019-08-15 17:35:00 +0100226 @GuardedBy("mLock") @Nullable private ServiceConnection mServiceConnection;
Zim17be6f92019-09-25 14:37:55 +0100227 // True if we are connecting, either bound or binding
228 // False && mRemote != null means we are connected
229 // False && mRemote == null means we are neither connecting nor connected
230 @GuardedBy("mLock") @Nullable private boolean mIsConnecting;
Zim42f1e9f2019-08-15 17:35:00 +0100231 // Binder object representing the external storage service.
232 // Non-null indicates we are connected
233 @GuardedBy("mLock") @Nullable private IExternalStorageService mRemote;
234 // Exception, if any, thrown from #startSessionLocked or #endSessionLocked
235 // Local variables cannot be referenced from a lambda expression :( so we
236 // save the exception received in the callback here. Since we guard access
237 // (and clear the exception state) with the same lock which we hold during
238 // the entire transaction, there is no risk of race.
239 @GuardedBy("mLock") @Nullable private ParcelableException mLastException;
Zim17be6f92019-09-25 14:37:55 +0100240 // Not guarded by any lock intentionally and non final because we cannot
241 // reset latches so need to create a new one after one use
242 private CountDownLatch mLatch;
Zim42f1e9f2019-08-15 17:35:00 +0100243
244 @Override
245 public void close() {
Zim17be6f92019-09-25 14:37:55 +0100246 ServiceConnection oldConnection = null;
Zim42f1e9f2019-08-15 17:35:00 +0100247 synchronized (mLock) {
Zim17be6f92019-09-25 14:37:55 +0100248 Slog.i(TAG, "Closing connection for user " + mUserId);
249 mIsConnecting = false;
250 oldConnection = mServiceConnection;
Zim42f1e9f2019-08-15 17:35:00 +0100251 mServiceConnection = null;
252 mRemote = null;
253 }
Zim17be6f92019-09-25 14:37:55 +0100254
255 if (oldConnection != null) {
256 mContext.unbindService(oldConnection);
257 }
258 }
259
260 public boolean isActiveLocked(Session session) {
261 if (!session.isInitialisedLocked()) {
262 Slog.i(TAG, "Session not initialised " + session);
263 return false;
264 }
265
266 if (mRemote == null) {
267 throw new IllegalStateException("Valid session with inactive connection");
268 }
269 return true;
Zim42f1e9f2019-08-15 17:35:00 +0100270 }
271
272 public void startSessionLocked(Session session) throws ExternalStorageServiceException {
Zim17be6f92019-09-25 14:37:55 +0100273 if (!isActiveLocked(session)) {
Zim42f1e9f2019-08-15 17:35:00 +0100274 return;
275 }
276
277 CountDownLatch latch = new CountDownLatch(1);
Zim17be6f92019-09-25 14:37:55 +0100278 try (ParcelFileDescriptor dupedPfd = session.pfd.dup()) {
Zim42f1e9f2019-08-15 17:35:00 +0100279 mRemote.startSession(session.sessionId,
280 FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE,
Zim17be6f92019-09-25 14:37:55 +0100281 dupedPfd, session.upperPath, session.lowerPath, new RemoteCallback(result ->
Zim42f1e9f2019-08-15 17:35:00 +0100282 setResultLocked(latch, result)));
Zim17be6f92019-09-25 14:37:55 +0100283 waitForLatch(latch, "start_session " + session);
284 maybeThrowExceptionLocked();
285 } catch (Exception e) {
286 throw new ExternalStorageServiceException("Failed to start session: " + session, e);
Zim42f1e9f2019-08-15 17:35:00 +0100287 }
Zim42f1e9f2019-08-15 17:35:00 +0100288 }
289
290 public void endSessionLocked(Session session) throws ExternalStorageServiceException {
Zim17be6f92019-09-25 14:37:55 +0100291 session.close();
292 if (!isActiveLocked(session)) {
293 // Nothing to end, not started yet
Zim42f1e9f2019-08-15 17:35:00 +0100294 return;
295 }
296
297 CountDownLatch latch = new CountDownLatch(1);
298 try {
299 mRemote.endSession(session.sessionId, new RemoteCallback(result ->
Zim17be6f92019-09-25 14:37:55 +0100300 setResultLocked(latch, result)));
301 waitForLatch(latch, "end_session " + session);
302 maybeThrowExceptionLocked();
303 } catch (Exception e) {
304 throw new ExternalStorageServiceException("Failed to end session: " + session, e);
Zim42f1e9f2019-08-15 17:35:00 +0100305 }
Zim42f1e9f2019-08-15 17:35:00 +0100306 }
307
308 private void setResultLocked(CountDownLatch latch, Bundle result) {
309 mLastException = result.getParcelable(EXTRA_ERROR);
310 latch.countDown();
311 }
312
Zim17be6f92019-09-25 14:37:55 +0100313 private void maybeThrowExceptionLocked() throws IOException {
Zim42f1e9f2019-08-15 17:35:00 +0100314 if (mLastException != null) {
Zim17be6f92019-09-25 14:37:55 +0100315 ParcelableException lastException = mLastException;
Zim42f1e9f2019-08-15 17:35:00 +0100316 mLastException = null;
317 try {
Zim17be6f92019-09-25 14:37:55 +0100318 lastException.maybeRethrow(IOException.class);
Zim42f1e9f2019-08-15 17:35:00 +0100319 } catch (IOException e) {
Zim17be6f92019-09-25 14:37:55 +0100320 throw e;
Zim42f1e9f2019-08-15 17:35:00 +0100321 }
Zim17be6f92019-09-25 14:37:55 +0100322 throw new RuntimeException(lastException);
Zim42f1e9f2019-08-15 17:35:00 +0100323 }
Zim42f1e9f2019-08-15 17:35:00 +0100324 }
325
Zim17be6f92019-09-25 14:37:55 +0100326 public CountDownLatch bind() throws ExternalStorageServiceException {
Zim42f1e9f2019-08-15 17:35:00 +0100327 ComponentName name = mSessionController.getExternalStorageServiceComponentName();
328 if (name == null) {
Zim17be6f92019-09-25 14:37:55 +0100329 // Not ready to bind
330 throw new ExternalStorageServiceException(
331 "Not ready to bind to the ExternalStorageService for user " + mUserId);
Zim42f1e9f2019-08-15 17:35:00 +0100332 }
333
Zim17be6f92019-09-25 14:37:55 +0100334 synchronized (mLock) {
335 if (mRemote != null || mIsConnecting) {
336 // Connected or connecting (bound or binding)
337 // Will wait on a latch that will countdown when we connect, unless we are
338 // connected and the latch has already countdown, yay!
339 return mLatch;
340 } // else neither connected nor connecting
341
342 mLatch = new CountDownLatch(1);
343 mIsConnecting = true;
344 mServiceConnection = new ServiceConnection() {
Zim42f1e9f2019-08-15 17:35:00 +0100345 @Override
346 public void onServiceConnected(ComponentName name, IBinder service) {
347 Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]");
348 handleConnection(service);
349 }
350
351 @Override
352 @MainThread
353 public void onServiceDisconnected(ComponentName name) {
354 // Service crashed or process was killed, #onServiceConnected will be called
355 // Don't need to re-bind.
356 Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]");
357 handleDisconnection();
358 }
359
360 @Override
361 public void onBindingDied(ComponentName name) {
362 // Application hosting service probably got updated
363 // Need to re-bind.
364 Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]");
365 handleDisconnection();
366 }
367
368 @Override
369 public void onNullBinding(ComponentName name) {
Zim42f1e9f2019-08-15 17:35:00 +0100370 Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]");
371 }
372
373 private void handleConnection(IBinder service) {
374 synchronized (mLock) {
Zim17be6f92019-09-25 14:37:55 +0100375 if (mIsConnecting) {
Zim42f1e9f2019-08-15 17:35:00 +0100376 mRemote = IExternalStorageService.Stub.asInterface(service);
Zim17be6f92019-09-25 14:37:55 +0100377 mIsConnecting = false;
378 mLatch.countDown();
379 // Separate thread so we don't block the main thead
380 return;
Zim42f1e9f2019-08-15 17:35:00 +0100381 }
382 }
Zim17be6f92019-09-25 14:37:55 +0100383 Slog.wtf(TAG, "Connection closed to the ExternalStorageService for user "
384 + mUserId);
Zim42f1e9f2019-08-15 17:35:00 +0100385 }
386
387 private void handleDisconnection() {
Zim42f1e9f2019-08-15 17:35:00 +0100388 // Clear all sessions because we will need a new device fd since
389 // StorageManagerService will reset the device mount state and #startSession
390 // will be called for any required mounts.
Zim42f1e9f2019-08-15 17:35:00 +0100391 // Notify StorageManagerService so it can restart all necessary sessions
Zim17be6f92019-09-25 14:37:55 +0100392 close();
393 new Thread(StorageUserConnection.this::startAllSessions).start();
Zim42f1e9f2019-08-15 17:35:00 +0100394 }
395 };
Zim17be6f92019-09-25 14:37:55 +0100396 }
Zim42f1e9f2019-08-15 17:35:00 +0100397
398 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
Zim17be6f92019-09-25 14:37:55 +0100399 if (mContext.bindServiceAsUser(new Intent().setComponent(name), mServiceConnection,
400 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
401 UserHandle.of(mUserId))) {
Zim42f1e9f2019-08-15 17:35:00 +0100402 Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
Zim17be6f92019-09-25 14:37:55 +0100403 return mLatch;
Zim42f1e9f2019-08-15 17:35:00 +0100404 } else {
Zim17be6f92019-09-25 14:37:55 +0100405 synchronized (mLock) {
406 mIsConnecting = false;
407 }
408 throw new ExternalStorageServiceException(
409 "Failed to bind to the ExternalStorageService for user " + mUserId);
Zim42f1e9f2019-08-15 17:35:00 +0100410 }
411 }
412 }
413
Zim17be6f92019-09-25 14:37:55 +0100414 private static final class Session implements AutoCloseable {
Zim42f1e9f2019-08-15 17:35:00 +0100415 public final String sessionId;
Zim17be6f92019-09-25 14:37:55 +0100416 public final ParcelFileDescriptor pfd;
Zim95eca1d2019-11-15 18:03:00 +0000417 public final String lowerPath;
418 public final String upperPath;
Zim42f1e9f2019-08-15 17:35:00 +0100419
Zim95eca1d2019-11-15 18:03:00 +0000420 Session(String sessionId, ParcelFileDescriptor pfd, String upperPath, String lowerPath) {
Zim42f1e9f2019-08-15 17:35:00 +0100421 this.sessionId = sessionId;
Zim17be6f92019-09-25 14:37:55 +0100422 this.pfd = pfd;
Zim95eca1d2019-11-15 18:03:00 +0000423 this.upperPath = upperPath;
424 this.lowerPath = lowerPath;
Zim17be6f92019-09-25 14:37:55 +0100425 }
426
427 @Override
428 public void close() {
429 try {
430 pfd.close();
431 } catch (IOException e) {
432 Slog.i(TAG, "Failed to close session: " + this);
433 }
434 }
435
436 @Override
437 public String toString() {
438 return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: "
439 + lowerPath + "]";
440 }
441
442 @GuardedBy("mLock")
443 public boolean isInitialisedLocked() {
444 return !TextUtils.isEmpty(upperPath) && !TextUtils.isEmpty(lowerPath);
Zim42f1e9f2019-08-15 17:35:00 +0100445 }
446 }
447}