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

Commit 45d94a48 authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSession2Service: Initial commit

Bug: 122563346
Test: Build
Change-Id: I250ee493837bfa7964fa7baf3d11f1673c879010
parent 2499cc2f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -476,6 +476,7 @@ java_defaults {
        "media/java/android/media/IMediaRouterClient.aidl",
        "media/java/android/media/IMediaRouterService.aidl",
        "media/java/android/media/IMediaSession2.aidl",
        "media/java/android/media/IMediaSession2Service.aidl",
        "media/java/android/media/IMediaScannerListener.aidl",
        "media/java/android/media/IMediaScannerService.aidl",
        "media/java/android/media/IPlaybackConfigDispatcher.aidl",
+32 −0
Original line number Diff line number Diff line
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media;

import android.os.Bundle;
import android.media.Controller2Link;

/**
 * Interface from MediaController2 to MediaSession2Service.
 * <p>
 * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
 * and holds calls from controller to make controller owner(s) frozen.
 * @hide
 */
oneway interface IMediaSession2Service {
    void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
    // Next Id : 1
}
+112 −6
Original line number Diff line number Diff line
@@ -26,12 +26,16 @@ import static android.media.Session2Token.TYPE_SESSION;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -63,6 +67,7 @@ public class MediaController2 implements AutoCloseable {
    private final Executor mCallbackExecutor;
    private final Controller2Link mControllerStub;
    private final Handler mResultHandler;
    private final SessionServiceConnection mServiceConnection;

    private final Object mLock = new Object();
    //@GuardedBy("mLock")
@@ -118,16 +123,25 @@ public class MediaController2 implements AutoCloseable {
        mPendingCommands = new ArrayMap<>();
        mRequestedCommandSeqNumbers = new ArraySet<>();

        boolean connectRequested;
        if (token.getType() == TYPE_SESSION) {
            connectToSession();
            mServiceConnection = null;
            connectRequested = requestConnectToSession();
        } else {
            // TODO: Handle connect to session service.
            mServiceConnection = new SessionServiceConnection();
            connectRequested = requestConnectToService();
        }
        if (!connectRequested) {
            close();
        }
    }

    @Override
    public void close() {
        synchronized (mLock) {
            if (mServiceConnection != null) {
                mContext.unbindService(mServiceConnection);
            }
            if (mSessionBinder != null) {
                try {
                    mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
@@ -299,18 +313,55 @@ public class MediaController2 implements AutoCloseable {
        }
    }

    private void connectToSession() {
        Session2Link sessionBinder = mSessionToken.getSessionLink();
    private Bundle createConnectionRequest() {
        Bundle connectionRequest = new Bundle();
        connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
        connectionRequest.putInt(KEY_PID, Process.myPid());
        return connectionRequest;
    }

    private boolean requestConnectToSession() {
        Session2Link sessionBinder = mSessionToken.getSessionLink();
        Bundle connectionRequest = createConnectionRequest();
        try {
            sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
        } catch (RuntimeException e) {
            Log.w(TAG, "Failed to call connection request. Framework will retry"
                    + " automatically");
            Log.w(TAG, "Failed to call connection request", e);
            return false;
        }
        return true;
    }

    private boolean requestConnectToService() {
        // Service. Needs to get fresh binder whenever connection is needed.
        final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
        intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());

        // Use bindService() instead of startForegroundService() to start session service for three
        // reasons.
        // 1. Prevent session service owner's stopSelf() from destroying service.
        //    With the startForegroundService(), service's call of stopSelf() will trigger immediate
        //    onDestroy() calls on the main thread even when onConnect() is running in another
        //    thread.
        // 2. Minimize APIs for developers to take care about.
        //    With bindService(), developers only need to take care about Service.onBind()
        //    but Service.onStartCommand() should be also taken care about with the
        //    startForegroundService().
        // 3. Future support for UI-less playback
        //    If a service wants to keep running, it should be either foreground service or
        //    bound service. But there had been request for the feature for system apps
        //    and using bindService() will be better fit with it.
        synchronized (mLock) {
            boolean result = mContext.bindService(
                    intent, mServiceConnection, Context.BIND_AUTO_CREATE);
            if (!result) {
                Log.w(TAG, "bind to " + mSessionToken + " failed");
                return false;
            } else if (DEBUG) {
                Log.d(TAG, "bind to " + mSessionToken + " succeeded");
            }
        }
        return true;
    }

    /**
@@ -367,4 +418,59 @@ public class MediaController2 implements AutoCloseable {
        public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token,
                @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
    }

    // This will be called on the main thread.
    private class SessionServiceConnection implements ServiceConnection {
        SessionServiceConnection() {
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // Note that it's always main-thread.
            boolean connectRequested = false;
            try {
                if (DEBUG) {
                    Log.d(TAG, "onServiceConnected " + name + " " + this);
                }
                // Sanity check
                if (!mSessionToken.getPackageName().equals(name.getPackageName())) {
                    Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName()
                            + " but is connected to " + name);
                    return;
                }
                IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service);
                if (iService == null) {
                    Log.wtf(TAG, "Service interface is missing.");
                    return;
                }
                Bundle connectionRequest = createConnectionRequest();
                iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
                connectRequested = true;
            } catch (RemoteException e) {
                Log.w(TAG, "Service " + name + " has died prematurely", e);
            } finally {
                if (!connectRequested) {
                    close();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // Temporal lose of the binding because of the service crash. System will automatically
            // rebind, so just no-op.
            if (DEBUG) {
                Log.w(TAG, "Session service " + name + " is disconnected.");
            }
            close();
        }

        @Override
        public void onBindingDied(ComponentName name) {
            // Permanent lose of the binding because of the service package update or removed.
            // This SessionServiceRecord will be removed accordingly, but forget session binder here
            // for sure.
            close();
        }
    }
}
+115 −111
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import android.content.Context;
import android.content.Intent;
import android.media.session.MediaSessionManager;
import android.media.session.MediaSessionManager.RemoteUserInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Process;
@@ -41,7 +40,6 @@ import android.util.ArraySet;
import android.util.Log;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -117,14 +115,18 @@ public class MediaSession2 implements AutoCloseable {
    @Override
    public void close() {
        try {
            synchronized (MediaSession2.class) {
                SESSION_ID_LIST.remove(mSessionId);
            }
            Collection<ControllerInfo> controllerInfos;
            List<ControllerInfo> controllerInfos;
            synchronized (mLock) {
                controllerInfos = mConnectedControllers.values();
                mConnectedControllers.clear();
                if (mClosed) {
                    return;
                }
                mClosed = true;
                controllerInfos = getConnectedControllers();
                mConnectedControllers.clear();
                mCallback.onSessionClosed(this);
            }
            synchronized (MediaSession2.class) {
                SESSION_ID_LIST.remove(mSessionId);
            }
            for (ControllerInfo info : controllerInfos) {
                info.notifyDisconnected();
@@ -160,10 +162,7 @@ public class MediaSession2 implements AutoCloseable {
        if (command == null) {
            throw new IllegalArgumentException("command shouldn't be null");
        }
        Collection<ControllerInfo> controllerInfos;
        synchronized (mLock) {
            controllerInfos = mConnectedControllers.values();
        }
        List<ControllerInfo> controllerInfos = getConnectedControllers();
        for (ControllerInfo controller : controllerInfos) {
            controller.sendSessionCommand(command, args, null);
        }
@@ -222,23 +221,26 @@ public class MediaSession2 implements AutoCloseable {
        }
    }

    // Called by Session2Link.onConnect
    void onConnect(final Controller2Link controller, int seq, Bundle connectionRequest) {
        if (controller == null || connectionRequest == null) {
            return;
    SessionCallback getCallback() {
        return mCallback;
    }
        final int uid = Binder.getCallingUid();
        final int callingPid = Binder.getCallingPid();
        final long token = Binder.clearCallingIdentity();
        // Binder.getCallingPid() can be 0 for an oneway call from the remote process.
        // If it's the case, use PID from the ConnectionRequest.
        final int pid = (callingPid != 0) ? callingPid : connectionRequest.getInt(KEY_PID);
        final String pkg = connectionRequest.getString(KEY_PACKAGE_NAME);
        try {
            RemoteUserInfo remoteUserInfo = new RemoteUserInfo(pkg, pid, uid);

    // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
    void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
            Bundle connectionRequest) {
        if (callingPid == 0) {
            // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
            // the remote process. If it's the case, use PID from the connectionRequest.
            callingPid = connectionRequest.getInt(KEY_PID);
        }
        String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);

        RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
        final ControllerInfo controllerInfo = new ControllerInfo(remoteUserInfo,
                mSessionManager.isTrustedForMediaControl(remoteUserInfo), controller);
        mCallbackExecutor.execute(() -> {
            boolean accept = false;
            try {
                if (isClosed()) {
                    return;
                }
@@ -247,9 +249,10 @@ public class MediaSession2 implements AutoCloseable {
                // Don't reject connection for the request from trusted app.
                // Otherwise server will fail to retrieve session's information to dispatch
                // media keys to.
                boolean accept =
                        controllerInfo.mAllowedCommands != null || controllerInfo.isTrusted();
                if (accept) {
                accept = controllerInfo.mAllowedCommands != null || controllerInfo.isTrusted();
                if (!accept) {
                    return;
                }
                if (controllerInfo.mAllowedCommands == null) {
                    // For trusted apps, send non-null allowed commands to keep
                    // connection.
@@ -280,44 +283,33 @@ public class MediaSession2 implements AutoCloseable {
                    return;
                }
                controllerInfo.notifyConnected(connectionResult);
                } else {
            } finally {
                if (!accept) {
                    if (DEBUG) {
                        Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
                    }
                }
                controllerInfo.notifyDisconnected();
            }
        });
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    // Called by Session2Link.onDisconnect
    void onDisconnect(final Controller2Link controller, int seq) {
        if (controller == null) {
            return;
        }
    void onDisconnect(@NonNull final Controller2Link controller, int seq) {
        final ControllerInfo controllerInfo;
        synchronized (mLock) {
            controllerInfo = mConnectedControllers.get(controller);
            controllerInfo = mConnectedControllers.remove(controller);
        }
        if (controllerInfo == null) {
            return;
        }

        final long token = Binder.clearCallingIdentity();
        try {
        mCallbackExecutor.execute(() -> {
            mCallback.onDisconnected(MediaSession2.this, controllerInfo);
        });
            mConnectedControllers.remove(controller);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    // Called by Session2Link.onSessionCommand
    void onSessionCommand(final Controller2Link controller, final int seq,
    void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
            final Session2Command command, final Bundle args,
            @Nullable ResultReceiver resultReceiver) {
        if (controller == null) {
@@ -332,12 +324,9 @@ public class MediaSession2 implements AutoCloseable {
        }

        // TODO: check allowed commands.
        final long token = Binder.clearCallingIdentity();
        try {
        synchronized (mLock) {
            controllerInfo.addRequestedCommandSeqNumber(seq);
        }

        mCallbackExecutor.execute(() -> {
            if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
                resultReceiver.send(RESULT_INFO_SKIPPED, null);
@@ -353,13 +342,10 @@ public class MediaSession2 implements AutoCloseable {
                }
            }
        });
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    // Called by Session2Link.onCancelCommand
    void onCancelCommand(final Controller2Link controller, final int seq) {
    void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
        final ControllerInfo controllerInfo;
        synchronized (mLock) {
            controllerInfo = mConnectedControllers.get(controller);
@@ -367,13 +353,15 @@ public class MediaSession2 implements AutoCloseable {
        if (controllerInfo == null) {
            return;
        }

        final long token = Binder.clearCallingIdentity();
        try {
        controllerInfo.removeRequestedCommandSeqNumber(seq);
        } finally {
            Binder.restoreCallingIdentity(token);
    }

    private List<ControllerInfo> getConnectedControllers() {
        List<ControllerInfo> controllers = new ArrayList<>();
        synchronized (mLock) {
            controllers.addAll(mConnectedControllers.values());
        }
        return controllers;
    }

    /**
@@ -660,6 +648,8 @@ public class MediaSession2 implements AutoCloseable {
     * This API is not generally intended for third party application developers.
     */
    public abstract static class SessionCallback {
        ForegroundServiceEventCallback mForegroundServiceEventCallback;

        /**
         * Called when a controller is created for this session. Return allowed commands for
         * controller. By default it returns {@code null}.
@@ -716,5 +706,19 @@ public class MediaSession2 implements AutoCloseable {
        public void onCommandResult(@NonNull MediaSession2 session,
                @NonNull ControllerInfo controller, @NonNull Object token,
                @NonNull Session2Command command, @NonNull Session2Command.Result result) {}

        final void onSessionClosed(MediaSession2 session) {
            if (mForegroundServiceEventCallback != null) {
                mForegroundServiceEventCallback.onSessionClosed(session);
            }
        }

        void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
            mForegroundServiceEventCallback = callback;
        }

        abstract static class ForegroundServiceEventCallback {
            public void onSessionClosed(MediaSession2 session) {}
        }
    }
}
+288 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading