diff --git a/examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl b/examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl index 7db26283d00de1..72a6cc2a89da83 100644 --- a/examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl +++ b/examples/tv-app/android/App/common-api/src/main/aidl/com/matter/tv/app/api/SupportedCluster.aidl @@ -5,8 +5,9 @@ parcelable SupportedCluster { int clusterIdentifier; - String[] features; + int features; int[] optionalCommandIdentifiers; + int[] optionalAttributesIdentifiers; } \ No newline at end of file diff --git a/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java index dbb80b1e884933..93cd816eb95984 100644 --- a/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java +++ b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java @@ -1,28 +1,260 @@ package com.matter.tv.app.api; +/** + * Helper class to hold the IDs and corresponding constants for media related clusters. TODO : Add + * the rest of the media clusters TODO : Maybe generate using ZAP tool + */ public class Clusters { // Clusters public static class MediaPlayback { - public static final int Id = 1286; + public static final int Id = 0x0506; + + public static class Commands { + public static class Play { + public static final int ID = 0x00; + } + + public static class Pause { + public static final int ID = 0x01; + } + + public static class StopPlayback { + public static final int ID = 0x02; + } + + public static class StartOver { + public static final int ID = 0x03; + } + + public static class Previous { + public static final int ID = 0x04; + } + + public static class Next { + public static final int ID = 0x05; + } + + public static class Rewind { + public static final int ID = 0x06; + } + + public static class FastForward { + public static final int ID = 0x07; + } + + public static class SkipForward { + public static final int ID = 0x08; + + public static class Fields { + public static final int DeltaPositionMilliseconds = 0x00; + } + } + + public static class SkipBackward { + public static final int ID = 0x09; + + public static class Fields { + public static final int DeltaPositionMilliseconds = 0x00; + } + } + + public static class Seek { + public static final int ID = 0x0B; + + public static class Fields { + public static final int Position = 0x00; + } + } + + public static class PlaybackResponse { + public static final int ID = 0x0A; + + public static class Fields { + public static final int Status = 0x00; + public static final int Data = 0x01; + } + } + } public static class Attributes { - public static final int CurrentState = 0; + public static final int CurrentState = 0x00; + public static final int StartTime = 0x01; + public static final int Duration = 0x02; + public static final int SampledPosition = 0x03; + public static final int PlaybackSpeed = 0x04; + public static final int SeekRangeEnd = 0x05; + public static final int SeekRangeStart = 0x06; } - public static class PlaybackStateEnum { - public static final int Playing = 0; - public static final int Paused = 1; - public static final int NotPlaying = 2; - public static final int Buffering = 3; + public static class Types { + public static class PlaybackStateEnum { + public static final int Playing = 0x00; + public static final int Paused = 0x01; + public static final int NotPlaying = 0x02; + public static final int Buffering = 0x03; + } + + public static class StatusEnum { + public static final int Success = 0x00; + public static final int InvalidStateForCommand = 0x01; + public static final int NotAllowed = 0x02; + public static final int NotActive = 0x03; + public static final int SpeedOutOfRange = 0x04; + public static final int SeekOutOfRange = 0x05; + } + + public static class PlaybackPosition { + public static final int UpdatedAt = 0x00; + public static final int Position = 0x01; + } } } public static class ContentLauncher { - public static final int Id = 1290; + public static final int Id = 0x050A; + + public static class Commands { + public static class LaunchContent { + public static final int ID = 0x00; + + public static class Fields { + public static final int Search = 0x00; + public static final int AutoPlay = 0x01; + public static final int Data = 0x02; + } + } + + public static class LaunchURL { + public static final int ID = 0x01; + + public static class Fields { + public static final int ContentURL = 0x00; + public static final int DisplayString = 0x01; + public static final int BrandingInformation = 0x02; + } + } + + public static class LaunchResponse { + public static final int ID = 0x02; + + public static class Fields { + public static final int Status = 0x00; + public static final int Data = 0x01; + } + } + } + + public static class Attributes { + public static final int AcceptHeader = 0x00; + public static final int SupportedStreamingProtocols = 0x01; + } + + public static class Types { + public static class ContentSearch { + public static final int ParameterList = 0x00; + } + + public static class StatusEnum { + public static final int Success = 0x00; + public static final int UrlNotAvailable = 0x01; + public static final int AuthFailed = 0x02; + } + + public static class Parameter { + public static final int Type = 0x00; + public static final int Value = 0x01; + public static final int ExternalIDList = 0x02; + } + + public static class ParameterEnum { + public static final int Actor = 0x00; + public static final int Channel = 0x01; + public static final int Character = 0x02; + public static final int Director = 0x03; + public static final int Event = 0x04; + public static final int Franchise = 0x05; + public static final int Genre = 0x06; + public static final int League = 0x07; + public static final int Popularity = 0x08; + public static final int Provider = 0x09; + public static final int Sport = 0x0A; + public static final int SportsTeam = 0x0B; + public static final int Type = 0x0C; + public static final int Video = 0x0D; + } + + public static class AdditionalInfo { + public static final int Name = 0x00; + public static final int Value = 0x01; + } + + public static class BrandingInformation { + public static final int ProviderName = 0x00; + public static final int Background = 0x01; + public static final int Logo = 0x02; + public static final int ProgressBar = 0x03; + public static final int Splash = 0x04; + public static final int WaterMark = 0x05; + } + + public static class StyleInformation { + public static final int ProviderName = 0x00; + public static final int Background = 0x01; + public static final int Logo = 0x02; + } + + public static class Dimension { + public static final int ImageUrl = 0x00; + public static final int Color = 0x01; + public static final int Size = 0x02; + } + + public static class MetricTypeEnum { + public static final int Pixels = 0x00; + public static final int Percentage = 0x01; + } + } + } + + public static class TargetNavigator { + public static final int Id = 0x0505; + + public static class Commands { + public static class NavigateTarget { + public static final int ID = 0x00; + + public static class Fields { + public static final int Target = 0x00; + public static final int Data = 0x01; + } + } + + public static class NavigateTargetResponse { + public static final int ID = 0x01; + + public static class Fields { + public static final int Status = 0x00; + public static final int Data = 0x01; + } + } + } public static class Attributes { - public static final int AcceptHeader = 0; - public static final int SupportedStreamingProtocols = 1; + public static final int TargetList = 0x00; + public static final int CurrentTarget = 0x01; + } + + public static class Types { + public static class TargetInfo { + public static final int Identifier = 0x00; + public static final int Name = 0x01; + } + + public static class StatusEnum { + public static final int Success = 0x00; + public static final int TargetNotFound = 0x01; + public static final int NotAllowed = 0x02; + } } } } 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 index e9fa3b028f6f58..61d036e0a90ef4 100644 --- a/examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml +++ b/examples/tv-app/android/App/content-app/src/main/AndroidManifest.xml @@ -25,8 +25,8 @@ android:exported="true"> - + > attributeValues = new HashMap<>(); - private AttributeHolder() {}; + private AttributeHolder() { + // Setting up attribute defaults + setAttributeValue( + Clusters.ContentLauncher.Id, + Clusters.ContentLauncher.Attributes.AcceptHeader, + "[\"video/mp4\", \"application/x-mpegURL\", \"application/dash+xml\"]"); + setAttributeValue( + Clusters.ContentLauncher.Id, + Clusters.ContentLauncher.Attributes.SupportedStreamingProtocols, + 3); + setAttributeValue(Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, 2); + setAttributeValue( + Clusters.TargetNavigator.Id, + Clusters.TargetNavigator.Attributes.TargetList, + "[{\"0\":1, \"1\":\"Home\"},{\"0\":2, \"1\":\"Settings\"},{\"0\":3, \"1\":\"Casting Home\"}]"); + setAttributeValue( + Clusters.TargetNavigator.Id, Clusters.TargetNavigator.Attributes.CurrentTarget, 1); + }; public static AttributeHolder getInstance() { return instance; 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 index c346a5c1ea4b82..a5d6d706e35f34 100644 --- 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 @@ -49,7 +49,7 @@ protected void onCreate(Bundle savedInstanceState) { .setAttributeValue( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, - Clusters.MediaPlayback.PlaybackStateEnum.Playing); + Clusters.MediaPlayback.Types.PlaybackStateEnum.Playing); reportAttributeChange( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState); break; @@ -58,7 +58,7 @@ protected void onCreate(Bundle savedInstanceState) { .setAttributeValue( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, - Clusters.MediaPlayback.PlaybackStateEnum.Paused); + Clusters.MediaPlayback.Types.PlaybackStateEnum.Paused); reportAttributeChange( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState); break; @@ -67,7 +67,7 @@ protected void onCreate(Bundle savedInstanceState) { .setAttributeValue( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, - Clusters.MediaPlayback.PlaybackStateEnum.Buffering); + Clusters.MediaPlayback.Types.PlaybackStateEnum.Buffering); reportAttributeChange( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState); break; @@ -76,7 +76,7 @@ protected void onCreate(Bundle savedInstanceState) { .setAttributeValue( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, - Clusters.MediaPlayback.PlaybackStateEnum.NotPlaying); + Clusters.MediaPlayback.Types.PlaybackStateEnum.NotPlaying); reportAttributeChange( Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState); break; @@ -91,21 +91,6 @@ protected void onCreate(Bundle savedInstanceState) { MatterAgentClient matterAgentClient = MatterAgentClient.getInstance(); executorService.execute(matterAgentClient::reportClusters); - - // Setting up attribute defaults - AttributeHolder.getInstance() - .setAttributeValue( - Clusters.ContentLauncher.Id, - Clusters.ContentLauncher.Attributes.AcceptHeader, - "[\"video/mp4\", \"application/x-mpegURL\", \"application/dash+xml\"]"); - AttributeHolder.getInstance() - .setAttributeValue( - Clusters.ContentLauncher.Id, - Clusters.ContentLauncher.Attributes.SupportedStreamingProtocols, - 3); - AttributeHolder.getInstance() - .setAttributeValue( - Clusters.MediaPlayback.Id, Clusters.MediaPlayback.Attributes.CurrentState, 2); } private void reportAttributeChange(final int clusterId, final int attributeId) { 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 index 930dc63d26f43a..c8d8987048be76 100644 --- 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 @@ -43,6 +43,7 @@ public void onReceive(Context context, Intent intent) { Intent in = new Intent(context, MainActivity.class); in.putExtra(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD, command); + in.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(in); Log.d(TAG, "Started activity. Now sending response"); 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 cfa74283d71759..04b30f27ad639d 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 @@ -39,8 +39,9 @@ android:launchMode="singleTask"> - + + diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/handlers/ContentAppEndpointManagerImpl.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/handlers/ContentAppEndpointManagerImpl.java index 0b9ea344b161ad..6f4e6eb98c52e2 100644 --- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/handlers/ContentAppEndpointManagerImpl.java +++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/handlers/ContentAppEndpointManagerImpl.java @@ -6,6 +6,8 @@ import com.matter.tv.server.receivers.ContentAppDiscoveryService; import com.matter.tv.server.service.ContentAppAgentService; import com.matter.tv.server.tvapp.ContentAppEndpointManager; +import com.matter.tv.server.utils.EndpointsDataStore; +import java.util.Collection; public class ContentAppEndpointManagerImpl implements ContentAppEndpointManager { @@ -18,15 +20,37 @@ public ContentAppEndpointManagerImpl(Context context) { public String sendCommand(int endpointId, int clusterId, int commandId, String commandPayload) { Log.d(TAG, "Received a command for endpointId " + endpointId + ". Message " + commandPayload); - for (ContentApp app : - ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps().values()) { - if (app.getEndpointId() == endpointId) { - Log.d( - TAG, "Sending a command for endpointId " + endpointId + ". Message " + commandPayload); - return ContentAppAgentService.sendCommand( - context, app.getAppName(), clusterId, commandId, commandPayload); - } + + ContentApp discoveredApp = + getContentApp( + ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps().values(), + endpointId); + if (discoveredApp != null) { + Log.d(TAG, "Sending a command for endpointId " + endpointId + ". Message " + commandPayload); + return ContentAppAgentService.sendCommand( + context, discoveredApp.getAppName(), clusterId, commandId, commandPayload); } + + // check to see if this was a previously discovered but now removed app + ContentApp persistedApp = + getContentApp( + EndpointsDataStore.getInstance(context).getAllPersistedContentApps().values(), + endpointId); + if (persistedApp != null) { + Log.d( + TAG, + "Message received for a previously discovered app that is no longer " + + "available. App Name " + + persistedApp.getAppName()); + return "{\"" + + ContentAppAgentService.FAILURE_KEY + + "\":{\"" + + ContentAppAgentService.FAILURE_STATUS_KEY + + "\":" + + ContentAppAgentService.FAILED_UNSUPPORTED_ENDPOINT + + "}}"; + } + // For test cases to pass. return "Success"; } @@ -39,14 +63,44 @@ public String readAttribute(int endpointId, int clusterId, int attributeId) { + clusterId + " attributeId " + attributeId); - for (ContentApp app : - ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps().values()) { + ContentApp discoveredApp = + getContentApp( + ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps().values(), + endpointId); + if (discoveredApp != null) { + Log.d(TAG, "Sending attribute read request for endpointId " + endpointId); + return ContentAppAgentService.sendAttributeReadRequest( + context, discoveredApp.getAppName(), clusterId, attributeId); + } + // check to see if this was a previously discovered but now removed app + ContentApp persistedApp = + getContentApp( + EndpointsDataStore.getInstance(context).getAllPersistedContentApps().values(), + endpointId); + if (persistedApp != null) { + Log.d( + TAG, + "Message received for a previously discovered app that is no longer " + + "available. App Name " + + persistedApp.getAppName()); + return "{\"" + + ContentAppAgentService.FAILURE_KEY + + "\":{\"" + + ContentAppAgentService.FAILURE_STATUS_KEY + + "\":" + + ContentAppAgentService.FAILED_UNSUPPORTED_ENDPOINT + + "}}"; + } + // For test cases to pass. + return ""; + } + + ContentApp getContentApp(Collection apps, int endpointId) { + for (ContentApp app : apps) { if (app.getEndpointId() == endpointId) { - Log.d(TAG, "Sending attribute read request for endpointId " + endpointId); - return ContentAppAgentService.sendAttributeReadRequest( - context, app.getAppName(), clusterId, attributeId); + return app; } } - return ""; + return null; } } diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/ContentApp.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/ContentApp.java index 7dd5f112cc8855..6b5856870eba58 100644 --- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/ContentApp.java +++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/ContentApp.java @@ -9,6 +9,7 @@ public class ContentApp { public static final int INVALID_ENDPOINTID = -1; + private final String version; private String appName; private String vendorName; private int vendorId; @@ -17,16 +18,27 @@ public class ContentApp { // initially set to an invalid value. private int endpoint = INVALID_ENDPOINTID; + public ContentApp( + String appName, String vendorName, int vendorId, int productId, String version) { + this.vendorName = vendorName; + this.appName = appName; + this.vendorId = vendorId; + this.productId = productId; + this.version = version; + } + public ContentApp( String appName, String vendorName, int vendorId, int productId, + String version, Set supportedClusters) { this.vendorName = vendorName; this.appName = appName; this.vendorId = vendorId; this.productId = productId; + this.version = version; this.supportedClusters = supportedClusters; } @@ -58,6 +70,10 @@ public Set getSupportedClusters() { return Collections.unmodifiableSet(supportedClusters); } + public String getVersion() { + return version; + } + public void setSupportedClusters(List supportedClusters) { this.supportedClusters = new HashSet<>(supportedClusters); } 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 index ebdc10acd3a557..ba654f3a7227d1 100644 --- 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 @@ -5,9 +5,11 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; import android.util.Log; import com.matter.tv.app.api.MatterIntentConstants; @@ -90,11 +92,18 @@ private void handlePackageAdded(Context context, String pkg) { if (appInfo.metaData == null) { return; } + PackageInfo packageInfo = pm.getPackageInfo(pkg, PackageManager.GET_META_DATA); int resId = appInfo.metaData.getInt(CLUSTERS_RESOURCE_METADATA_KEY, 0); int vendorId = appInfo.metaData.getInt(MATTER_VENDOR_ID_METADATA_KEY, -1); int productId = appInfo.metaData.getInt(MATTER_PRODUCT_ID_METADATA_KEY, -1); String vendorName = appInfo.metaData.getString(MATTER_VENDOR_NAME_METADATA_KEY, ""); + String version; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + version = String.valueOf(packageInfo.getLongVersionCode()); + } else { + version = String.valueOf(packageInfo.versionName); + } if (vendorId == -1 || productId == -1) { return; @@ -109,7 +118,8 @@ private void handlePackageAdded(Context context, String pkg) { supportedClusters = new HashSet<>(); } - ContentApp app = new ContentApp(pkg, vendorName, vendorId, productId, supportedClusters); + ContentApp app = + new ContentApp(pkg, vendorName, vendorId, productId, version, supportedClusters); applications.put(pkg, app); Intent in = new Intent(DISCOVERY_APPAGENT_ACTION_ADD); diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/AppPlatformService.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/AppPlatformService.java index a841fece436bff..d5d634015366c5 100644 --- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/AppPlatformService.java +++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/AppPlatformService.java @@ -22,13 +22,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import com.matter.tv.server.handlers.ContentAppEndpointManagerImpl; import com.matter.tv.server.model.ContentApp; import com.matter.tv.server.receivers.ContentAppDiscoveryService; import com.matter.tv.server.tvapp.AppPlatform; +import com.matter.tv.server.utils.EndpointsDataStore; import java.util.HashMap; import java.util.Map; @@ -42,10 +42,9 @@ public class AppPlatformService { private static final String TAG = "AppPlatformService"; - private static final String MATTER_APPPLATFORM_ENDPOINTS = "matter.appplatform.endpoints"; private AppPlatform mAppPlatform; private BroadcastReceiver mBroadcastReceiver; - private SharedPreferences discoveredEndpoints; + private EndpointsDataStore endpointsDataStore; private AppPlatformService() {} @@ -66,27 +65,48 @@ public static AppPlatformService get() { public void init(@NonNull Context context) { this.context = context; - discoveredEndpoints = - context.getSharedPreferences(MATTER_APPPLATFORM_ENDPOINTS, Context.MODE_PRIVATE); - mAppPlatform = new AppPlatform(new ContentAppEndpointManagerImpl(context)); ContentAppDiscoveryService.getReceiverInstance().registerSelf(context.getApplicationContext()); - Map previouslyPersistedEndpoints = new HashMap(); - previouslyPersistedEndpoints.putAll((Map) discoveredEndpoints.getAll()); - for (ContentApp app : - ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps().values()) { - addContentApp(app); - previouslyPersistedEndpoints.remove(app.getAppName()); + mAppPlatform = new AppPlatform(new ContentAppEndpointManagerImpl(context)); + endpointsDataStore = EndpointsDataStore.getInstance(context); + initializeContentAppEndpoints(); + registerContentAppUpdatesReceiver(); + } + + private void initializeContentAppEndpoints() { + + // Read the metadada of previously discovered endpoints. + Map previouslyPersistedEndpoints = + new HashMap((Map) endpointsDataStore.getAllPersistedContentApps()); + + // Get the list of currently discovered content apps. + Map discoveredContentApps = + new HashMap<>(ContentAppDiscoveryService.getReceiverInstance().getDiscoveredContentApps()); + + // Iterate through the previously discovered endpoints + for (ContentApp persistedContentApp : previouslyPersistedEndpoints.values()) { + ContentApp discoveredContentApp = + discoveredContentApps.remove(persistedContentApp.getAppName()); + if (discoveredContentApp != null) { + // If the content app for the persisted endpoint is present in currently discovered list, + // register endpoint with updated metadata and update the metadata in the persisted + // endpoints. + discoveredContentApp.setEndpointId(persistedContentApp.getEndpointId()); + addContentApp(discoveredContentApp); + } else { + // If the content app for the persisted endpoint is not present register the endpoint with + // previously persisted data. + addContentApp(persistedContentApp); + } } - SharedPreferences.Editor editor = discoveredEndpoints.edit(); - for (Map.Entry appEntry : previouslyPersistedEndpoints.entrySet()) { - editor.remove(appEntry.getKey()); - // TODO : Figure out how to cleanup ACLs + + // For newly discovered content apps register new endpoints and persist the metadata for future + // use + for (ContentApp discoveredContentApp : discoveredContentApps.values()) { + addContentApp(discoveredContentApp); } - editor.apply(); - registerReceiver(); } - private void registerReceiver() { + private void registerContentAppUpdatesReceiver() { mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -100,14 +120,24 @@ public void onReceive(Context context, Intent intent) { ContentAppDiscoveryService.getReceiverInstance() .getDiscoveredContentApps() .get(packageName); - addContentApp(app); - } else if (action.equals(ContentAppDiscoveryService.DISCOVERY_APPAGENT_ACTION_REMOVE)) { - int endpointId = - intent.getIntExtra( - ContentAppDiscoveryService.DISCOVERY_APPAGENT_EXTRA_ENDPOINTID, -1); - if (endpointId != -1) { - removeContentApp(endpointId, packageName); + // if this app was already added as endpoint remove and add so that the app metadata + // stored by the content app platform is also updated + ContentApp persistedContentApp = + endpointsDataStore.getAllPersistedContentApps().get(app.getAppName()); + if (persistedContentApp != null) { + mAppPlatform.removeContentApp(persistedContentApp.getEndpointId()); + app.setEndpointId(persistedContentApp.getEndpointId()); } + addContentApp(app); + // } else if + // (action.equals(ContentAppDiscoveryService.DISCOVERY_APPAGENT_ACTION_REMOVE)) { + // int endpointId = + // intent.getIntExtra( + // + // ContentAppDiscoveryService.DISCOVERY_APPAGENT_EXTRA_ENDPOINTID, -1); + // if (endpointId != -1) { + // removeContentApp(endpointId, packageName); + // } } } }; @@ -125,7 +155,7 @@ public void setActivity(Activity activity) { public void addContentApp(ContentApp app) { int retEndpointId = -1; - int desiredEndpointId = discoveredEndpoints.getInt(app.getAppName(), -1); + int desiredEndpointId = app.getEndpointId(); if (desiredEndpointId > 0) { retEndpointId = mAppPlatform.addContentAppAtEndpoint( @@ -133,7 +163,7 @@ public void addContentApp(ContentApp app) { app.getVendorId(), app.getAppName(), app.getProductId(), - "1.0", + app.getVersion(), desiredEndpointId, new ContentAppEndpointManagerImpl(context)); } else { @@ -143,22 +173,14 @@ public void addContentApp(ContentApp app) { app.getVendorId(), app.getAppName(), app.getProductId(), - "1.0", + app.getVersion(), new ContentAppEndpointManagerImpl(context)); } if (retEndpointId > 0) { app.setEndpointId(retEndpointId); - discoveredEndpoints.edit().putInt(app.getAppName(), app.getEndpointId()).apply(); + endpointsDataStore.persistContentAppEndpoint(app); } else { Log.e(TAG, "Could not add content app as endpoint. App Name " + app.getAppName()); } } - - public int removeContentApp(int endpointID, String appName) { - int retEndpointId = mAppPlatform.removeContentApp(endpointID); - if (endpointID == retEndpointId) { - discoveredEndpoints.edit().remove(appName).apply(); - } - return retEndpointId; - } } 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 index 6fdfa6a0b3ff5e..a9a5e2af7d6b47 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -24,6 +25,18 @@ public class ContentAppAgentService extends Service { 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 final IBinder appAgentBinder = @@ -100,13 +113,7 @@ public static String sendCommand( MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT, getPendingIntentForResponse(context, packageName, messageId)); context.sendBroadcast(in); - responseRegistry.waitForMessage(messageId, 10, TimeUnit.SECONDS); - String response = responseRegistry.readAndRemoveResponse(messageId); - if (response == null) { - response = ""; - } - Log.d(TAG, "Response " + response + " being returned for message " + messageId); - return response; + return getResponse(messageId, COMMAND_TIMEOUT); } public static String sendAttributeReadRequest( @@ -127,10 +134,61 @@ public static String sendAttributeReadRequest( MatterIntentConstants.EXTRA_DIRECTIVE_RESPONSE_PENDING_INTENT, getPendingIntentForResponse(context, packageName, messageId)); context.sendBroadcast(in); - responseRegistry.waitForMessage(messageId, 10, TimeUnit.SECONDS); - String response = responseRegistry.readAndRemoveResponse(messageId); - if (response == null) { - response = ""; + 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; diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ResponseRegistry.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ResponseRegistry.java index 9c1ebc0c0b6031..1056cf493906af 100644 --- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ResponseRegistry.java +++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/ResponseRegistry.java @@ -9,6 +9,13 @@ public class ResponseRegistry { + public enum WaitState { + TIMED_OUT, + INTERRUPTED, + SUCCESS, + INVALID_COUNTER + } + private static final String TAG = "ResponseRegistry"; private AtomicInteger messageCounter = new AtomicInteger(); @@ -27,18 +34,21 @@ public int getNextMessageCounter() { return counter; } - public void waitForMessage(int counter, long timeout, TimeUnit unit) { + public WaitState waitForMessage(int counter, long timeout, TimeUnit unit) { CountDownLatch latch = latches.get(counter); if (latch == null) { - return; + return WaitState.INVALID_COUNTER; } try { if (!latch.await(timeout, unit)) { Log.i(TAG, "Timed out while waiting for response for message " + counter); + return WaitState.TIMED_OUT; } } catch (InterruptedException e) { Log.i(TAG, "Interrupted while waiting for response for message " + counter); + return WaitState.INTERRUPTED; } + return WaitState.SUCCESS; } public void receivedMessageResponse(int counter, String response) { diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/EndpointsDataStore.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/EndpointsDataStore.java new file mode 100644 index 00000000000000..f587905fb09145 --- /dev/null +++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/EndpointsDataStore.java @@ -0,0 +1,119 @@ +package com.matter.tv.server.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.JsonReader; +import android.util.JsonWriter; +import com.matter.tv.server.model.ContentApp; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class EndpointsDataStore { + + private static final String MATTER_APPPLATFORM_ENDPOINTS_PREF = "matter.appplatform.endpoints"; + private static final String KEY_NAME = "N"; + private static final String KEY_VENDORID = "VID"; + private static final String KEY_VENDORNAME = "VN"; + private static final String KEY_PRODUCTID = "PID"; + private static final String KEY_VERSION = "VER"; + private static final String KEY_ENDPOINTID = "EPID"; + private static EndpointsDataStore instance; + private final SharedPreferences discoveredEndpoints; + Map persistedContentApps = new HashMap<>(); + + private EndpointsDataStore(Context context) { + discoveredEndpoints = + context.getSharedPreferences(MATTER_APPPLATFORM_ENDPOINTS_PREF, Context.MODE_PRIVATE); + // Take care of versioning when required + Map appMetadata = (Map) discoveredEndpoints.getAll(); + for (Map.Entry contentAppEntry : appMetadata.entrySet()) { + persistedContentApps.put( + contentAppEntry.getKey(), + deserializeContentApp(contentAppEntry.getKey(), contentAppEntry.getValue())); + } + } + + public static synchronized EndpointsDataStore getInstance(Context context) { + if (instance == null) { + instance = new EndpointsDataStore(context); + } + return instance; + } + + public Map getAllPersistedContentApps() { + return Collections.unmodifiableMap(persistedContentApps); + } + + public void persistContentAppEndpoint(ContentApp app) { + persistedContentApps.put(app.getAppName(), app); + discoveredEndpoints.edit().putString(app.getAppName(), serializeContentApp(app)).apply(); + } + + private String serializeContentApp(ContentApp app) { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + try { + jsonWriter + .beginObject() + .name(KEY_VENDORID) + .value(app.getVendorId()) + .name(KEY_VENDORNAME) + .value(app.getVendorName()) + .name(KEY_PRODUCTID) + .value(app.getProductId()) + .name(KEY_VERSION) + .value(app.getVersion()) + .name(KEY_ENDPOINTID) + .value(app.getEndpointId()) + .endObject(); + jsonWriter.flush(); + jsonWriter.close(); + } catch (IOException e) { + // Cannot happen + } + return stringWriter.toString(); + } + + private ContentApp deserializeContentApp(String appName, String appMetadata) { + JsonReader jsonReader = new JsonReader(new StringReader(appMetadata)); + ContentApp app = null; + try { + jsonReader.beginObject(); + String version = ""; + String vendorName = ""; + int vendorId = 0; + int productId = 0; + int endpoint = ContentApp.INVALID_ENDPOINTID; + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case KEY_VENDORID: + vendorId = jsonReader.nextInt(); + break; + case KEY_VENDORNAME: + vendorName = jsonReader.nextString(); + break; + case KEY_PRODUCTID: + productId = jsonReader.nextInt(); + break; + case KEY_VERSION: + version = jsonReader.nextString(); + break; + case KEY_ENDPOINTID: + endpoint = jsonReader.nextInt(); + break; + } + } + app = new ContentApp(appName, vendorName, vendorId, productId, version); + jsonReader.endObject(); + jsonReader.close(); + } catch (IOException e) { + // Cannot happen + } + return app; + } +} 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 index 71ccc02f1799b2..1a04c5294d2f30 100644 --- 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 @@ -18,7 +18,9 @@ public class ResourceUtils { private static final String KEY_CLUSTERS = "clusters"; private static final String KEY_CLUSTER_ID = "identifier"; private static final String KEY_FEATURES = "features"; + private static final String KEY_FEATURE_FLAGS = "featureFlags"; private static final String KEY_OPTIONAL_COMMANDS = "optionalCommands"; + private static final String KEY_OPTIONAL_ATTRIBUTES = "optionalAttributes"; private ResourceUtils() {} @@ -58,14 +60,8 @@ public Set getSupportedClusters(final Resources resources, fin String name = reader.nextName(); if (name.equals(KEY_CLUSTER_ID)) { cluster.clusterIdentifier = reader.nextInt(); - } else if (name.equals(KEY_FEATURES)) { - List features = new ArrayList<>(); - reader.beginArray(); - while (reader.hasNext()) { - features.add(reader.nextString()); - } - reader.endArray(); - cluster.features = features.toArray(new String[features.size()]); + } else if (name.equals(KEY_FEATURE_FLAGS)) { + cluster.features = reader.nextInt(); } else if (name.equals(KEY_OPTIONAL_COMMANDS)) { List commands = new ArrayList<>(); reader.beginArray(); @@ -79,6 +75,19 @@ public Set getSupportedClusters(final Resources resources, fin commandIds[i++] = command; } cluster.optionalCommandIdentifiers = commandIds; + } else if (name.equals(KEY_OPTIONAL_ATTRIBUTES)) { + List attributes = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + attributes.add(reader.nextInt()); + } + reader.endArray(); + int[] attributeIds = new int[attributes.size()]; + int i = 0; + for (Integer command : attributes) { + attributeIds[i++] = command; + } + cluster.optionalAttributesIdentifiers = attributeIds; } else { reader.skipValue(); } diff --git a/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.cpp b/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.cpp index ec9b24598acd3d..5bb39495b0a703 100644 --- a/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.cpp +++ b/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.cpp @@ -16,12 +16,16 @@ */ #include "TargetNavigatorManager.h" +#include using namespace std; using namespace chip::app; using namespace chip::app::Clusters::TargetNavigator; +using ContentAppAttributeDelegate = chip::AppPlatform::ContentAppAttributeDelegate; -TargetNavigatorManager::TargetNavigatorManager(std::list targets, uint8_t currentTarget) +TargetNavigatorManager::TargetNavigatorManager(ContentAppAttributeDelegate * attributeDelegate, std::list targets, + uint8_t currentTarget) : + mAttributeDelegate(attributeDelegate) { mTargets = targets; mCurrentTarget = currentTarget; @@ -29,6 +33,54 @@ TargetNavigatorManager::TargetNavigatorManager(std::list targets, u CHIP_ERROR TargetNavigatorManager::HandleGetTargetList(AttributeValueEncoder & aEncoder) { + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleNavigateTarget"); + + if (mAttributeDelegate != nullptr) + { + chip::app::ConcreteReadAttributePath aPath(mEndpointId, chip::app::Clusters::TargetNavigator::Id, + chip::app::Clusters::TargetNavigator::Attributes::TargetList::Id); + const char * resStr = mAttributeDelegate->Read(aPath); + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleNavigateTarget response %s", resStr); + + if (resStr != nullptr && *resStr != 0) + { + Json::Reader reader; + Json::Value value; + if (reader.parse(resStr, value)) + { + std::string attrId = to_string(chip::app::Clusters::TargetNavigator::Attributes::TargetList::Id); + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleNavigateTarget response parsing done. reading attr %s", + attrId.c_str()); + if (value[attrId].isArray()) + { + return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { + int i = 0; + std::string targetId = to_string( + static_cast(chip::app::Clusters::TargetNavigator::Structs::TargetInfo::Fields::kIdentifier)); + std::string targetName = to_string( + static_cast(chip::app::Clusters::TargetNavigator::Structs::TargetInfo::Fields::kName)); + for (Json::Value & entry : value[attrId]) + { + if (!entry[targetId].isUInt() || !entry[targetName].isString() || entry[targetId].asUInt() > 255) + { + // invalid target ID. Ignore. + ChipLogError(Zcl, "TargetNavigatorManager::HandleNavigateTarget invalid target ignored"); + i++; + continue; + } + Structs::TargetInfo::Type outputInfo; + outputInfo.identifier = static_cast(entry[targetId].asUInt()); + outputInfo.name = CharSpan::fromCharString(entry[targetName].asCString()); + ReturnErrorOnFailure(encoder.Encode(outputInfo)); + i++; + } + return CHIP_NO_ERROR; + }); + } + } + } + } + // NOTE: the ids for each target start at 1 so that we can reserve 0 as "no current target" return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { int i = 0; @@ -46,6 +98,32 @@ CHIP_ERROR TargetNavigatorManager::HandleGetTargetList(AttributeValueEncoder & a uint8_t TargetNavigatorManager::HandleGetCurrentTarget() { + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleGetCurrentTarget"); + + if (mAttributeDelegate != nullptr) + { + chip::app::ConcreteReadAttributePath aPath(mEndpointId, chip::app::Clusters::TargetNavigator::Id, + chip::app::Clusters::TargetNavigator::Attributes::TargetList::Id); + const char * resStr = mAttributeDelegate->Read(aPath); + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleGetCurrentTarget response %s", resStr); + + if (resStr != nullptr && *resStr != 0) + { + Json::Reader reader; + Json::Value value; + if (reader.parse(resStr, value)) + { + std::string attrId = to_string(chip::app::Clusters::TargetNavigator::Attributes::CurrentTarget::Id); + ChipLogProgress(Zcl, "TargetNavigatorManager::HandleGetCurrentTarget response parsing done. reading attr %s", + attrId.c_str()); + if (value[attrId].isUInt() && value[attrId].asUInt() < 256) + { + return static_cast(value[attrId].asUInt()); + } + } + } + } + return mCurrentTarget; } diff --git a/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.h b/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.h index f4cb0b372bc7e3..8e0825c24c93ce 100644 --- a/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.h +++ b/examples/tv-app/android/include/target-navigator/TargetNavigatorManager.h @@ -17,29 +17,38 @@ #pragma once +#include "../../java/ContentAppAttributeDelegate.h" #include using chip::CharSpan; +using chip::EndpointId; using chip::app::AttributeValueEncoder; using chip::app::CommandResponseHelper; -using TargetNavigatorDelegate = chip::app::Clusters::TargetNavigator::Delegate; -using TargetInfoType = chip::app::Clusters::TargetNavigator::Structs::TargetInfo::Type; -using NavigateTargetResponseType = chip::app::Clusters::TargetNavigator::Commands::NavigateTargetResponse::Type; +using TargetNavigatorDelegate = chip::app::Clusters::TargetNavigator::Delegate; +using TargetInfoType = chip::app::Clusters::TargetNavigator::Structs::TargetInfo::Type; +using NavigateTargetResponseType = chip::app::Clusters::TargetNavigator::Commands::NavigateTargetResponse::Type; +using ContentAppAttributeDelegate = chip::AppPlatform::ContentAppAttributeDelegate; class TargetNavigatorManager : public TargetNavigatorDelegate { public: - TargetNavigatorManager() : TargetNavigatorManager({ "exampleName", "exampleName" }, kNoCurrentTarget){}; - TargetNavigatorManager(std::list targets, uint8_t currentTarget); + TargetNavigatorManager() : TargetNavigatorManager(nullptr, { "exampleName", "exampleName" }, kNoCurrentTarget){}; + TargetNavigatorManager(ContentAppAttributeDelegate * attributeDelegate, std::list targets, uint8_t currentTarget); CHIP_ERROR HandleGetTargetList(AttributeValueEncoder & aEncoder) override; uint8_t HandleGetCurrentTarget() override; void HandleNavigateTarget(CommandResponseHelper & responser, const uint64_t & target, const CharSpan & data) override; + void SetEndpointId(EndpointId epId) { mEndpointId = epId; }; + protected: // NOTE: the ids for each target start at 1 so that we can reserve 0 as "no current target" static const uint8_t kNoCurrentTarget = 0; std::list mTargets; uint8_t mCurrentTarget; + + EndpointId mEndpointId; + + ContentAppAttributeDelegate * mAttributeDelegate; }; diff --git a/examples/tv-app/android/java/AppImpl.h b/examples/tv-app/android/java/AppImpl.h index e86a750cc533c4..70ea36f452ac30 100644 --- a/examples/tv-app/android/java/AppImpl.h +++ b/examples/tv-app/android/java/AppImpl.h @@ -91,7 +91,8 @@ class DLL_EXPORT ContentAppImpl : public ContentApp mAccountLoginDelegate(setupPIN), mContentLauncherDelegate(attributeDelegate, { "image/*", "video/*" }, to_underlying(SupportedStreamingProtocol::kDash) | to_underlying(SupportedStreamingProtocol::kHls)), - mMediaPlaybackDelegate(attributeDelegate), mTargetNavigatorDelegate({ "home", "search", "info", "guide", "menu" }, 0){}; + mMediaPlaybackDelegate(attributeDelegate), + mTargetNavigatorDelegate(attributeDelegate, { "home", "search", "info", "guide", "menu" }, 0){}; virtual ~ContentAppImpl() {} AccountLoginDelegate * GetAccountLoginDelegate() override { return &mAccountLoginDelegate; }; @@ -109,7 +110,11 @@ class DLL_EXPORT ContentAppImpl : public ContentApp mMediaPlaybackDelegate.SetEndpointId(GetEndpointId()); return &mMediaPlaybackDelegate; }; - TargetNavigatorDelegate * GetTargetNavigatorDelegate() override { return &mTargetNavigatorDelegate; }; + TargetNavigatorDelegate * GetTargetNavigatorDelegate() override + { + mTargetNavigatorDelegate.SetEndpointId(GetEndpointId()); + return &mTargetNavigatorDelegate; + }; protected: ApplicationBasicManager mApplicationBasicDelegate; diff --git a/examples/tv-app/android/java/ContentAppCommandDelegate.cpp b/examples/tv-app/android/java/ContentAppCommandDelegate.cpp index c5b2e11c97e485..0b406ab4237794 100644 --- a/examples/tv-app/android/java/ContentAppCommandDelegate.cpp +++ b/examples/tv-app/android/java/ContentAppCommandDelegate.cpp @@ -34,8 +34,13 @@ namespace chip { namespace AppPlatform { -using CommandHandlerInterface = chip::app::CommandHandlerInterface; -using LaunchResponseType = chip::app::Clusters::ContentLauncher::Commands::LaunchResponse::Type; +using CommandHandlerInterface = chip::app::CommandHandlerInterface; +using LaunchResponseType = chip::app::Clusters::ContentLauncher::Commands::LaunchResponse::Type; +using PlaybackResponseType = chip::app::Clusters::MediaPlayback::Commands::PlaybackResponse::Type; +using NavigateTargetResponseType = chip::app::Clusters::TargetNavigator::Commands::NavigateTargetResponse::Type; + +const std::string FAILURE_KEY = "PlatformError"; +const std::string FAILURE_STATUS_KEY = "Status"; void ContentAppCommandDelegate::InvokeCommand(CommandHandlerInterface::HandlerContext & handlerContext) { @@ -49,8 +54,9 @@ void ContentAppCommandDelegate::InvokeCommand(CommandHandlerInterface::HandlerCo err = TlvToJson(readerForJson, json); if (err != CHIP_NO_ERROR) { - // TODO : Add an interface to let the apps know a message came but there was a serialization error. - handlerContext.SetCommandNotHandled(); + handlerContext.SetCommandHandled(); + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, + Protocols::InteractionModel::Status::InvalidCommand); return; } @@ -85,11 +91,25 @@ void ContentAppCommandDelegate::InvokeCommand(CommandHandlerInterface::HandlerCo void ContentAppCommandDelegate::FormatResponseData(CommandHandlerInterface::HandlerContext & handlerContext, const char * response) { + handlerContext.SetCommandHandled(); Json::Reader reader; Json::Value value; if (!reader.parse(response, value)) { - handlerContext.SetCommandNotHandled(); + return; + } + + // handle errors from platform-app + if (!value[FAILURE_KEY].empty()) + { + value = value[FAILURE_KEY]; + if (!value[FAILURE_STATUS_KEY].empty() && value[FAILURE_STATUS_KEY].isUInt()) + { + handlerContext.mCommandHandler.AddStatus( + handlerContext.mRequestPath, static_cast(value[FAILURE_STATUS_KEY].asUInt())); + return; + } + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Failure); return; } @@ -97,28 +117,75 @@ void ContentAppCommandDelegate::FormatResponseData(CommandHandlerInterface::Hand { case app::Clusters::ContentLauncher::Id: { LaunchResponseType launchResponse; - if (value["0"].empty()) + std::string statusFieldId = + std::to_string(to_underlying(app::Clusters::ContentLauncher::Commands::LaunchResponse::Fields::kStatus)); + if (value[statusFieldId].empty()) { - launchResponse.status = chip::app::Clusters::ContentLauncher::ContentLaunchStatusEnum::kAuthFailed; + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Failure); + return; } else { - launchResponse.status = static_cast(value["0"].asInt()); - if (!value["1"].empty()) + launchResponse.status = + static_cast(value[statusFieldId].asInt()); + std::string dataFieldId = + std::to_string(to_underlying(app::Clusters::ContentLauncher::Commands::LaunchResponse::Fields::kData)); + if (!value[dataFieldId].empty()) { - launchResponse.data = chip::MakeOptional(CharSpan::fromCharString(value["1"].asCString())); + launchResponse.data = chip::MakeOptional(CharSpan::fromCharString(value[dataFieldId].asCString())); } } handlerContext.mCommandHandler.AddResponseData(handlerContext.mRequestPath, launchResponse); - handlerContext.SetCommandHandled(); break; } - // case app::Clusters::TargetNavigator::Id: - // break; + case app::Clusters::TargetNavigator::Id: { + NavigateTargetResponseType navigateTargetResponse; + std::string statusFieldId = + std::to_string(to_underlying(app::Clusters::TargetNavigator::Commands::NavigateTargetResponse::Fields::kStatus)); + if (value[statusFieldId].empty()) + { + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Failure); + return; + } + else + { + navigateTargetResponse.status = + static_cast(value[statusFieldId].asInt()); + std::string dataFieldId = + std::to_string(to_underlying(app::Clusters::TargetNavigator::Commands::NavigateTargetResponse::Fields::kData)); + if (!value[dataFieldId].empty()) + { + navigateTargetResponse.data = chip::MakeOptional(CharSpan::fromCharString(value[dataFieldId].asCString())); + } + } + handlerContext.mCommandHandler.AddResponseData(handlerContext.mRequestPath, navigateTargetResponse); + break; + } - // case app::Clusters::MediaPlayback::Id: - // break; + case app::Clusters::MediaPlayback::Id: { + PlaybackResponseType playbackResponse; + std::string statusFieldId = + std::to_string(to_underlying(app::Clusters::MediaPlayback::Commands::PlaybackResponse::Fields::kStatus)); + if (value[statusFieldId].empty()) + { + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Failure); + return; + } + else + { + playbackResponse.status = + static_cast(value[statusFieldId].asInt()); + std::string dataFieldId = + std::to_string(to_underlying(app::Clusters::MediaPlayback::Commands::PlaybackResponse::Fields::kData)); + if (!value[dataFieldId].empty()) + { + playbackResponse.data = chip::MakeOptional(CharSpan::fromCharString(value[dataFieldId].asCString())); + } + } + handlerContext.mCommandHandler.AddResponseData(handlerContext.mRequestPath, playbackResponse); + break; + } // case app::Clusters::AccountLogin::Id: // break;