blob: c59c5f61bf913ebc38767be4e6dff41ea9c796f3 [file] [log] [blame]
Jaewan Kimceb6b6e2018-01-21 20:56:10 +09001/*
2 * Copyright 2018 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.media;
18
19import android.annotation.CallSuper;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.Context;
23import android.media.IMediaSession2;
24import android.media.MediaController2;
25import android.media.MediaSession2;
26import android.media.SessionToken;
27import android.os.Handler;
28import android.os.Looper;
29import android.util.Log;
30import java.util.concurrent.Executor;
31import java.util.concurrent.atomic.AtomicBoolean;
32
33/**
34 * Records a {@link MediaSession2} and holds {@link MediaController2}.
35 * <p>
36 * Owner of this object should handle synchronization.
37 */
38class MediaSession2Record {
39 interface SessionDestroyedListener {
40 void onSessionDestroyed(MediaSession2Record record);
41 }
42
43 private static final String TAG = "Session2Record";
44 private static final boolean DEBUG = true; // TODO(jaewan): Change
45
46 private final Context mContext;
47 private final SessionDestroyedListener mSessionDestroyedListener;
48
49 // TODO(jaewan): Replace these with the mContext.getMainExecutor()
50 private final Handler mMainHandler;
51 private final Executor mMainExecutor;
52
53 private MediaController2 mController;
54 private ControllerCallback mControllerCallback;
55
56 private int mSessionPid;
57
58 /**
59 * Constructor
60 */
61 public MediaSession2Record(@NonNull Context context,
62 @NonNull SessionDestroyedListener listener) {
63 mContext = context;
64 mSessionDestroyedListener = listener;
65
66 mMainHandler = new Handler(Looper.getMainLooper());
67 mMainExecutor = (runnable) -> {
68 mMainHandler.post(runnable);
69 };
70 }
71
72 public int getSessionPid() {
73 return mSessionPid;
74 }
75
76 public Context getContext() {
77 return mContext;
78 }
79
80 @CallSuper
81 public void onSessionDestroyed() {
82 if (mController != null) {
83 mControllerCallback.destroy();
Jaewan Kimcf707c42018-01-24 13:13:53 +090084 mController.close();
Jaewan Kimceb6b6e2018-01-21 20:56:10 +090085 mController = null;
86 }
87 mSessionPid = 0;
88 }
89
90 /**
91 * Create session token and tell server that session is now active.
92 *
93 * @param sessionPid session's pid
94 * @return a token if successfully set, {@code null} if sanity check fails.
95 */
96 // TODO(jaewan): also add uid for multiuser support
97 @CallSuper
98 public @Nullable
99 SessionToken createSessionToken(int sessionPid, String packageName, String id,
100 IMediaSession2 sessionBinder) {
101 if (mController != null) {
102 if (mSessionPid != sessionPid) {
103 // A package uses the same id for session across the different process.
104 return null;
105 }
106 // If a session becomes inactive and then active again very quickly, previous 'inactive'
107 // may not have delivered yet. Check if it's the case and destroy controller before
108 // creating its session record to prevents getXXTokens() API from returning duplicated
109 // tokens.
110 // TODO(jaewan): Change this. If developer is really creating two sessions with the same
111 // id, this will silently invalidate previous session and no way for
112 // developers to know that.
113 // Instead, keep the list of static session ids from our APIs.
114 // Also change Controller2Impl.onConnectionChanged / getController.
115 // Also clean up ControllerCallback#destroy().
116 if (DEBUG) {
117 Log.d(TAG, "Session is recreated almost immediately. " + this);
118 }
119 onSessionDestroyed();
120 }
121 mController = onCreateMediaController(packageName, id, sessionBinder);
122 mSessionPid = sessionPid;
123 return mController.getSessionToken();
124 }
125
126 /**
127 * Called when session becomes active and needs controller to listen session's activeness.
128 * <p>
129 * Should be overridden by subclasses to create token with its own extra information.
130 */
131 MediaController2 onCreateMediaController(
132 String packageName, String id, IMediaSession2 sessionBinder) {
133 SessionToken token = new SessionToken(
134 SessionToken.TYPE_SESSION, packageName, id, null, sessionBinder);
135 return createMediaController(token);
136 }
137
138 final MediaController2 createMediaController(SessionToken token) {
139 mControllerCallback = new ControllerCallback();
140 return new MediaController2(mContext, token, mControllerCallback, mMainExecutor);
141 }
142
143 /**
144 * @return controller. Note that framework can only call oneway calls.
145 */
146 public SessionToken getToken() {
147 return mController == null ? null : mController.getSessionToken();
148 }
149
150 @Override
151 public String toString() {
152 return getToken() == null
153 ? "Token {null}"
154 : "SessionRecord {pid=" + mSessionPid + ", " + getToken().toString() + "}";
155 }
156
157 private class ControllerCallback extends MediaController2.ControllerCallback {
158 private final AtomicBoolean mIsActive = new AtomicBoolean(true);
159
160 // This is called on the main thread with no lock. So place ensure followings.
161 // 1. Don't touch anything in the parent class that needs synchronization.
162 // All other APIs in the MediaSession2Record assumes that server would use them with
163 // the lock hold.
164 // 2. This can be called after the controller registered is released.
165 @Override
166 public void onDisconnected() {
167 if (!mIsActive.get()) {
168 return;
169 }
170 if (DEBUG) {
171 Log.d(TAG, "onDisconnected, token=" + getToken());
172 }
173 mSessionDestroyedListener.onSessionDestroyed(MediaSession2Record.this);
174 }
175
176 // TODO(jaewan): Remove this API when we revisit createSessionToken()
177 public void destroy() {
178 mIsActive.set(false);
179 }
180 };
181}