blob: 5d89a9d62dea206bb6cd44d27502ed441a2180ea [file] [log] [blame]
/*
* 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 com.android.server.media;
import android.app.ActivityManager;
import android.content.Context;
import android.media.IMediaRouter2Manager;
import android.media.IMediaRouterClient;
import android.media.MediaRoute2ProviderInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* TODO: Merge this to MediaRouterService once it's finished.
*/
class MediaRouter2ServiceImpl {
private static final String TAG = "MediaRouter2ServiceImpl";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Context mContext;
private final Object mLock = new Object();
private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
private final ArrayMap<IBinder, ManagerRecord> mAllManagerRecords = new ArrayMap<>();
private int mCurrentUserId = -1;
MediaRouter2ServiceImpl(Context context) {
mContext = context;
}
public void registerManagerAsUser(IMediaRouter2Manager client,
String packageName, int userId) {
if (client == null) {
throw new IllegalArgumentException("client must not be null");
}
//TODO: should check permission
final boolean trusted = true;
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
false /*allowAll*/, true /*requireFull*/, "registerManagerAsUser", packageName);
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
registerManagerLocked(client, uid, pid, packageName, resolvedUserId, trusted);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void unregisterManager(IMediaRouter2Manager client) {
if (client == null) {
throw new IllegalArgumentException("client must not be null");
}
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
unregisterManagerLocked(client, false);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void setControlCategories(IMediaRouterClient client, List<String> categories) {
if (client == null) {
throw new IllegalArgumentException("client must not be null");
}
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
setControlCategoriesLocked(client, categories);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void setRemoteRoute(IMediaRouter2Manager client,
int uid, String routeId, boolean explicit) {
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
setRemoteRouteLocked(client, uid, routeId, explicit);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
void clientDied(ManagerRecord managerRecord) {
synchronized (mLock) {
unregisterManagerLocked(managerRecord.mClient, true);
}
}
private void registerManagerLocked(IMediaRouter2Manager client,
int uid, int pid, String packageName, int userId, boolean trusted) {
final IBinder binder = client.asBinder();
ManagerRecord managerRecord = mAllManagerRecords.get(binder);
if (managerRecord == null) {
boolean newUser = false;
UserRecord userRecord = mUserRecords.get(userId);
if (userRecord == null) {
userRecord = new UserRecord(userId);
newUser = true;
}
managerRecord = new ManagerRecord(userRecord, client, uid, pid, packageName, trusted);
try {
binder.linkToDeath(managerRecord, 0);
} catch (RemoteException ex) {
throw new RuntimeException("Media router client died prematurely.", ex);
}
if (newUser) {
mUserRecords.put(userId, userRecord);
initializeUserLocked(userRecord);
}
userRecord.mManagerRecords.add(managerRecord);
mAllManagerRecords.put(binder, managerRecord);
//TODO: remove this when it's unnecessary
// Sends published routes to newly added manager
userRecord.mHandler.scheduleUpdateManagerState();
}
}
private void unregisterManagerLocked(IMediaRouter2Manager client, boolean died) {
ManagerRecord clientRecord = mAllManagerRecords.remove(client.asBinder());
if (clientRecord != null) {
UserRecord userRecord = clientRecord.mUserRecord;
userRecord.mManagerRecords.remove(clientRecord);
clientRecord.dispose();
disposeUserIfNeededLocked(userRecord); // since client removed from user
}
}
private void setRemoteRouteLocked(IMediaRouter2Manager client,
int uid, String routeId, boolean explicit) {
ManagerRecord managerRecord = mAllManagerRecords.get(client.asBinder());
if (managerRecord != null) {
if (explicit && managerRecord.mTrusted) {
Pair<Integer, String> obj = new Pair<>(uid, routeId);
managerRecord.mUserRecord.mHandler.obtainMessage(
UserHandler.MSG_SELECT_REMOTE_ROUTE, obj).sendToTarget();
}
}
}
private void setControlCategoriesLocked(IMediaRouterClient client, List<String> categories) {
//TODO: implement this when we have client record (MediaRouter2?)
// final IBinder binder = client.asBinder();
// ClientRecord clientRecord = mAllClientRecords.get(binder);
//
// if (clientRecord != null) {
// clientRecord.mControlCategories = categories;
// clientRecord.mUserRecord.mHandler.obtainMessage(
// UserHandler.MSG_UPDATE_CLIENT_USAGE, clientRecord).sendToTarget();
// }
}
private void initializeUserLocked(UserRecord userRecord) {
if (DEBUG) {
Slog.d(TAG, userRecord + ": Initialized");
}
if (userRecord.mUserId == mCurrentUserId) {
userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START);
}
}
private void disposeUserIfNeededLocked(UserRecord userRecord) {
// If there are no records left and the user is no longer current then go ahead
// and purge the user record and all of its associated state. If the user is current
// then leave it alone since we might be connected to a route or want to query
// the same route information again soon.
if (userRecord.mUserId != mCurrentUserId
&& userRecord.mManagerRecords.isEmpty()) {
if (DEBUG) {
Slog.d(TAG, userRecord + ": Disposed");
}
mUserRecords.remove(userRecord.mUserId);
// Note: User already stopped (by switchUser) so no need to send stop message here.
}
}
final class UserRecord {
public final int mUserId;
final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>();
final UserHandler mHandler;
UserRecord(int userId) {
mUserId = userId;
mHandler = new UserHandler(MediaRouter2ServiceImpl.this, this);
}
}
final class ManagerRecord implements IBinder.DeathRecipient {
public final UserRecord mUserRecord;
public final IMediaRouter2Manager mClient;
public final int mUid;
public final int mPid;
public final String mPackageName;
public final boolean mTrusted;
ManagerRecord(UserRecord userRecord, IMediaRouter2Manager client,
int uid, int pid, String packageName, boolean trusted) {
mUserRecord = userRecord;
mClient = client;
mUid = uid;
mPid = pid;
mPackageName = packageName;
mTrusted = trusted;
}
public void dispose() {
mClient.asBinder().unlinkToDeath(this, 0);
}
@Override
public void binderDied() {
clientDied(this);
}
public void dump(PrintWriter pw, String prefix) {
pw.println(prefix + this);
final String indent = prefix + " ";
pw.println(indent + "mTrusted=" + mTrusted);
}
@Override
public String toString() {
return "Client " + mPackageName + " (pid " + mPid + ")";
}
}
static final class UserHandler extends Handler implements
MediaRoute2ProviderWatcher.Callback,
MediaRoute2ProviderProxy.Callback {
//TODO: Should be rearranged
public static final int MSG_START = 1;
public static final int MSG_STOP = 2;
private static final int MSG_SELECT_REMOTE_ROUTE = 10;
private static final int MSG_UPDATE_CLIENT_USAGE = 11;
private static final int MSG_UPDATE_MANAGER_STATE = 12;
private final WeakReference<MediaRouter2ServiceImpl> mServiceRef;
private final UserRecord mUserRecord;
private final MediaRoute2ProviderWatcher mWatcher;
private final ArrayList<IMediaRouter2Manager> mTempManagers = new ArrayList<>();
private final ArrayList<MediaRoute2ProviderProxy> mMediaProviders =
new ArrayList<>();
private boolean mRunning;
private boolean mManagerStateUpdateScheduled;
UserHandler(MediaRouter2ServiceImpl service, UserRecord userRecord) {
mServiceRef = new WeakReference<>(service);
mUserRecord = userRecord;
mWatcher = new MediaRoute2ProviderWatcher(service.mContext, this,
this, mUserRecord.mUserId);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_START: {
start();
break;
}
case MSG_STOP: {
stop();
break;
}
case MSG_SELECT_REMOTE_ROUTE: {
Pair<Integer, String> obj = (Pair<Integer, String>) msg.obj;
selectRemoteRoute(obj.first, obj.second);
break;
}
case MSG_UPDATE_CLIENT_USAGE: {
updateClientUsage();
break;
}
case MSG_UPDATE_MANAGER_STATE: {
updateManagerState();
break;
}
}
}
private void start() {
if (!mRunning) {
mRunning = true;
mWatcher.start();
}
}
private void stop() {
if (mRunning) {
mRunning = false;
//TODO: may unselect routes
mWatcher.stop(); // also stops all providers
}
}
@Override
public void onAddProvider(MediaRoute2ProviderProxy provider) {
provider.setCallback(this);
mMediaProviders.add(provider);
}
@Override
public void onRemoveProvider(MediaRoute2ProviderProxy provider) {
mMediaProviders.remove(provider);
}
@Override
public void onProviderStateChanged(MediaRoute2ProviderProxy provider) {
updateProvider(provider);
}
private void updateProvider(MediaRoute2ProviderProxy provider) {
scheduleUpdateManagerState();
}
private void selectRemoteRoute(int uid, String routeId) {
if (routeId != null) {
final int providerCount = mMediaProviders.size();
//TODO: should find proper provider (currently assumes a single provider)
for (int i = 0; i < providerCount; ++i) {
mMediaProviders.get(i).setSelectedRoute(uid, routeId);
}
}
}
private void scheduleUpdateManagerState() {
if (!mManagerStateUpdateScheduled) {
mManagerStateUpdateScheduled = true;
sendEmptyMessage(MSG_UPDATE_MANAGER_STATE);
}
}
private void updateManagerState() {
mManagerStateUpdateScheduled = false;
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
return;
}
//TODO: send provider info
MediaRoute2ProviderInfo providerInfo = null;
int selectedUid = 0;
String selectedRouteId = null;
final int mediaCount = mMediaProviders.size();
for (int i = 0; i < mediaCount; i++) {
providerInfo = mMediaProviders.get(i).getProviderInfo();
}
try {
synchronized (service.mLock) {
final int count = mUserRecord.mManagerRecords.size();
for (int i = 0; i < count; i++) {
mTempManagers.add(mUserRecord.mManagerRecords.get(i).mClient);
}
}
//TODO: Call proper callbacks when provider descriptor is implemented.
if (providerInfo != null) {
final int count = mTempManagers.size();
for (int i = 0; i < count; i++) {
try {
mTempManagers.get(i).notifyProviderInfoUpdated(providerInfo);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to call onStateChanged. Manager probably died.",
ex);
}
}
}
} finally {
// Clear the list in preparation for the next time.
mTempManagers.clear();
}
}
private void updateClientUsage() {
//TODO: merge these code to updateClientState()
// List<IMediaRouter2Manager> managers = new ArrayList<>();
// synchronized (mService.mLock) {
// final int count = mUserRecord.mManagerRecords.size();
// for (int i = 0; i < count; i++) {
// managers.add(mUserRecord.mManagerRecords.get(i).mClient);
// }
// }
// final int count = managers.size();
// for (int i = 0; i < count; i++) {
// try {
// managers.get(i).onControlCategoriesChanged(clientRecord.mUid,
// clientRecord.mControlCategories);
// } catch (RemoteException ex) {
// Slog.w(TAG, "Failed to call onControlCategoriesChanged. "
// + "Manager probably died.", ex);
// }
// }
}
}
}