Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ed7b77e8 authored by Martijn Coenen's avatar Martijn Coenen Committed by Android (Google) Code Review
Browse files

Merge "Revert "Use futures for binding and talking to the ExternalStorageService.""

parents a2402d95 734059ba
Loading
Loading
Loading
Loading
+175 −99
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import static android.service.storage.ExternalStorageService.FLAG_SESSION_TYPE_F
import static com.android.server.storage.StorageSessionController.ExternalStorageServiceException;

import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
@@ -35,27 +34,28 @@ import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.ParcelableException;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.storage.StorageManagerInternal;
import android.os.storage.StorageVolume;
import android.service.storage.ExternalStorageService;
import android.service.storage.IExternalStorageService;
import android.text.TextUtils;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Controls the lifecycle of the {@link ActiveConnection} to an {@link ExternalStorageService}
@@ -66,21 +66,26 @@ public final class StorageUserConnection {

    private static final int DEFAULT_REMOTE_TIMEOUT_SECONDS = 20;

    private final Object mSessionsLock = new Object();
    private final Object mLock = new Object();
    private final Context mContext;
    private final int mUserId;
    private final StorageSessionController mSessionController;
    private final ActiveConnection mActiveConnection = new ActiveConnection();
    private final boolean mIsDemoUser;
    @GuardedBy("mLock") private final Map<String, Session> mSessions = new HashMap<>();
    private final HandlerThread mHandlerThread;
    @GuardedBy("mLock") @Nullable private HandlerThread mHandlerThread;

    public StorageUserConnection(Context context, int userId, StorageSessionController controller) {
        mContext = Objects.requireNonNull(context);
        mUserId = Preconditions.checkArgumentNonnegative(userId);
        mSessionController = controller;
        mIsDemoUser = LocalServices.getService(UserManagerInternal.class)
                .getUserInfo(userId).isDemo();
        if (mIsDemoUser) {
            mHandlerThread = new HandlerThread("StorageUserConnectionThread-" + mUserId);
            mHandlerThread.start();
        }
    }

    /**
     * Creates and starts a storage {@link Session}.
@@ -96,12 +101,13 @@ public final class StorageUserConnection {
        Objects.requireNonNull(upperPath);
        Objects.requireNonNull(lowerPath);

        Session session = new Session(sessionId, upperPath, lowerPath);
        synchronized (mSessionsLock) {
        prepareRemote();
        synchronized (mLock) {
            Preconditions.checkArgument(!mSessions.containsKey(sessionId));
            Session session = new Session(sessionId, upperPath, lowerPath);
            mSessions.put(sessionId, session);
            mActiveConnection.startSessionLocked(session, pfd);
        }
        mActiveConnection.startSession(session, pfd);
    }

    /**
@@ -115,7 +121,10 @@ public final class StorageUserConnection {
        Objects.requireNonNull(sessionId);
        Objects.requireNonNull(vol);

        mActiveConnection.notifyVolumeStateChanged(sessionId, vol);
        prepareRemote();
        synchronized (mLock) {
            mActiveConnection.notifyVolumeStateChangedLocked(sessionId, vol);
        }
    }

    /**
@@ -126,7 +135,7 @@ public final class StorageUserConnection {
     * with {@link #waitForExit}.
     **/
    public Session removeSession(String sessionId) {
        synchronized (mSessionsLock) {
        synchronized (mLock) {
            return mSessions.remove(sessionId);
        }
    }
@@ -144,7 +153,10 @@ public final class StorageUserConnection {
        }

        Slog.i(TAG, "Waiting for session end " + session + " ...");
        mActiveConnection.endSession(session);
        prepareRemote();
        synchronized (mLock) {
            mActiveConnection.endSessionLocked(session);
        }
    }

    /** Restarts all available sessions for a user without blocking.
@@ -152,7 +164,7 @@ public final class StorageUserConnection {
     * Any failures will be ignored.
     **/
    public void resetUserSessions() {
        synchronized (mSessionsLock) {
        synchronized (mLock) {
            if (mSessions.isEmpty()) {
                // Nothing to reset if we have no sessions to restart; we typically
                // hit this path if the user was consciously shut down.
@@ -167,7 +179,7 @@ public final class StorageUserConnection {
     * Removes all sessions, without waiting.
     */
    public void removeAllSessions() {
        synchronized (mSessionsLock) {
        synchronized (mLock) {
            Slog.i(TAG, "Removing  " + mSessions.size() + " sessions for user: " + mUserId + "...");
            mSessions.clear();
        }
@@ -179,54 +191,68 @@ public final class StorageUserConnection {
     */
    public void close() {
        mActiveConnection.close();
        if (mIsDemoUser) {
            mHandlerThread.quit();
        }
    }

    /** Returns all created sessions. */
    public Set<String> getAllSessionIds() {
        synchronized (mSessionsLock) {
        synchronized (mLock) {
            return new HashSet<>(mSessions.keySet());
        }
    }

    @FunctionalInterface
    interface AsyncStorageServiceCall {
        void run(@NonNull IExternalStorageService service, RemoteCallback callback) throws
                RemoteException;
    private void prepareRemote() throws ExternalStorageServiceException {
        try {
            waitForLatch(mActiveConnection.bind(), "remote_prepare_user " + mUserId);
        } catch (IllegalStateException | TimeoutException e) {
            throw new ExternalStorageServiceException("Failed to prepare remote", e);
        }
    }

    private final class ActiveConnection implements AutoCloseable {
        private final Object mLock = new Object();
    private void waitForLatch(CountDownLatch latch, String reason) throws TimeoutException {
        try {
            if (!latch.await(DEFAULT_REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                // TODO(b/140025078): Call ActivityManager ANR API?
                Slog.wtf(TAG, "Failed to bind to the ExternalStorageService for user " + mUserId);
                throw new TimeoutException("Latch wait for " + reason + " elapsed");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Latch wait for " + reason + " interrupted");
        }
    }

    private final class ActiveConnection implements AutoCloseable {
        // Lifecycle connection to the external storage service, needed to unbind.
        @GuardedBy("mLock") @Nullable private ServiceConnection mServiceConnection;

        // A future that holds the remote interface
        @GuardedBy("mLock")
        @Nullable private CompletableFuture<IExternalStorageService> mRemoteFuture;

        // A list of outstanding futures for async calls, for which we are still waiting
        // for a callback. Used to unblock waiters if the service dies.
        @GuardedBy("mLock")
        private ArrayList<CompletableFuture<Void>> mOutstandingOps = new ArrayList<>();
        // True if we are connecting, either bound or binding
        // False && mRemote != null means we are connected
        // False && mRemote == null means we are neither connecting nor connected
        @GuardedBy("mLock") @Nullable private boolean mIsConnecting;
        // Binder object representing the external storage service.
        // Non-null indicates we are connected
        @GuardedBy("mLock") @Nullable private IExternalStorageService mRemote;
        // Exception, if any, thrown from #startSessionLocked or #endSessionLocked
        // Local variables cannot be referenced from a lambda expression :( so we
        // save the exception received in the callback here. Since we guard access
        // (and clear the exception state) with the same lock which we hold during
        // the entire transaction, there is no risk of race.
        @GuardedBy("mLock") @Nullable private ParcelableException mLastException;
        // Not guarded by any lock intentionally and non final because we cannot
        // reset latches so need to create a new one after one use
        private CountDownLatch mLatch;

        @Override
        public void close() {
            ServiceConnection oldConnection = null;
            synchronized (mLock) {
                Slog.i(TAG, "Closing connection for user " + mUserId);
                mIsConnecting = false;
                oldConnection = mServiceConnection;
                mServiceConnection = null;
                if (mRemoteFuture != null) {
                    // Let folks who are waiting for the connection know it ain't gonna happen
                    mRemoteFuture.cancel(true);
                    mRemoteFuture = null;
                }
                // Let folks waiting for callbacks from the remote know it ain't gonna happen
                for (CompletableFuture<Void> op : mOutstandingOps) {
                    op.cancel(true);
                }
                mOutstandingOps.clear();
                mRemote = null;
            }

            if (oldConnection != null) {
@@ -240,37 +266,37 @@ public final class StorageUserConnection {
            }
        }

        private void waitForAsync(AsyncStorageServiceCall asyncCall) throws Exception {
            CompletableFuture<IExternalStorageService> serviceFuture = connectIfNeeded();
            CompletableFuture<Void> opFuture = new CompletableFuture<>();
        public boolean isActiveLocked(Session session) {
            if (!session.isInitialisedLocked()) {
                Slog.i(TAG, "Session not initialised " + session);
                return false;
            }

            try {
                synchronized (mLock) {
                    mOutstandingOps.add(opFuture);
            if (mRemote == null) {
                throw new IllegalStateException("Valid session with inactive connection");
            }
                serviceFuture.thenCompose(service -> {
                    try {
                        asyncCall.run(service,
                                new RemoteCallback(result -> setResult(result, opFuture)));
                    } catch (RemoteException e) {
                        opFuture.completeExceptionally(e);
            return true;
        }

                    return opFuture;
                }).get(DEFAULT_REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            } finally {
                synchronized (mLock) {
                    mOutstandingOps.remove(opFuture);
                }
        public void startSessionLocked(Session session, ParcelFileDescriptor fd)
                throws ExternalStorageServiceException {
            if (!isActiveLocked(session)) {
                try {
                    fd.close();
                } catch (IOException e) {
                    // ignore
                }
                return;
            }

        public void startSession(Session session, ParcelFileDescriptor fd)
                throws ExternalStorageServiceException {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                waitForAsync((service, callback) -> service.startSession(session.sessionId,
                mRemote.startSession(session.sessionId,
                        FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE,
                        fd, session.upperPath, session.lowerPath, callback));
                        fd, session.upperPath, session.lowerPath, new RemoteCallback(result ->
                                setResultLocked(latch, result)));
                waitForLatch(latch, "start_session " + session);
                maybeThrowExceptionLocked();
            } catch (Exception e) {
                throw new ExternalStorageServiceException("Failed to start session: " + session, e);
            } finally {
@@ -282,49 +308,73 @@ public final class StorageUserConnection {
            }
        }

        public void endSession(Session session) throws ExternalStorageServiceException {
        public void endSessionLocked(Session session) throws ExternalStorageServiceException {
            if (!isActiveLocked(session)) {
                // Nothing to end, not started yet
                return;
            }

            CountDownLatch latch = new CountDownLatch(1);
            try {
                waitForAsync((service, callback) ->
                        service.endSession(session.sessionId, callback));
                mRemote.endSession(session.sessionId, new RemoteCallback(result ->
                        setResultLocked(latch, result)));
                waitForLatch(latch, "end_session " + session);
                maybeThrowExceptionLocked();
            } catch (Exception e) {
                throw new ExternalStorageServiceException("Failed to end session: " + session, e);
            }
        }


        public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) throws
        public void notifyVolumeStateChangedLocked(String sessionId, StorageVolume vol) throws
                ExternalStorageServiceException {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                waitForAsync((service, callback) ->
                        service.notifyVolumeStateChanged(sessionId, vol, callback));
                mRemote.notifyVolumeStateChanged(sessionId, vol, new RemoteCallback(
                        result -> setResultLocked(latch, result)));
                waitForLatch(latch, "notify_volume_state_changed " + vol);
                maybeThrowExceptionLocked();
            } catch (Exception e) {
                throw new ExternalStorageServiceException("Failed to notify volume state changed "
                        + "for vol : " + vol, e);
            }
        }

        private void setResult(Bundle result, CompletableFuture<Void> future) {
            ParcelableException ex = result.getParcelable(EXTRA_ERROR);
            if (ex != null) {
                future.completeExceptionally(ex);
            } else {
                future.complete(null);
        private void setResultLocked(CountDownLatch latch, Bundle result) {
            mLastException = result.getParcelable(EXTRA_ERROR);
            latch.countDown();
        }

        private void maybeThrowExceptionLocked() throws IOException {
            if (mLastException != null) {
                ParcelableException lastException = mLastException;
                mLastException = null;
                try {
                    lastException.maybeRethrow(IOException.class);
                } catch (IOException e) {
                    throw e;
                }
                throw new RuntimeException(lastException);
            }
        }

        private CompletableFuture<IExternalStorageService> connectIfNeeded() throws
                ExternalStorageServiceException {
        public CountDownLatch bind() throws ExternalStorageServiceException {
            ComponentName name = mSessionController.getExternalStorageServiceComponentName();
            if (name == null) {
                // Not ready to bind
                throw new ExternalStorageServiceException(
                        "Not ready to bind to the ExternalStorageService for user " + mUserId);
            }

            synchronized (mLock) {
                if (mRemoteFuture != null) {
                    return mRemoteFuture;
                }
                mRemoteFuture = new CompletableFuture<>();
                if (mRemote != null || mIsConnecting) {
                    // Connected or connecting (bound or binding)
                    // Will wait on a latch that will countdown when we connect, unless we are
                    // connected and the latch has already countdown, yay!
                    return mLatch;
                } // else neither connected nor connecting

                mLatch = new CountDownLatch(1);
                mIsConnecting = true;
                mServiceConnection = new ServiceConnection() {
                    @Override
                    public void onServiceConnected(ComponentName name, IBinder service) {
@@ -356,9 +406,16 @@ public final class StorageUserConnection {

                    private void handleConnection(IBinder service) {
                        synchronized (mLock) {
                            mRemoteFuture.complete(
                                    IExternalStorageService.Stub.asInterface(service));
                            if (mIsConnecting) {
                                mRemote = IExternalStorageService.Stub.asInterface(service);
                                mIsConnecting = false;
                                mLatch.countDown();
                                // Separate thread so we don't block the main thead
                                return;
                            }
                        }
                        Slog.wtf(TAG, "Connection closed to the ExternalStorageService for user "
                                + mUserId);
                    }

                    private void handleDisconnection() {
@@ -372,22 +429,36 @@ public final class StorageUserConnection {
                };

                Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
                // Schedule on a worker thread, because the system server main thread can be
                // very busy early in boot.
                if (mIsDemoUser) {
                    // Schedule on a worker thread for demo user to avoid deadlock
                    if (mContext.bindServiceAsUser(new Intent().setComponent(name),
                                    mServiceConnection,
                                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
                                    mHandlerThread.getThreadHandler(),
                                    UserHandle.of(mUserId))) {
                        Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
                    return mRemoteFuture;
                        return mLatch;
                    } else {
                        mIsConnecting = false;
                        throw new ExternalStorageServiceException(
                                "Failed to bind to the ExternalStorageService for user " + mUserId);
                    }
                } else {
                    if (mContext.bindServiceAsUser(new Intent().setComponent(name),
                                    mServiceConnection,
                                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
                                    UserHandle.of(mUserId))) {
                        Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
                        return mLatch;
                    } else {
                        mIsConnecting = false;
                        throw new ExternalStorageServiceException(
                                "Failed to bind to the ExternalStorageService for user " + mUserId);
                    }
                }
            }
        }
    }

    private static final class Session {
        public final String sessionId;
@@ -405,5 +476,10 @@ public final class StorageUserConnection {
            return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: "
                    + lowerPath + "]";
        }

        @GuardedBy("mLock")
        public boolean isInitialisedLocked() {
            return !TextUtils.isEmpty(upperPath) && !TextUtils.isEmpty(lowerPath);
        }
    }
}