Просмотр исходного кода

tv-app:content-app to tv-app:platform-app service binding and command response support (#18353)

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

* IMaaterAppAgent interface
amitnj 3 лет назад
Родитель
Сommit
cb116b9fba
15 измененных файлов с 431 добавлено и 20 удалено
  1. 33 0
      examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/IMatterAppAgent.aidl
  2. 12 0
      examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/ReportAttributeChangeRequest.aidl
  3. 10 0
      examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SetSupportedClustersRequest.aidl
  4. 12 0
      examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl
  5. 12 0
      examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/MatterIntentConstants.java
  6. 1 0
      examples/tv-app/android/App/content-app/build.gradle
  7. 7 1
      examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml
  8. 8 0
      examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java
  9. 216 0
      examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/matter/MatterAgentClient.java
  10. 17 1
      examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java
  11. 2 0
      examples/tv-app/android/App/platform-app/build.gradle
  12. 16 10
      examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
  13. 10 2
      examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
  14. 7 5
      examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/receivers/ContentAppDiscoveryService.java
  15. 68 1
      examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ContentAppAgentService.java

+ 33 - 0
examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/IMatterAppAgent.aidl

@@ -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);
+}

+ 12 - 0
examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/ReportAttributeChangeRequest.aidl

@@ -0,0 +1,12 @@
+// ReportAttributeChangeRequest.aidl
+package com.matter.tv.app.api;
+
+parcelable ReportAttributeChangeRequest{
+
+    int clusterIdentifier;
+
+    int attributeIdentifier;
+
+    String value;
+
+}

+ 10 - 0
examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SetSupportedClustersRequest.aidl

@@ -0,0 +1,10 @@
+// SetSupportedClustersRequest.aidl
+package com.matter.tv.app.api;
+
+import com.matter.tv.app.api.SupportedCluster;
+
+parcelable SetSupportedClustersRequest {
+
+    List<SupportedCluster> supportedClusters;
+
+}

+ 12 - 0
examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl

@@ -0,0 +1,12 @@
+// SupportedCluster.aidl
+package com.matter.tv.app.api;
+
+parcelable SupportedCluster {
+
+    int clusterIdentifier;
+
+    String[] features;
+
+    int[] optionalCommandIdentifiers;
+
+}

+ 12 - 0
examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/MatterIntentConstants.java

@@ -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 - 0
examples/tv-app/android/App/content-app/build.gradle

@@ -33,6 +33,7 @@ android {
                     'src/main/java',
                     '../common-api/src/main/java',
             ]
+            aidl.srcDirs = ['../common-api/src/main/aidl']
         }
     }
 

+ 7 - 1
examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml

@@ -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"
@@ -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-->

+ 8 - 0
examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java

@@ -2,12 +2,20 @@ package com.example.contentapp;
 
 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);
   }
 }

+ 216 - 0
examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/matter/MatterAgentClient.java

@@ -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;
+    }
+  }
+}

+ 17 - 1
examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java

@@ -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;
@@ -22,12 +23,27 @@ public class MatterCommandReceiver extends BroadcastReceiver {
       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,

+ 2 - 0
examples/tv-app/android/App/platform-app/build.gradle

@@ -44,10 +44,12 @@ android {
                     'src/main/java',
                     '../common-api/src/main/java',
             ]
+            aidl.srcDirs = ['../common-api/src/main/aidl']
 
             // uncomment this code to debug
 //            java.srcDirs = [
 //                    'src/main/java',
+//                    '../common-api/src/main/java',
 //                    '../../third_party/connectedhomeip/src/setup_payload/java/src',
 //                    '../../third_party/connectedhomeip/src/platform/android/java',
 //                    '../../third_party/connectedhomeip/src/app/server/java/src/',

+ 16 - 10
examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml

@@ -1,13 +1,18 @@
 <?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.matter.tv.server">
 
+    <!-- Permission to bind to Matter Agent service from Matter content apps-->
+    <permission android:name="com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION"
+        android:protectionLevel="normal" />
+
     <!-- Permission for Matter Agent to send Matter commands -->
     <permission
-        android:name="com.matter.app_agent_api.permission.SEND_DATA"
+        android:name="com.matter.tv.app.api.permission.SEND_DATA"
         android:protectionLevel="signature" />
 
-    <uses-permission android:name="com.matter.app_agent_api.permission.SEND_DATA" />
+    <uses-permission android:name="com.matter.tv.app.api.permission.SEND_DATA" />
 
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
@@ -18,6 +23,8 @@
     <uses-permission android:name="android.permission.NFC" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
+        tools:ignore="QueryAllPackagesPermission" />
 
     <application
         android:allowBackup="true"
@@ -39,17 +46,16 @@
         </activity>
 
         <service android:name="com.matter.tv.server.service.MatterServantService" />
-
-        <receiver
-            android:name="com.matter.tv.server.receivers.ContentAppDiscoveryService"
+        <service
+            android:name="com.matter.tv.server.service.ContentAppAgentService"
             android:enabled="true"
-            android:exported="true">
+            android:exported="true"
+            android:permission="com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION">
+
             <intent-filter>
-                <data android:scheme="package" />
-                <action android:name="android.intent.action.PACKAGE_ADDED" />
-                <action android:name="android.intent.action.PACKAGE_REMOVED" />
+                <action android:name="com.matter.tv.app.api.action.MatterAppAgent" />
             </intent-filter>
-        </receiver>
+        </service>
 
     </application>
 

+ 10 - 2
examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java

@@ -42,6 +42,14 @@ public class MainActivity extends AppCompatActivity {
 
   private LinkedHashMap<String, String> packages = new LinkedHashMap<>();
 
+  @Override
+  protected void onRestart() {
+    super.onRestart();
+    packages.clear();
+    ContentAppDiscoveryService.getReceiverInstance()
+        .initializeMatterApps(this.getApplicationContext());
+  }
+
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
@@ -140,8 +148,8 @@ public class MainActivity extends AppCompatActivity {
     registerReceiver(broadcastReceiver, new IntentFilter("com.matter.tv.server.appagent.add"));
     registerReceiver(broadcastReceiver, new IntentFilter("com.matter.tv.server.appagent.remove"));
 
-    ContentAppDiscoveryService.getRecieverInstance().registerSelf(this.getApplicationContext());
-    ContentAppDiscoveryService.getRecieverInstance()
+    ContentAppDiscoveryService.getReceiverInstance().registerSelf(this.getApplicationContext());
+    ContentAppDiscoveryService.getReceiverInstance()
         .initializeMatterApps(this.getApplicationContext());
   }
 

+ 7 - 5
examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/receivers/ContentAppDiscoveryService.java

@@ -17,7 +17,7 @@ import java.util.List;
 import java.util.Set;
 
 public class ContentAppDiscoveryService extends BroadcastReceiver {
-  private static final String TAG = "MatterContentAppDiscoveryService";
+  private static final String TAG = "ContentAppDiscoveryService";
   static final String CLUSTERS_RESOURCE_METADATA_KEY = "com.matter.app_agent_api.clusters";
   private static final String ANDROID_PACKAGE_REMOVED_ACTION =
       "android.intent.action.PACKAGE_REMOVED";
@@ -116,10 +116,10 @@ public class ContentAppDiscoveryService extends BroadcastReceiver {
 
     Log.i(TAG, "Trying to register the matter package update receiver");
     IntentFilter pckAdded = new IntentFilter(ANDROID_PACKAGE_ADDED_ACTION);
-    pckAdded.hasDataScheme("package");
+    pckAdded.addDataScheme("package");
     context.registerReceiver(this, pckAdded);
     IntentFilter pckRemoved = new IntentFilter(ANDROID_PACKAGE_REMOVED_ACTION);
-    pckRemoved.hasDataScheme("package");
+    pckRemoved.addDataScheme("package");
     context.registerReceiver(this, pckRemoved);
     Log.i(TAG, "Registered the matter package update receiver");
   }
@@ -134,7 +134,8 @@ public class ContentAppDiscoveryService extends BroadcastReceiver {
   private Set<String> getMatterApps(Context context) {
     PackageManager pm = context.getPackageManager();
     List<ResolveInfo> receivers =
-        pm.queryBroadcastReceivers(new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND), 0);
+        pm.queryBroadcastReceivers(
+            new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND), PackageManager.MATCH_ALL);
 
     Set<String> matterApps = new HashSet<>();
     if (receivers.isEmpty()) {
@@ -151,7 +152,8 @@ public class ContentAppDiscoveryService extends BroadcastReceiver {
     return matterApps;
   }
 
-  public static ContentAppDiscoveryService getRecieverInstance() {
+  // TODO : Introduce dependency injection
+  public static ContentAppDiscoveryService getReceiverInstance() {
     return instance;
   }
 }

+ 68 - 1
examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ContentAppAgentService.java

@@ -1,17 +1,53 @@
 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.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
 import androidx.annotation.Nullable;
+import com.matter.tv.app.api.IMatterAppAgent;
 import com.matter.tv.app.api.MatterIntentConstants;
 
 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";
+
+  private final IBinder appAgentBinder =
+      new IMatterAppAgent.Stub() {
+        @Override
+        public boolean setSupportedClusters(
+            com.matter.tv.app.api.SetSupportedClustersRequest request) throws RemoteException {
+          Log.d(
+              TAG,
+              "Received request to add the following supported clusters "
+                  + request.supportedClusters.toString());
+          // TODO : need to (re)discover the app with the new clusters.
+          return true;
+        }
+
+        @Override
+        public boolean reportAttributeChange(
+            com.matter.tv.app.api.ReportAttributeChangeRequest request) throws RemoteException {
+          return false;
+        }
+      };
+
   @Nullable
   @Override
-  public IBinder onBind(Intent intent) {
+  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;
   }
 
@@ -21,6 +57,37 @@ public class ContentAppAgentService extends Service {
     extras.putByteArray(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD, "test payload".getBytes());
     in.putExtras(extras);
     in.setPackage(packageName);
+    int flags = Intent.FLAG_INCLUDE_STOPPED_PACKAGES;
+    flags |= Intent.FLAG_RECEIVER_FOREGROUND;
+    in.setFlags(flags);
+    in.putExtra(
+        MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT,
+        getPendingIntentForResponse(context, packageName, "0"));
     context.sendBroadcast(in);
   }
+
+  private static PendingIntent getPendingIntentForResponse(
+      final Context context, final String targetPackage, final String 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) {
+    Log.d(TAG, "onStartCommand");
+
+    if (intent != null && ACTION_MATTER_RESPONSE.equals(intent.getAction())) {
+      Log.d(
+          TAG,
+          "Command response "
+              + intent.getByteArrayExtra(MatterIntentConstants.EXTRA_RESPONSE_PAYLOAD));
+      // Send the response back to the client.
+    }
+
+    return START_NOT_STICKY;
+  }
 }