Skip to content

Commit

Permalink
tv-app:content-app to tv-app:platform-app service binding and command…
Browse files Browse the repository at this point in the history
… response support (#18353)

* Changes to add dynamic discovery of clusters and response for commands

* IMaaterAppAgent interface
  • Loading branch information
amitnj authored and pull[bot] committed Nov 17, 2023
1 parent 9daa7d6 commit 2338200
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// IMatterAppAgent.aidl
package com.matter.tv.app.api;

import com.matter.tv.app.api.SetSupportedClustersRequest;
import com.matter.tv.app.api.ReportAttributeChangeRequest;

/*
* To use this interface, partners should query for and bind to a service that handles the "com.matter.tv.app.api.action.MatterAppAgent" Action.
* They should verify the host process holds the "com.matter.tv.app.api.permission.SEND_DATA" permission
* To bind to this service the client app itself must hold "com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION".
*/
interface IMatterAppAgent {
/**
* Report dynamic clusters to matter agent. Note that this api is not incremental, every time it is called
* you must report ALL dynamic clusters the app supports. Any dynamic clusters previously reported
* which are not reported in a subsequent call will be removed. This does NOT impact static clusters
* declared in app resources; those cannot be removed. However, a dynamic cluster can be used to override
* and hide a static one based on cluster name.
*
* @param SetClustersRequest request object containing the list of clusters to assert for this app.
* @returns true if successful.
*/
// TODO : replace the boolean with some kind of enumerated status field
boolean setSupportedClusters(in SetSupportedClustersRequest request);

/**
* Reports the Attribute changes of attributes.
* @param - ReportAttributeChangeRequest, request object containing all attributes which have changed.
* @return - ReportAttributeChangeResult, returns success or error code
*/
// TODO : replace the boolean with some kind of enumerated status field
boolean reportAttributeChange(in ReportAttributeChangeRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// ReportAttributeChangeRequest.aidl
package com.matter.tv.app.api;

parcelable ReportAttributeChangeRequest{

int clusterIdentifier;

int attributeIdentifier;

String value;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SetSupportedClustersRequest.aidl
package com.matter.tv.app.api;

import com.matter.tv.app.api.SupportedCluster;

parcelable SetSupportedClustersRequest {

List<SupportedCluster> supportedClusters;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SupportedCluster.aidl
package com.matter.tv.app.api;

parcelable SupportedCluster {

int clusterIdentifier;

String[] features;

int[] optionalCommandIdentifiers;

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,17 @@ public class MatterIntentConstants {

public static final String ACTION_MATTER_COMMAND = "com.matter.tv.app.api.action.MATTER_COMMAND";

public static final String ACTION_MATTER_AGENT = "com.matter.tv.app.api.action.MatterAppAgent";

public static final String PERMISSION_MATTER_AGENT_BIND =
"com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION";

public static final String PERMISSION_MATTER_AGENT = "com.matter.tv.app.api.permission.SEND_DATA";

public static final String EXTRA_COMMAND_PAYLOAD = "EXTRA_COMMAND_PAYLOAD";

public static final String EXTRA_RESPONSE_PAYLOAD = "EXTRA_RESPONSE_PAYLOAD";

public static final String EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT =
"EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT";
}
1 change: 1 addition & 0 deletions examples/tv-app/android/App/content-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ android {
'src/main/java',
'../common-api/src/main/java',
]
aidl.srcDirs = ['../common-api/src/main/aidl']
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.contentapp">


<uses-permission android:name="com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand All @@ -22,7 +28,7 @@
</activity>
<receiver
android:name=".receiver.MatterCommandReceiver"
android:permission="com.matter.app_agent_api.permission.SEND_DATA"
android:permission="com.matter.tv.app.api.permission.SEND_DATA"
android:enabled="true"
android:exported="true">
<!-- Intent action for receiving an Matter directive-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.example.contentapp.matter.MatterAgentClient;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "ContentAppMainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MatterAgentClient matterAgentClient = MatterAgentClient.getInstance(getApplicationContext());
final ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(matterAgentClient::reportClusters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package com.example.contentapp.matter;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.matter.tv.app.api.IMatterAppAgent;
import com.matter.tv.app.api.MatterIntentConstants;
import com.matter.tv.app.api.SetSupportedClustersRequest;
import com.matter.tv.app.api.SupportedCluster;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class MatterAgentClient {

private static final String TAG = "MatterAgentClient";
private static MatterAgentClient instance;
private IMatterAppAgent service;
private boolean bound = false;
private CountDownLatch latch = new CountDownLatch(1);

// TODO : Introduce dependency injection
private MatterAgentClient() {};

public static synchronized MatterAgentClient getInstance(Context context) {
if (instance == null || (instance.service == null && !instance.bound)) {
instance = new MatterAgentClient();
if (!instance.bindService(context)) {
Log.e(TAG, "Matter agent binding request unsuccessful.");
instance = null;
} else {
Log.d(TAG, "Matter agent binding request successful.");
}
}
return instance;
}

public void reportClusters() {
IMatterAppAgent matterAgent = instance.getMatterAgent();
if (matterAgent == null) {
Log.e(TAG, "Matter agent not retrieved.");
return;
}
SetSupportedClustersRequest supportedClustersRequest = new SetSupportedClustersRequest();
supportedClustersRequest.supportedClusters = new ArrayList<SupportedCluster>();
SupportedCluster supportedCluster = new SupportedCluster();
supportedCluster.clusterIdentifier = 1;

supportedClustersRequest.supportedClusters.add(supportedCluster);
try {
boolean success = matterAgent.setSupportedClusters(supportedClustersRequest);
Log.d(TAG, "Setting supported clusters returned " + (success ? "True" : "False"));
} catch (RemoteException e) {
e.printStackTrace();
}
}

private IMatterAppAgent getMatterAgent() {
try {
latch.await();
return service;
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for service connection.", e);
}
return null;
}

private synchronized boolean bindService(Context context) {

ServiceConnection serviceConnection = new MyServiceConnection();
final Intent intent = new Intent(MatterIntentConstants.ACTION_MATTER_AGENT);
if (intent.getComponent() == null) {
final ResolveInfo resolveInfo =
resolveBindIntent(
context,
intent,
MatterIntentConstants.PERMISSION_MATTER_AGENT_BIND,
MatterIntentConstants.PERMISSION_MATTER_AGENT);
if (resolveInfo == null) {
Log.e(TAG, "No Service available on device to bind for intent " + intent);
return false;
}
final ComponentName component =
new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
intent.setComponent(component);
}

try {
Log.e(TAG, "Binding to service");
bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
return bound;
} catch (final Throwable e) {
Log.e(TAG, "Exception binding to service", e);
}
return false;
}

/**
* Returns a {@link ResolveInfo} for a service bindable with the provided intent and permission.
*
* @param context Android Context.
* @param bindIntent The Intent used to bind to the Service. Implicit or Explicit.
* @param bindPermission The permission that the resolved Service must enforce.
* @param permissionHeldByService A permission that the resolved Service must hold.
* @return A {@link ResolveInfo} with ServiceInfo for the service, or null.
*/
private ResolveInfo resolveBindIntent(
final Context context,
final Intent bindIntent,
final String bindPermission,
final String permissionHeldByService) {
if (bindPermission == null || permissionHeldByService == null) {
Log.w(
TAG,
"Must specify the permission protecting the service, as well as "
+ "a permission held by the service's package.");
return null;
}
final PackageManager pm = context.getPackageManager();
if (pm == null) {
Log.w(TAG, "Package manager is not available.");
return null;
}
// Check for Services able to handle this intent.
final List<ResolveInfo> infos = pm.queryIntentServices(bindIntent, 0);
if (infos == null || infos.isEmpty()) {
return null;
}

// For all the services returned, remove those that don't have the specified permissions.
int size = infos.size();
for (int i = size - 1; i >= 0; --i) {
final ResolveInfo resolveInfo = infos.get(i);
// The service must be protected by the bindPermission
if (!bindPermission.equals(resolveInfo.serviceInfo.permission)) {
Log.w(
TAG,
String.format(
"Service (%s) does not enforce the required permission (%s)",
resolveInfo.serviceInfo.name, bindPermission));
infos.remove(i);
continue;
}
// And the service's package must hold the permissionHeldByService permission
final String pkgName = resolveInfo.serviceInfo.packageName;
final int state = pm.checkPermission(permissionHeldByService, pkgName);
if (state != PackageManager.PERMISSION_GRANTED) {
Log.w(
TAG,
String.format(
"Package (%s) does not hold the required permission (%s)",
pkgName, bindPermission));
infos.remove(i);
}
}
size = infos.size();

if (size > 1) {
// This is suspicious. This means we've got at least 2 services both claiming to handle
// this intent, and they both have declared this permission. In this case, filter those
// that aren't on the system image.
for (int i = size - 1; i >= 0; --i) {
final ResolveInfo resolveInfo = infos.get(i);
try {
final ApplicationInfo appInfo =
pm.getApplicationInfo(resolveInfo.serviceInfo.packageName, 0);
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
&& (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) {
// Not a system app or an updated system app. Remove this sketchy service.
infos.remove(i);
}
} catch (final PackageManager.NameNotFoundException e) {
infos.remove(i);
}
}
}

if (infos.size() > 1) {
Log.w(
TAG,
"More than one permission-enforced system"
+ " service can handle intent "
+ bindIntent
+ " and permission "
+ bindPermission);
}

return (infos.isEmpty() ? null : infos.get(0));
}

private class MyServiceConnection implements ServiceConnection {

@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
Log.d(
TAG,
String.format(
"onServiceConnected for API with intent action %s",
MatterIntentConstants.ACTION_MATTER_AGENT));
service = IMatterAppAgent.Stub.asInterface(binder);
latch.countDown();
}

@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.contentapp.receiver;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
Expand All @@ -22,12 +23,27 @@ public void onReceive(Context context, Intent intent) {
case MatterIntentConstants.ACTION_MATTER_COMMAND:
byte[] commandPayload =
intent.getByteArrayExtra(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD);
Log.e(
Log.d(
TAG,
new StringBuilder()
.append("Received matter command: ")
.append(intent.getAction())
.toString());

PendingIntent pendingIntent =
intent.getParcelableExtra(
MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT);
if (pendingIntent != null) {
final Intent responseIntent =
new Intent()
.putExtra(MatterIntentConstants.EXTRA_RESPONSE_PAYLOAD, "Success".getBytes());
try {
pendingIntent.send(context, 0, responseIntent);
} catch (final PendingIntent.CanceledException ex) {
Log.e(TAG, "Error sending pending intent to the Matter agent", ex);
}
}
break;
default:
Log.e(
TAG,
Expand Down
Loading

0 comments on commit 2338200

Please sign in to comment.