diff --git a/examples/tv-app/android/App/.idea/gradle.xml b/examples/tv-app/android/App/.idea/gradle.xml
index 86ed2c6e5bddaf..021d59beb6e1eb 100644
--- a/examples/tv-app/android/App/.idea/gradle.xml
+++ b/examples/tv-app/android/App/.idea/gradle.xml
@@ -10,6 +10,7 @@
diff --git a/examples/tv-app/android/App/.idea/runConfigurations.xml b/examples/tv-app/android/App/.idea/runConfigurations.xml
deleted file mode 100644
index 797acea53eb091..00000000000000
--- a/examples/tv-app/android/App/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/examples/tv-app/android/App/common-api/.gitignore b/examples/tv-app/android/App/common-api/.gitignore
new file mode 100644
index 00000000000000..42afabfd2abebf
--- /dev/null
+++ b/examples/tv-app/android/App/common-api/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/MatterIntentConstants.java b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/MatterIntentConstants.java
new file mode 100644
index 00000000000000..3f78e9de06b1c4
--- /dev/null
+++ b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/MatterIntentConstants.java
@@ -0,0 +1,8 @@
+package com.matter.tv.app.api;
+
+public class MatterIntentConstants {
+
+ public static final String ACTION_MATTER_COMMAND = "com.matter.tv.app.api.action.MATTER_COMMAND";
+
+ public static final String EXTRA_COMMAND_PAYLOAD = "EXTRA_COMMAND_PAYLOAD";
+}
diff --git a/examples/tv-app/android/App/content-app/.gitignore b/examples/tv-app/android/App/content-app/.gitignore
new file mode 100644
index 00000000000000..42afabfd2abebf
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/build.gradle b/examples/tv-app/android/App/content-app/build.gradle
new file mode 100644
index 00000000000000..21553a014e51a7
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdk 30
+
+ defaultConfig {
+ applicationId "com.example.contentapp"
+ minSdk 24
+ targetSdk 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['../app/libs/jniLibs']
+ java.srcDirs = [
+ 'src/main/java',
+ '../common-api/src/main/java',
+ ]
+ }
+ }
+
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'com.google.android.material:material:1.4.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/proguard-rules.pro b/examples/tv-app/android/App/content-app/proguard-rules.pro
new file mode 100644
index 00000000000000..481bb434814107
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/androidTest/java/com/example/contentapp/ExampleInstrumentedTest.java b/examples/tv-app/android/App/content-app/src/androidTest/java/com/example/contentapp/ExampleInstrumentedTest.java
new file mode 100644
index 00000000000000..afcd98ec6b4e33
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/androidTest/java/com/example/contentapp/ExampleInstrumentedTest.java
@@ -0,0 +1,24 @@
+package com.example.contentapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.example.contentapp", appContext.getPackageName());
+ }
+}
diff --git a/examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml b/examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000000..107283a2474a44
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java
new file mode 100644
index 00000000000000..6b1474e21b57e1
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java
@@ -0,0 +1,13 @@
+package com.example.contentapp;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+}
diff --git a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java
new file mode 100644
index 00000000000000..21f285cac24878
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java
@@ -0,0 +1,40 @@
+package com.example.contentapp.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import com.matter.tv.app.api.MatterIntentConstants;
+
+public class MatterCommandReceiver extends BroadcastReceiver {
+ private static final String TAG = "MatterCommandReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String intentAction = intent.getAction();
+ Log.i(TAG, "Some Intent received from the matter server " + intentAction);
+ if (intentAction == null || intentAction.isEmpty()) {
+ Log.i(TAG, "empty action.");
+ return;
+ }
+
+ switch (intentAction) {
+ case MatterIntentConstants.ACTION_MATTER_COMMAND:
+ byte[] commandPayload =
+ intent.getByteArrayExtra(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD);
+ Log.e(
+ TAG,
+ new StringBuilder()
+ .append("Received matter command: ")
+ .append(intent.getAction())
+ .toString());
+ default:
+ Log.e(
+ TAG,
+ new StringBuilder()
+ .append("Received unknown Intent: ")
+ .append(intent.getAction())
+ .toString());
+ }
+ }
+}
diff --git a/examples/tv-app/android/App/content-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/tv-app/android/App/content-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000000000..2b068d11462a4b
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/drawable/ic_launcher_background.xml b/examples/tv-app/android/App/content-app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000000000..07d5da9cbf1419
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml b/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000000000..4fc244418b5fe5
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000000000..eca70cfe52eac1
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000000000..eca70cfe52eac1
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000000000..c209e78ecd3723
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000000000..b2dfe3d1ba5cf3
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000000000..4f0f1d64e58ba6
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000000000..62b611da081676
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000000000..948a3070fe34c6
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000000000..1b9a6956b3acdc
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000000000..28d4b77f9f036a
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000000000..9287f5083623b3
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000000000..aa7d6427e6fa10
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000000000..9126ae37cbc358
Binary files /dev/null and b/examples/tv-app/android/App/content-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/tv-app/android/App/content-app/src/main/res/raw/static_matter_clusters b/examples/tv-app/android/App/content-app/src/main/res/raw/static_matter_clusters
new file mode 100644
index 00000000000000..836bfe530df37a
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/raw/static_matter_clusters
@@ -0,0 +1,13 @@
+{
+ "clusters": [
+ {
+ "identifier": "0x050a",
+ "features": "CS"
+ },
+ {
+ "identifier": "0x0506",
+ "features": "AS",
+ "optionalCommands" : [4, 5]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/values-night/themes.xml b/examples/tv-app/android/App/content-app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000000000..7a8937006f4dae
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/values/colors.xml b/examples/tv-app/android/App/content-app/src/main/res/values/colors.xml
new file mode 100644
index 00000000000000..f8c6127d327620
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/values/strings.xml b/examples/tv-app/android/App/content-app/src/main/res/values/strings.xml
new file mode 100644
index 00000000000000..efae55a88a95ea
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Content App
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/main/res/values/themes.xml b/examples/tv-app/android/App/content-app/src/main/res/values/themes.xml
new file mode 100644
index 00000000000000..f39e1eabcca031
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/content-app/src/test/java/com/example/contentapp/ExampleUnitTest.java b/examples/tv-app/android/App/content-app/src/test/java/com/example/contentapp/ExampleUnitTest.java
new file mode 100644
index 00000000000000..2cf192922ae87b
--- /dev/null
+++ b/examples/tv-app/android/App/content-app/src/test/java/com/example/contentapp/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.contentapp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/build.gradle b/examples/tv-app/android/App/platform-app/build.gradle
index 8c9d066650ed5f..e41d3556a488ae 100644
--- a/examples/tv-app/android/App/platform-app/build.gradle
+++ b/examples/tv-app/android/App/platform-app/build.gradle
@@ -40,6 +40,10 @@ android {
sourceSets {
main {
jniLibs.srcDirs = ['../app/libs/jniLibs']
+ java.srcDirs = [
+ 'src/main/java',
+ '../common-api/src/main/java',
+ ]
// uncomment this code to debug
// java.srcDirs = [
diff --git a/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml b/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
index e9a3f07fed5111..02cd1a760b91c6 100644
--- a/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
@@ -2,6 +2,13 @@
+
+
+
+
+
@@ -21,6 +28,7 @@
android:supportsRtl="true"
android:name="com.matter.tv.server.MatterTvServerApplication"
android:theme="@style/Theme.MatterTVSlave">
+
@@ -29,7 +37,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
index e16937f8248324..3d37170f7479da 100644
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
@@ -1,23 +1,46 @@
package com.matter.tv.server;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
import android.widget.ImageView;
+import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import chip.setuppayload.DiscoveryCapability;
import chip.setuppayload.SetupPayload;
import chip.setuppayload.SetupPayloadParser;
+import com.matter.tv.server.receivers.ContentAppDiscoveryService;
+import com.matter.tv.server.service.ContentAppAgentService;
import com.matter.tv.server.service.MatterServant;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MatterMainActivity";
+
private ImageView mQrCodeImg;
private TextView mQrCodeTxt;
private TextView mManualPairingCodeTxt;
+ private BroadcastReceiver broadcastReceiver;
+ private ListView pkgUpdatesView;
+
+ private LinkedHashMap packages = new LinkedHashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -81,5 +104,95 @@ public void onStopTrackingTouch(SeekBar seekBar) {}
} catch (SetupPayloadParser.SetupPayloadException e) {
e.printStackTrace();
}
+
+ ArrayList> lst =
+ new ArrayList>(packages.entrySet());
+
+ ContentAppListAdapter adapter = new ContentAppListAdapter(this, R.layout.applist_item, lst);
+
+ pkgUpdatesView = findViewById(R.id.pkgUpdates);
+ pkgUpdatesView.setAdapter(adapter);
+ registerReceiver(adapter);
+ }
+
+ private void registerReceiver(ArrayAdapter adapter) {
+ broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ String packageName = intent.getStringExtra("com.matter.tv.server.appagent.add.pkg");
+ if (action.equals("com.matter.tv.server.appagent.add")) {
+ packages.put(
+ packageName, intent.getStringExtra("com.matter.tv.server.appagent.add.clusters"));
+ adapter.clear();
+ adapter.addAll(packages.entrySet().toArray());
+ adapter.notifyDataSetChanged();
+ } else if (action.equals("com.matter.tv.server.appagent.remove")) {
+ if (packages.remove(packageName) != null) {
+ adapter.clear();
+ adapter.addAll(packages.entrySet().toArray());
+ adapter.notifyDataSetChanged();
+ }
+ }
+ }
+ };
+ 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()
+ .initializeMatterApps(this.getApplicationContext());
+ }
+
+ private class ContentAppListAdapter extends ArrayAdapter> {
+
+ private int layout;
+
+ public ContentAppListAdapter(
+ @NonNull Context context,
+ int resource,
+ @NonNull ArrayList> packages) {
+ super(context, resource, packages);
+ layout = resource;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder mainViewHolder = null;
+ if (convertView == null) {
+ LayoutInflater inflator = LayoutInflater.from(getContext());
+ convertView = inflator.inflate(layout, parent, false);
+ ViewHolder viewHolder = new ViewHolder();
+ viewHolder.appName = (TextView) convertView.findViewById(R.id.appNameTextView);
+ viewHolder.appDetails = (TextView) convertView.findViewById(R.id.appDetailsTextView);
+ viewHolder.appName.setText(getItem(position).getKey());
+ viewHolder.appDetails.setText(getItem(position).getValue());
+ viewHolder.sendMessageButton = (Button) convertView.findViewById(R.id.sendMessageButton);
+ viewHolder.sendMessageButton.setText(R.string.send_command);
+ viewHolder.sendMessageButton.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Log.i(TAG, "Button was clicked for " + position);
+ ContentAppAgentService.sendCommand(
+ getApplicationContext(), getItem(position).getKey());
+ }
+ });
+ convertView.setTag(viewHolder);
+ } else {
+ mainViewHolder = (ViewHolder) convertView.getTag();
+ mainViewHolder.appName.setText(getItem(position).getKey());
+ mainViewHolder.appDetails.setText(getItem(position).getValue());
+ }
+ return convertView;
+ }
+ }
+
+ public class ViewHolder {
+ TextView appName;
+ TextView appDetails;
+ Button sendMessageButton;
}
}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/receivers/ContentAppDiscoveryService.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/receivers/ContentAppDiscoveryService.java
new file mode 100644
index 00000000000000..a108d6118e0c4f
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/receivers/ContentAppDiscoveryService.java
@@ -0,0 +1,157 @@
+package com.matter.tv.server.receivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.Log;
+import com.matter.tv.app.api.MatterIntentConstants;
+import com.matter.tv.server.utils.ResourceUtils;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ContentAppDiscoveryService extends BroadcastReceiver {
+ private static final String TAG = "MatterContentAppDiscoveryService";
+ 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";
+ private static final String ANDROID_PACKAGE_ADDED_ACTION = "android.intent.action.PACKAGE_ADDED";
+
+ private static ResourceUtils resourceUtils = ResourceUtils.getInstance();
+
+ private static final ContentAppDiscoveryService instance = new ContentAppDiscoveryService();
+
+ public ContentAppDiscoveryService() {}
+
+ private volatile boolean registered = false;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String intentAction = intent.getAction();
+ Log.i(TAG, "Some Intent received by the matter server " + intentAction);
+ if (intentAction == null || intentAction.isEmpty()) {
+ Log.i(TAG, "empty action.");
+ return;
+ }
+
+ switch (intentAction) {
+ case Intent.ACTION_BOOT_COMPLETED:
+ // discoveryAgent.init();
+ break;
+ case ANDROID_PACKAGE_ADDED_ACTION:
+ handlePackageAdded(intent, context);
+ break;
+ case ANDROID_PACKAGE_REMOVED_ACTION:
+ handlePackageRemoved(intent, context);
+ break;
+ default:
+ Log.e(
+ TAG,
+ new StringBuilder()
+ .append("Received unknown Intent: ")
+ .append(intent.getAction())
+ .toString());
+ }
+ }
+
+ private void handlePackageAdded(final Intent intent, final Context context) {
+ String pkg = intent.getData().getSchemeSpecificPart();
+ Log.i(TAG, pkg + " Added. MATTERSERVER");
+
+ handlePackageAdded(context, pkg);
+ }
+
+ private void handlePackageAdded(Context context, String pkg) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ ApplicationInfo appInfo = pm.getApplicationInfo(pkg, PackageManager.GET_META_DATA);
+ if (appInfo.metaData == null) {
+ Log.i(TAG, pkg + " has no metadata.");
+ return;
+ }
+
+ int resId = appInfo.metaData.getInt(CLUSTERS_RESOURCE_METADATA_KEY, 0);
+ Log.d(TAG, "got static capability for package " + pkg + ", resourceId: " + resId);
+ if (resId != 0) {
+ Resources res = pm.getResourcesForApplication(appInfo);
+ String rawJson = resourceUtils.getRawTextResource(res, resId);
+ Log.d(TAG, "Got capabilities resource:\n" + rawJson);
+
+ Intent in = new Intent("com.matter.tv.server.appagent.add");
+ Bundle extras = new Bundle();
+ extras.putString("com.matter.tv.server.appagent.add.pkg", pkg);
+ extras.putString("com.matter.tv.server.appagent.add.clusters", rawJson);
+ in.putExtras(extras);
+ context.sendBroadcast(in);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Could not find package " + pkg, e);
+ }
+ }
+
+ private void handlePackageRemoved(final Intent intent, final Context context) {
+ String pkg = intent.getData().getSchemeSpecificPart();
+ Log.i(TAG, pkg + " Removed. MATTERSERVER");
+ Intent in = new Intent("com.matter.tv.server.appagent.remove");
+ Bundle extras = new Bundle();
+ extras.putString("com.matter.tv.server.appagent.add.pkg", pkg);
+ in.putExtras(extras);
+ context.sendBroadcast(in);
+ }
+
+ public void registerSelf(Context context) {
+ Log.i(TAG, "Starting the registration of the matter package update receiver");
+ if (registered) {
+ Log.i(TAG, "Package update receiver for matter already registered");
+ return;
+ } else {
+ registered = true;
+ }
+
+ Log.i(TAG, "Trying to register the matter package update receiver");
+ IntentFilter pckAdded = new IntentFilter(ANDROID_PACKAGE_ADDED_ACTION);
+ pckAdded.hasDataScheme("package");
+ context.registerReceiver(this, pckAdded);
+ IntentFilter pckRemoved = new IntentFilter(ANDROID_PACKAGE_REMOVED_ACTION);
+ pckRemoved.hasDataScheme("package");
+ context.registerReceiver(this, pckRemoved);
+ Log.i(TAG, "Registered the matter package update receiver");
+ }
+
+ public void initializeMatterApps(Context context) {
+ Set matterApps = getMatterApps(context);
+ for (String matterApp : matterApps) {
+ handlePackageAdded(context, matterApp);
+ }
+ }
+
+ private Set getMatterApps(Context context) {
+ PackageManager pm = context.getPackageManager();
+ List receivers =
+ pm.queryBroadcastReceivers(new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND), 0);
+
+ Set matterApps = new HashSet<>();
+ if (receivers.isEmpty()) {
+ Log.i(TAG, "No receivers for Matter apps found.");
+ return matterApps;
+ }
+
+ for (ResolveInfo receiver : receivers) {
+ if (receiver != null && receiver.activityInfo != null) {
+ Log.i("Activity Info found. Package Name is %s", receiver.activityInfo.packageName);
+ matterApps.add(receiver.activityInfo.packageName);
+ }
+ }
+ return matterApps;
+ }
+
+ public static ContentAppDiscoveryService getRecieverInstance() {
+ return instance;
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ContentAppAgentService.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ContentAppAgentService.java
new file mode 100644
index 00000000000000..d9fb849aae26e8
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ContentAppAgentService.java
@@ -0,0 +1,26 @@
+package com.matter.tv.server.service;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import androidx.annotation.Nullable;
+import com.matter.tv.app.api.MatterIntentConstants;
+
+public class ContentAppAgentService extends Service {
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ public static void sendCommand(Context context, String packageName) {
+ Intent in = new Intent(MatterIntentConstants.ACTION_MATTER_COMMAND);
+ Bundle extras = new Bundle();
+ extras.putByteArray(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD, "test payload".getBytes());
+ in.putExtras(extras);
+ in.setPackage(packageName);
+ context.sendBroadcast(in);
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/ResourceUtils.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/ResourceUtils.java
new file mode 100644
index 00000000000000..769b039efbd267
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/ResourceUtils.java
@@ -0,0 +1,51 @@
+package com.matter.tv.server.utils;
+
+import android.content.res.Resources;
+import android.util.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/** Util class for dealing with android Resources. */
+public class ResourceUtils {
+ private static final String TAG = "ResourceUtils";
+ private static final ResourceUtils resourceUtils = new ResourceUtils();
+
+ private ResourceUtils() {}
+
+ public static ResourceUtils getInstance() {
+ return resourceUtils;
+ }
+
+ /**
+ * Attempts to open the resource given by the resourceId as a raw resource and return its contents
+ * as a string.
+ *
+ * @param resources Resources object containing the resource
+ * @param resId resourceId
+ * @return String containing the contents of the resource file. Null if an error occurred.
+ */
+ public String getRawTextResource(final Resources resources, final int resId) {
+ // TODO: Enforce some size limit on raw resource text? We don't want to
+ // potentially take a long time reading an image file or something like that.
+
+ // openRawResource cannot return null, it either succeeds or throws an exception
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(resources.openRawResource(resId)))) {
+
+ String line;
+ StringBuilder result = new StringBuilder();
+
+ while ((line = reader.readLine()) != null) {
+ result.append(line).append('\n');
+ }
+ return result.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading raw resource for id " + resId);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Could not find raw resource for id " + resId);
+ }
+
+ return null;
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/layout/activity_main.xml b/examples/tv-app/android/App/platform-app/src/main/res/layout/activity_main.xml
index caa0d4e651d450..b3514808fce848 100644
--- a/examples/tv-app/android/App/platform-app/src/main/res/layout/activity_main.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/res/layout/activity_main.xml
@@ -63,4 +63,9 @@
android:max="100"
/>
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/layout/applist_item.xml b/examples/tv-app/android/App/platform-app/src/main/res/layout/applist_item.xml
new file mode 100644
index 00000000000000..6d3dd40d2c2400
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/layout/applist_item.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/values/strings.xml b/examples/tv-app/android/App/platform-app/src/main/res/values/strings.xml
index 5d203be00da7da..f6a1943ceff7c3 100644
--- a/examples/tv-app/android/App/platform-app/src/main/res/values/strings.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
MatterTVServer
+ Send Command
\ No newline at end of file
diff --git a/examples/tv-app/android/App/settings.gradle b/examples/tv-app/android/App/settings.gradle
index 9775d632e171e9..cc7e3deab70c5d 100644
--- a/examples/tv-app/android/App/settings.gradle
+++ b/examples/tv-app/android/App/settings.gradle
@@ -1,2 +1,3 @@
rootProject.name = "MatterTVAndroid"
include ':platform-app'
+include ':content-app'
diff --git a/scripts/build/builders/android.py b/scripts/build/builders/android.py
index 52b6317959269e..6653cd88f6a7a3 100644
--- a/scripts/build/builders/android.py
+++ b/scripts/build/builders/android.py
@@ -95,6 +95,12 @@ def ExampleName(self):
else:
return None
+ def Modules(self):
+ if self == AndroidApp.CHIP_TVServer:
+ return ["platform-app", "content-app"]
+ else:
+ return None
+
class AndroidBuilder(Builder):
@@ -225,16 +231,29 @@ def gradlewBuildSrcAndroid(self):
def gradlewBuildExampleAndroid(self):
# Example compilation
- self._Execute([
- '%s/examples/%s/android/App/gradlew' % (self.root,
- self.app.ExampleName()), '-p',
- '%s/examples/%s/android/App/' % (self.root,
- self.app.ExampleName()),
- '-PmatterBuildSrcDir=%s' % self.output_dir,
- '-PmatterSdkSourceBuild=false',
- '-PbuildDir=%s' % self.output_dir, 'assembleDebug'
- ],
- title='Building Example ' + self.identifier)
+ if self.app.Modules():
+ for module in self.app.Modules():
+ self._Execute([
+ '%s/examples/%s/android/App/gradlew' % (self.root,
+ self.app.ExampleName()), '-p',
+ '%s/examples/%s/android/App/' % (self.root,
+ self.app.ExampleName()),
+ '-PmatterBuildSrcDir=%s' % self.output_dir,
+ '-PmatterSdkSourceBuild=false',
+ '-PbuildDir=%s/%s' % (self.output_dir, module), ':%s:assembleDebug' % module
+ ],
+ title='Building Example %s, module %s' % (self.identifier, module))
+ else:
+ self._Execute([
+ '%s/examples/%s/android/App/gradlew' % (self.root,
+ self.app.ExampleName()), '-p',
+ '%s/examples/%s/android/App/' % (self.root,
+ self.app.ExampleName()),
+ '-PmatterBuildSrcDir=%s' % self.output_dir,
+ '-PmatterSdkSourceBuild=false',
+ '-PbuildDir=%s' % self.output_dir, 'assembleDebug'
+ ],
+ title='Building Example ' + self.identifier)
def generate(self):
self._Execute([
diff --git a/scripts/build/testdata/build_all_except_host.txt b/scripts/build/testdata/build_all_except_host.txt
index 83ec903009cfff..d0a931a883877b 100644
--- a/scripts/build/testdata/build_all_except_host.txt
+++ b/scripts/build/testdata/build_all_except_host.txt
@@ -1103,8 +1103,11 @@ cp {out}/android-arm-chip-tvserver/lib/third_party/connectedhomeip/src/app/serve
cp {out}/android-arm-chip-tvserver/lib/TvApp.jar {root}/examples/tv-app/android/App/app/libs/TvApp.jar
-# Building Example android-arm-chip-tvserver
-{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm-chip-tvserver assembleDebug
+# Building Example android-arm-chip-tvserver, module platform-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm-chip-tvserver/platform-app :platform-app:assembleDebug
+
+# Building Example android-arm-chip-tvserver, module content-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm-chip-tvserver/content-app :content-app:assembleDebug
# Building JNI android-arm64-chip-test
ninja -C {out}/android-arm64-chip-test
@@ -1191,8 +1194,11 @@ cp {out}/android-arm64-chip-tvserver/lib/third_party/connectedhomeip/src/app/ser
cp {out}/android-arm64-chip-tvserver/lib/TvApp.jar {root}/examples/tv-app/android/App/app/libs/TvApp.jar
-# Building Example android-arm64-chip-tvserver
-{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm64-chip-tvserver assembleDebug
+# Building Example android-arm64-chip-tvserver, module platform-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm64-chip-tvserver/platform-app :platform-app:assembleDebug
+
+# Building Example android-arm64-chip-tvserver, module content-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-arm64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-arm64-chip-tvserver/content-app :content-app:assembleDebug
# Building JNI android-x64-chip-tool
ninja -C {out}/android-x64-chip-tool
@@ -1235,8 +1241,11 @@ cp {out}/android-x64-chip-tvserver/lib/third_party/connectedhomeip/src/app/serve
cp {out}/android-x64-chip-tvserver/lib/TvApp.jar {root}/examples/tv-app/android/App/app/libs/TvApp.jar
-# Building Example android-x64-chip-tvserver
-{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x64-chip-tvserver assembleDebug
+# Building Example android-x64-chip-tvserver, module platform-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x64-chip-tvserver/platform-app :platform-app:assembleDebug
+
+# Building Example android-x64-chip-tvserver, module content-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x64-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x64-chip-tvserver/content-app :content-app:assembleDebug
# Building JNI android-x86-chip-tool
ninja -C {out}/android-x86-chip-tool
@@ -1279,8 +1288,11 @@ cp {out}/android-x86-chip-tvserver/lib/third_party/connectedhomeip/src/app/serve
cp {out}/android-x86-chip-tvserver/lib/TvApp.jar {root}/examples/tv-app/android/App/app/libs/TvApp.jar
-# Building Example android-x86-chip-tvserver
-{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x86-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x86-chip-tvserver assembleDebug
+# Building Example android-x86-chip-tvserver, module platform-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x86-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x86-chip-tvserver/platform-app :platform-app:assembleDebug
+
+# Building Example android-x86-chip-tvserver, module content-app
+{root}/examples/tv-app/android/App/gradlew -p {root}/examples/tv-app/android/App/ -PmatterBuildSrcDir={out}/android-x86-chip-tvserver -PmatterSdkSourceBuild=false -PbuildDir={out}/android-x86-chip-tvserver/content-app :content-app:assembleDebug
# Building bl602-light
ninja -C {out}/bl602-light