blob: 6ad6d3a4aa72bb9fb1f4d7b38254ccf93fe0ce24 [file] [log] [blame]
/*
* Copyright (C) 2023 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.companion.transport;
import android.annotation.NonNull;
import android.companion.IOnMessageReceivedListener;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import libcore.util.EmptyArray;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class represents the channel established between two devices.
*/
public abstract class Transport {
protected static final String TAG = "CDM_CompanionTransport";
protected static final boolean DEBUG = Build.IS_DEBUGGABLE;
static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
public static final int MESSAGE_REQUEST_CONTEXT_SYNC = 0x63678883; // ?CXS
public static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983; // ?RES
static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
protected static final int HEADER_LENGTH = 12;
protected final int mAssociationId;
protected final ParcelFileDescriptor mFd;
protected final InputStream mRemoteIn;
protected final OutputStream mRemoteOut;
protected final Context mContext;
/**
* Message type -> Listener
*
* For now, the transport only supports 1 listener for each message type. If there's a need in
* the future to allow multiple listeners to receive callbacks for the same message type, the
* value of the map can be a list.
*/
private final Map<Integer, IOnMessageReceivedListener> mListeners;
private static boolean isRequest(int message) {
return (message & 0xFF000000) == 0x63000000;
}
private static boolean isResponse(int message) {
return (message & 0xFF000000) == 0x33000000;
}
@GuardedBy("mPendingRequests")
protected final SparseArray<CompletableFuture<byte[]>> mPendingRequests =
new SparseArray<>();
protected final AtomicInteger mNextSequence = new AtomicInteger();
Transport(int associationId, ParcelFileDescriptor fd, Context context) {
mAssociationId = associationId;
mFd = fd;
mRemoteIn = new ParcelFileDescriptor.AutoCloseInputStream(fd);
mRemoteOut = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
mContext = context;
mListeners = new HashMap<>();
}
/**
* Add a listener when a message is received for the message type
* @param message Message type
* @param listener Execute when a message with the type is received
*/
public void addListener(int message, IOnMessageReceivedListener listener) {
mListeners.put(message, listener);
}
public int getAssociationId() {
return mAssociationId;
}
protected ParcelFileDescriptor getFd() {
return mFd;
}
/**
* Start listening to messages.
*/
abstract void start();
/**
* Soft stop listening to the incoming data without closing the streams.
*/
abstract void stop();
/**
* Stop listening to the incoming data and close the streams.
*/
abstract void close();
protected abstract void sendMessage(int message, int sequence, @NonNull byte[] data)
throws IOException;
/**
* Send a message.
*/
public void sendMessage(int message, @NonNull byte[] data) throws IOException {
sendMessage(message, mNextSequence.incrementAndGet(), data);
}
public Future<byte[]> requestForResponse(int message, byte[] data) {
if (DEBUG) Slog.d(TAG, "Requesting for response");
final int sequence = mNextSequence.incrementAndGet();
final CompletableFuture<byte[]> pending = new CompletableFuture<>();
synchronized (mPendingRequests) {
mPendingRequests.put(sequence, pending);
}
try {
sendMessage(message, sequence, data);
} catch (IOException e) {
synchronized (mPendingRequests) {
mPendingRequests.remove(sequence);
}
pending.completeExceptionally(e);
}
return pending;
}
protected final void handleMessage(int message, int sequence, @NonNull byte[] data)
throws IOException {
if (DEBUG) {
Slog.d(TAG, "Received message 0x" + Integer.toHexString(message)
+ " sequence " + sequence + " length " + data.length
+ " from association " + mAssociationId);
}
if (isRequest(message)) {
try {
processRequest(message, sequence, data);
} catch (IOException e) {
Slog.w(TAG, "Failed to respond to 0x" + Integer.toHexString(message), e);
}
} else if (isResponse(message)) {
processResponse(message, sequence, data);
} else {
Slog.w(TAG, "Unknown message 0x" + Integer.toHexString(message));
}
}
private void processRequest(int message, int sequence, byte[] data)
throws IOException {
switch (message) {
case MESSAGE_REQUEST_PING: {
sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, data);
break;
}
case MESSAGE_REQUEST_CONTEXT_SYNC: {
callback(message, data);
sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, EmptyArray.BYTE);
break;
}
case MESSAGE_REQUEST_PERMISSION_RESTORE: {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
&& !Build.isDebuggable()) {
Slog.w(TAG, "Restoring permissions only supported on watches");
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
break;
}
try {
callback(message, data);
sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, EmptyArray.BYTE);
} catch (Exception e) {
Slog.w(TAG, "Failed to restore permissions");
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
}
break;
}
default: {
Slog.w(TAG, "Unknown request 0x" + Integer.toHexString(message));
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
break;
}
}
}
private void callback(int message, byte[] data) {
if (mListeners.containsKey(message)) {
try {
mListeners.get(message).onMessageReceived(getAssociationId(), data);
Slog.i(TAG, "Message 0x" + Integer.toHexString(message)
+ " is received from associationId " + mAssociationId
+ ", sending data length " + data.length + " to the listener.");
} catch (RemoteException ignored) {
}
}
}
private void processResponse(int message, int sequence, byte[] data) {
final CompletableFuture<byte[]> future;
synchronized (mPendingRequests) {
future = mPendingRequests.removeReturnOld(sequence);
}
if (future == null) {
Slog.w(TAG, "Ignoring unknown sequence " + sequence);
return;
}
switch (message) {
case MESSAGE_RESPONSE_SUCCESS: {
future.complete(data);
break;
}
case MESSAGE_RESPONSE_FAILURE: {
future.completeExceptionally(new RuntimeException("Remote failure"));
break;
}
default: {
Slog.w(TAG, "Ignoring unknown response 0x" + Integer.toHexString(message));
}
}
}
}