blob: 542e8e4f93f33c702b228876c779be5aaefc0125 [file] [log] [blame]
package com.matter.tv.server.service;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.matter.tv.app.api.IMatterAppAgent;
import com.matter.tv.app.api.MatterIntentConstants;
import com.matter.tv.server.model.ContentApp;
import com.matter.tv.server.receivers.ContentAppDiscoveryService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ContentAppAgentService extends Service {
private static final String TAG = "ContentAppAgentService";
public static final String ACTION_MATTER_RESPONSE =
"com.matter.tv.app.api.action.MATTER_COMMAND_RESPONSE";
public static final String EXTRA_RESPONSE_RECEIVING_PACKAGE = "EXTRA_RESPONSE_RECEIVING_PACKAGE";
public static final String EXTRA_RESPONSE_ID = "EXTRA_RESPONSE_ID";
public static final String FAILURE_KEY = "PlatformError";
public static final String FAILURE_STATUS_KEY = "Status";
public static final int FAILED_UNSUPPORTED_ENDPOINT = 0x7f;
public static final int FAILED_UNSUPPORTED_CLUSTER = 0xc3;
public static final int FAILED_UNSUPPORTED_COMMAND = 0x81;
public static final int FAILED_UNSUPPORTED_ATTRIBUTE = 0x86;
public static final int FAILED_UNKNOWN = 0x01;
public static final int FAILED_TIMEOUT = 0x94;
private static final int COMMAND_TIMEOUT = 8; // seconds
private static final int ATTRIBUTE_TIMEOUT = 2; // seconds
private static ResponseRegistry responseRegistry = new ResponseRegistry();
private static ExecutorService executorService =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private final IBinder appAgentBinder =
new IMatterAppAgent.Stub() {
@Override
public boolean setSupportedClusters(
com.matter.tv.app.api.SetSupportedClustersRequest request) throws RemoteException {
final int callingUID = Binder.getCallingUid();
final String pkg = getApplicationContext().getPackageManager().getNameForUid(callingUID);
Log.d(
TAG,
"Received request to add the following supported clusters "
+ request.supportedClusters.toString()
+ " for app "
+ pkg);
ContentApp contentApp =
ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApp(pkg);
if (contentApp != null) {
contentApp.setSupportedClusters(request.supportedClusters);
return true;
}
Log.e(TAG, "No matter content app found for package " + pkg);
return false;
}
@Override
public boolean reportAttributeChange(int clusterId, int attributeId)
throws RemoteException {
final int callingUID = Binder.getCallingUid();
final String pkg = getApplicationContext().getPackageManager().getNameForUid(callingUID);
Log.d(
TAG,
"Received request to report attribute change for cluster "
+ clusterId
+ " attribute "
+ attributeId);
ContentApp contentApp =
ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApp(pkg);
if (contentApp != null && contentApp.getEndpointId() != ContentApp.INVALID_ENDPOINTID) {
// Make this call async so that even if the content apps make this call during command
// processing and synchronously, the command processing thread will not block for the
// chip stack lock.
executorService.execute(
() -> {
AppPlatformService.get()
.reportAttributeChange(contentApp.getEndpointId(), clusterId, attributeId);
});
return true;
}
Log.e(TAG, "No matter content app found for package " + pkg);
return false;
}
};
@Nullable
@Override
public IBinder onBind(final Intent intent) {
Log.d(TAG, "Received binding request.");
if (MatterIntentConstants.ACTION_MATTER_AGENT.equals(intent.getAction())) {
Log.d(TAG, "Returning MatterAppAgent");
return appAgentBinder;
}
return null;
}
public static String sendCommand(
Context context, String packageName, long clusterId, long commandId, String payload) {
Intent in = new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND);
Bundle extras = new Bundle();
extras.putByteArray(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD, payload.getBytes());
extras.putLong(MatterIntentConstants.EXTRA_COMMAND_ID, commandId);
extras.putLong(MatterIntentConstants.EXTRA_CLUSTER_ID, clusterId);
in.putExtras(extras);
in.setPackage(packageName);
int flags = Intent.FLAG_INCLUDE_STOPPED_PACKAGES;
flags |= Intent.FLAG_RECEIVER_FOREGROUND;
in.setFlags(flags);
int messageId = responseRegistry.getNextMessageCounter();
in.putExtra(
MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT,
getPendingIntentForResponse(context, packageName, messageId));
context.sendBroadcast(in);
return getResponse(messageId, COMMAND_TIMEOUT);
}
public static String sendAttributeReadRequest(
Context context, String packageName, long clusterId, long attributeId) {
Intent in = new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND);
Bundle extras = new Bundle();
extras.putString(
MatterIntentConstants.EXTRA_ATTRIBUTE_ACTION, MatterIntentConstants.ATTRIBUTE_ACTION_READ);
extras.putLong(MatterIntentConstants.EXTRA_ATTRIBUTE_ID, attributeId);
extras.putLong(MatterIntentConstants.EXTRA_CLUSTER_ID, clusterId);
in.putExtras(extras);
in.setPackage(packageName);
int flags = Intent.FLAG_INCLUDE_STOPPED_PACKAGES;
flags |= Intent.FLAG_RECEIVER_FOREGROUND;
in.setFlags(flags);
int messageId = responseRegistry.getNextMessageCounter();
in.putExtra(
MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT,
getPendingIntentForResponse(context, packageName, messageId));
context.sendBroadcast(in);
return getResponse(messageId, ATTRIBUTE_TIMEOUT);
}
@NonNull
private static String getResponse(int messageId, int timeout) {
ResponseRegistry.WaitState status =
responseRegistry.waitForMessage(messageId, timeout, TimeUnit.SECONDS);
String response = "";
switch (status) {
case SUCCESS:
case INVALID_COUNTER:
response = responseRegistry.readAndRemoveResponse(messageId);
if (response == null) {
response =
"{\""
+ FAILURE_KEY
+ "\":{\""
+ ContentAppAgentService.FAILURE_STATUS_KEY
+ "\":"
+ FAILED_UNKNOWN
+ "}}";
}
break;
case TIMED_OUT:
response =
"{\""
+ FAILURE_KEY
+ "\":{\""
+ ContentAppAgentService.FAILURE_STATUS_KEY
+ "\":"
+ FAILED_TIMEOUT
+ "}}";
break;
case INTERRUPTED:
response = responseRegistry.readAndRemoveResponse(messageId);
if (response == null) {
response =
"{\""
+ FAILURE_KEY
+ "\":{\""
+ ContentAppAgentService.FAILURE_STATUS_KEY
+ "\":"
+ FAILED_TIMEOUT
+ "}}";
}
break;
default:
response =
"{\""
+ FAILURE_KEY
+ "\":{\""
+ ContentAppAgentService.FAILURE_STATUS_KEY
+ "\":"
+ FAILED_UNKNOWN
+ "}}";
}
Log.d(TAG, "Response " + response + " being returned for message " + messageId);
return response;
}
private static PendingIntent getPendingIntentForResponse(
Context context, final String targetPackage, final int responseId) {
Intent ackBackIntent = new Intent(ACTION_MATTER_RESPONSE);
ackBackIntent.setClass(context, ContentAppAgentService.class);
ackBackIntent.putExtra(EXTRA_RESPONSE_RECEIVING_PACKAGE, targetPackage);
ackBackIntent.putExtra(EXTRA_RESPONSE_ID, responseId);
return PendingIntent.getService(context, 0, ackBackIntent, PendingIntent.FLAG_ONE_SHOT);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent != null && ACTION_MATTER_RESPONSE.equals(intent.getAction())) {
String response =
new String(intent.getByteArrayExtra(MatterIntentConstants.EXTRA_RESPONSE_PAYLOAD));
int messageId = intent.getIntExtra(EXTRA_RESPONSE_ID, Integer.MAX_VALUE);
Log.d(TAG, "Response " + response + " received for message " + messageId);
responseRegistry.receivedMessageResponse(messageId, response);
}
return START_NOT_STICKY;
}
}