From 01a26df688a03dba3309cc998a50c3071b2e5b78 Mon Sep 17 00:00:00 2001 From: Sharad Binjola Date: Fri, 5 Apr 2024 11:43:18 -0700 Subject: [PATCH] android/tv-casting-app: Implemented commands and attributes using CHIPInteractionClient.jar --- examples/tv-casting-app/APIs.md | 155 +++++++++++++++++- .../com/chip/casting/app/MainActivity.java | 14 ++ .../casting/util/GlobalCastingConstants.java | 2 +- .../casting/ActionSelectorFragment.java | 24 +++ ...ationBasicReadVendorIDExampleFragment.java | 129 +++++++++++++++ ...ntentLauncherLaunchURLExampleFragment.java | 80 ++++++--- .../casting/EndpointSelectorExample.java | 34 ++++ ...ubscribeToCurrentStateExampleFragment.java | 140 ++++++++++++++++ .../jni/com/matter/casting/core/Endpoint.java | 6 + .../matter/casting/core/MatterEndpoint.java | 60 +++++++ .../main/jni/cpp/core/MatterEndpoint-JNI.cpp | 25 ++- .../main/jni/cpp/core/MatterEndpoint-JNI.h | 12 ++ .../main/jni/cpp/support/Converters-JNI.cpp | 24 +++ .../src/main/jni/cpp/support/Converters-JNI.h | 2 + .../main/jni/cpp/support/MatterCallback-JNI.h | 3 +- .../fragment_matter_action_selector.xml | 18 ++ ...atter_application_basic_read_vendor_id.xml | 48 ++++++ ...ent_matter_content_launcher_launch_url.xml | 6 +- ...media_playback_subscribe_current_state.xml | 57 +++++++ .../App/app/src/main/res/values/strings.xml | 8 + 20 files changed, 808 insertions(+), 39 deletions(-) create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ApplicationBasicReadVendorIDExampleFragment.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/EndpointSelectorExample.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/MediaPlaybackSubscribeToCurrentStateExampleFragment.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_application_basic_read_vendor_id.xml create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_media_playback_subscribe_current_state.xml diff --git a/examples/tv-casting-app/APIs.md b/examples/tv-casting-app/APIs.md index 12f5fdf30ebc1b..7a78b07322f978 100644 --- a/examples/tv-casting-app/APIs.md +++ b/examples/tv-casting-app/APIs.md @@ -830,7 +830,7 @@ func connect(selectedCastingPlayer: MCCastingPlayer?) { ### Select an Endpoint on the Casting Player _{Complete Endpoint selection examples: [Linux](linux/simple-app-helper.cpp) | -[Android](android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java) +[Android](android/App/app/src/main/java/com/matter/casting/EndpointSelectorExample.java) | [iOS](darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift)}_ @@ -905,18 +905,25 @@ Once the Casting Client has selected an `Endpoint`, it is ready to [issue commands](#issuing-commands) to it, [read](#read-operations) current playback state, and [subscribe](#subscriptions) to playback events. -On Linux refer to the following platform specific files: +Refer to the following platform specific files, to find the list of clusters, +commands and attributes, with their request/response types available for use +with the Matter TV Casting library. + +For Linux, refer to the following files: -1. For a list of clusters, commands and attributes supported by the Matter TV - Casting library: +1. For a list of supported clusters, commands and attributes: [tv-casting-common/clusters/Clusters.h](tv-casting-common/clusters/Clusters.h) 2. For the IDs and request / response types to use with these APIs: [/zzz_generated/app-common/app-common/zap-generated/cluster-objects.h](/zzz_generated/app-common/app-common/zap-generated/cluster-objects.h) -On iOS refer to the following platform specific files: +For Android, refer to the following files: + +1. For a list of supported clusters, commands and attributes: + [/src/controller/java/generated/java/chip/devicecontroller/ChipClusters.java](/src/controller/java/generated/java/chip/devicecontroller/ChipClusters.java) -1. For a list of clusters, commands and attributes supported by the Matter TV - Casting library: +On iOS, refer to the following files: + +1. For a list of supported clusters, commands and attribute: [darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCClusterObjects.h](darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCClusterObjects.h) 2. For the IDs and request / response types to use with the commands: [darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCCommandObjects.h](darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCCommandObjects.h) @@ -929,6 +936,8 @@ On iOS refer to the following platform specific files: ### Issuing Commands _{Complete Command invocation examples: [Linux](linux/simple-app-helper.cpp) | +[Android](android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java) +| [iOS](darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift)}_ The Casting Client can get a reference to an `Endpoint` on a `CastingPlayer`, @@ -975,6 +984,51 @@ void InvokeContentLauncherLaunchURL(matter::casting::memory::Strong data) { + Log.d(TAG, "LaunchURL success. Status: " + status + ", Data: " + data); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView launcherResult = getView().findViewById(R.id.launcherResult); + launcherResult.setText( + "LaunchURL result\nStatus: " + status + ", Data: " + data); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "LaunchURL failure " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView launcherResult = getView().findViewById(R.id.launcherResult); + launcherResult.setText("LaunchURL result\nError: " + error); + }); + } + }, + contentUrl, + Optional.of(contentDisplayString), + Optional.empty()); +``` + On iOS, given an `MCEndpoint` endpoint, it can send a `LaunchURL` command (part of the Content Launcher cluster) by calling the `invoke` API on a `MCContentLauncherClusterLaunchURLCommand` @@ -1033,6 +1087,8 @@ timedInvokeTimeoutMs: 5000) // time out after 5000ms ### Read Operations _{Complete Attribute Read examples: [Linux](linux/simple-app-helper.cpp) | +[Android](android/App/app/src/main/java/com/matter/casting/ApplicationBasicReadVendorIDExampleFragment.java) +| [iOS](darwin/TvCasting/TvCasting/MCApplicationBasicReadVendorIDExampleViewModel.swift)}_ The `CastingClient` may read an Attribute from the `Endpoint` on the @@ -1080,6 +1136,45 @@ void ReadApplicationBasicVendorID(matter::casting::memory::Strong { + TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult); + vendorIdResult.setText( + "Read VendorID result\nValue: " + value ); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "ReadVendorID failure " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult); + vendorIdResult.setText("Read VendorID result\nError: " + error); + }); + } +}); +``` + On iOS, given a `MCEndpoint`, the `VendorID` can be read similarly, by calling the `read` API on the `MCApplicationBasicClusterVendorIDAttribute` @@ -1138,6 +1233,9 @@ vendorIDAttribute!.read(nil) { context, before, after, err in ### Subscriptions _{Complete Attribute subscription examples: [Linux](linux/simple-app-helper.cpp) +| +[Android](android/App/app/src/main/java/com/matter/casting/MediaPlaybackSubscribeToCurrentStateExampleFragment.java) +| |[iOS](darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift)}_ A Casting Client may subscribe to an attribute on an `Endpoint` of the @@ -1187,6 +1285,49 @@ void SubscribeToMediaPlaybackCurrentState(matter::casting::memory::Strong { + TextView currentStateResult = getView().findViewById(R.id.currentStateResult); + currentStateResult.setText( + "Current State result\nValue: " + value ); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "Read failure on subscription: " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView currentStateResult = getView().findViewById(R.id.currentStateResult); + currentStateResult.setText("Current State result\nError: " + error); + }); + } + }, 0, 1); +``` + On iOS, given a `MCEndpoint`, `CurrentState` can be subscribed to by calling the `subscribe` API on the it can subscribe to the `CurrentState` (part of the Media Playback Basic cluster) by calling the `Subscribe` API on the diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java index 6e54ff61b01c7e..cb04518f5c6259 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java @@ -11,10 +11,12 @@ import com.chip.casting.TvCastingApp; import com.chip.casting.util.GlobalCastingConstants; import com.matter.casting.ActionSelectorFragment; +import com.matter.casting.ApplicationBasicReadVendorIDExampleFragment; import com.matter.casting.ConnectionExampleFragment; import com.matter.casting.ContentLauncherLaunchURLExampleFragment; import com.matter.casting.DiscoveryExampleFragment; import com.matter.casting.InitializationExample; +import com.matter.casting.MediaPlaybackSubscribeToCurrentStateExampleFragment; import com.matter.casting.PreferencesConfigurationManager; import com.matter.casting.core.CastingPlayer; import java.util.Random; @@ -85,6 +87,18 @@ public void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCasting showFragment(ContentLauncherLaunchURLExampleFragment.newInstance(selectedCastingPlayer)); } + @Override + public void handleApplicationBasicReadVendorIDSelected(CastingPlayer selectedCastingPlayer) { + showFragment(ApplicationBasicReadVendorIDExampleFragment.newInstance(selectedCastingPlayer)); + } + + @Override + public void handleMediaPlaybackSubscribeToCurrentStateSelected( + CastingPlayer selectedCastingPlayer) { + showFragment( + MediaPlaybackSubscribeToCurrentStateExampleFragment.newInstance(selectedCastingPlayer)); + } + @Override public void handleContentLauncherSelected() { showFragment(ContentLauncherFragment.newInstance(tvCastingApp)); diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java index d063cc7e6f78c5..754d3bbe5ff4fd 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java @@ -6,5 +6,5 @@ public class GlobalCastingConstants { public static final int SetupPasscode = 20202021; public static final int Discriminator = 0xF00; public static final boolean ChipCastingSimplified = - false; // set this flag to true to demo simplified casting APIs + true; // set this flag to true to demo simplified casting APIs } diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java index 2906ad186d6054..cb5d2170e96d6d 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java @@ -33,6 +33,8 @@ public class ActionSelectorFragment extends Fragment { private final CastingPlayer selectedCastingPlayer; private View.OnClickListener selectContentLauncherButtonClickListener; + private View.OnClickListener selectApplicationBasicButtonClickListener; + private View.OnClickListener selectMediaPlaybackButtonClickListener; private View.OnClickListener disconnectButtonClickListener; public ActionSelectorFragment(CastingPlayer selectedCastingPlayer) { @@ -64,6 +66,16 @@ public View onCreateView( Log.d(TAG, "handle() called on selectContentLauncherButtonClickListener"); callback.handleContentLauncherLaunchURLSelected(selectedCastingPlayer); }; + this.selectApplicationBasicButtonClickListener = + v -> { + Log.d(TAG, "handle() called on selectApplicationBasicButtonClickListener"); + callback.handleApplicationBasicReadVendorIDSelected(selectedCastingPlayer); + }; + this.selectMediaPlaybackButtonClickListener = + v -> { + Log.d(TAG, "handle() called on selectMediaPlaybackButtonClickListener"); + callback.handleMediaPlaybackSubscribeToCurrentStateSelected(selectedCastingPlayer); + }; this.disconnectButtonClickListener = v -> { @@ -82,6 +94,12 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { getView() .findViewById(R.id.selectContentLauncherLaunchURLButton) .setOnClickListener(selectContentLauncherButtonClickListener); + getView() + .findViewById(R.id.selectApplicationBasicReadVendorIDButton) + .setOnClickListener(selectApplicationBasicButtonClickListener); + getView() + .findViewById(R.id.selectMediaPlaybackSubscribeToCurrentStateButton) + .setOnClickListener(selectMediaPlaybackButtonClickListener); getView().findViewById(R.id.disconnectButton).setOnClickListener(disconnectButtonClickListener); } @@ -91,6 +109,12 @@ public interface Callback { /** Notifies listener to trigger transition on selection of Content Launcher cluster */ void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCastingPlayer); + /** Notifies listener to trigger transition on selection of Application Basic cluster */ + void handleApplicationBasicReadVendorIDSelected(CastingPlayer selectedCastingPlayer); + + /** Notifies listener to trigger transition on selection of Media PLayback cluster */ + void handleMediaPlaybackSubscribeToCurrentStateSelected(CastingPlayer selectedCastingPlayer); + /** Notifies listener to trigger transition on click of the Disconnect button */ void handleDisconnect(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ApplicationBasicReadVendorIDExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ApplicationBasicReadVendorIDExampleFragment.java new file mode 100644 index 00000000000000..63ceb19057acd2 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ApplicationBasicReadVendorIDExampleFragment.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.matter.casting; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.R; +import com.matter.casting.core.CastingPlayer; +import com.matter.casting.core.Endpoint; + +import java.util.List; +import java.util.Optional; + +import chip.devicecontroller.ChipClusters; + +/** A {@link Fragment} to read the VendorID (from ApplicationBasic cluster) using the TV Casting App. */ +public class ApplicationBasicReadVendorIDExampleFragment extends Fragment { + private static final String TAG = ApplicationBasicReadVendorIDExampleFragment.class.getSimpleName(); + + private final CastingPlayer selectedCastingPlayer; + + private View.OnClickListener readButtonClickListener; + + public ApplicationBasicReadVendorIDExampleFragment(CastingPlayer selectedCastingPlayer) { + this.selectedCastingPlayer = selectedCastingPlayer; + } + + /** + * Use this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param selectedCastingPlayer CastingPlayer that the casting app connected to + * @return A new instance of fragment ApplicationBasicReadVendorIDExampleFragment. + */ + public static ApplicationBasicReadVendorIDExampleFragment newInstance( + CastingPlayer selectedCastingPlayer) { + return new ApplicationBasicReadVendorIDExampleFragment(selectedCastingPlayer); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + this.readButtonClickListener = + v -> { + Endpoint endpoint = EndpointSelectorExample.selectEndpointByVID(selectedCastingPlayer); + if (endpoint == null) { + Log.e( + TAG, + "No Endpoint with sample vendorID found on CastingPlayer"); + return; + } + + // get ChipClusters.ApplicationBasic from the endpoint + ChipClusters.ApplicationBasicCluster cluster = + endpoint.getCluster(ChipClusters.ApplicationBasicCluster.class); + if (cluster == null) { + Log.e( + TAG, + "Could not get ApplicationBasicCluster for endpoint with ID: " + endpoint.getId()); + return; + } + + // call readVendorIDAttribute on the cluster object while passing in a + // ChipClusters.IntegerAttributeCallback + cluster.readVendorIDAttribute(new ChipClusters.IntegerAttributeCallback() { + @Override + public void onSuccess(int value) { + Log.d(TAG, "ReadVendorID success. Value: " + value); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult); + vendorIdResult.setText( + "Read VendorID result\nValue: " + value ); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "ReadVendorID failure " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult); + vendorIdResult.setText("Read VendorID result\nError: " + error); + }); + } + }); + }; + return inflater.inflate(R.layout.fragment_matter_application_basic_read_vendor_id, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Log.d(TAG, "ApplicationBasicReadVendorIDExampleFragment.onViewCreated called"); + getView().findViewById(R.id.readVendorIdButton).setOnClickListener(readButtonClickListener); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java index c1bde0f9bb0323..062d530e9bcd83 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java @@ -17,18 +17,23 @@ package com.matter.casting; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import chip.devicecontroller.ChipClusters; import com.R; import com.matter.casting.core.CastingPlayer; import com.matter.casting.core.Endpoint; -import java.util.List; +import java.util.Optional; -/** A {@link Fragment} to send Content Launcher LaunchURL command from the TV Casting App. */ +/** A {@link Fragment} to send Content Launcher LaunchURL command using the TV Casting App. */ public class ContentLauncherLaunchURLExampleFragment extends Fragment { private static final String TAG = ContentLauncherLaunchURLExampleFragment.class.getSimpleName(); private static final Integer SAMPLE_ENDPOINT_VID = 65521; @@ -63,19 +68,60 @@ public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { this.launchUrlButtonClickListener = v -> { - Endpoint endpoint = selectEndpoint(); + Endpoint endpoint = EndpointSelectorExample.selectEndpointByVID(selectedCastingPlayer); if (endpoint == null) { + Log.e(TAG, "No Endpoint with sample vendorID found on CastingPlayer"); + return; + } + + EditText contentUrlEditText = getView().findViewById(R.id.contentUrlEditText); + String contentUrl = contentUrlEditText.getText().toString(); + EditText contentDisplayStringEditText = + getView().findViewById(R.id.contentDisplayStringEditText); + String contentDisplayString = contentDisplayStringEditText.getText().toString(); + + // get ChipClusters.ContentLauncherCluster from the endpoint + ChipClusters.ContentLauncherCluster cluster = + endpoint.getCluster(ChipClusters.ContentLauncherCluster.class); + if (cluster == null) { Log.e( TAG, - "No Endpoint with chosen vendorID: " - + SAMPLE_ENDPOINT_VID - + " found on CastingPlayer"); + "Could not get ContentLauncherCluster for endpoint with ID: " + endpoint.getId()); return; } - // TODO: add command invocation API call + // call launchURL on the cluster object while passing in a + // ChipClusters.ContentLauncherCluster.LauncherResponseCallback and request parameters + cluster.launchURL( + new ChipClusters.ContentLauncherCluster.LauncherResponseCallback() { + @Override + public void onSuccess(Integer status, Optional data) { + Log.d(TAG, "LaunchURL success. Status: " + status + ", Data: " + data); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView launcherResult = getView().findViewById(R.id.launcherResult); + launcherResult.setText( + "LaunchURL result\nStatus: " + status + ", Data: " + data); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "LaunchURL failure " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView launcherResult = getView().findViewById(R.id.launcherResult); + launcherResult.setText("LaunchURL result\nError: " + error); + }); + } + }, + contentUrl, + Optional.of(contentDisplayString), + Optional.empty()); }; - return inflater.inflate(R.layout.fragment_content_launcher, container, false); + return inflater.inflate(R.layout.fragment_matter_content_launcher_launch_url, container, false); } @Override @@ -84,22 +130,4 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { Log.d(TAG, "ContentLauncherLaunchURLExampleFragment.onViewCreated called"); getView().findViewById(R.id.launchUrlButton).setOnClickListener(launchUrlButtonClickListener); } - - private Endpoint selectEndpoint() { - Endpoint endpoint = null; - if (selectedCastingPlayer != null) { - List endpoints = selectedCastingPlayer.getEndpoints(); - if (endpoints == null) { - Log.e(TAG, "No Endpoints found on CastingPlayer"); - } else { - endpoint = - endpoints - .stream() - .filter(e -> SAMPLE_ENDPOINT_VID.equals(e.getVendorId())) - .findFirst() - .get(); - } - } - return endpoint; - } } diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/EndpointSelectorExample.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/EndpointSelectorExample.java new file mode 100644 index 00000000000000..b2bfc4e3a8c88d --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/EndpointSelectorExample.java @@ -0,0 +1,34 @@ +package com.matter.casting; + +import android.util.Log; + +import com.matter.casting.core.CastingPlayer; +import com.matter.casting.core.Endpoint; + +import java.util.List; + +/** + * A utility that selects an endpoint based on some criterion + */ +public class EndpointSelectorExample { + private static final String TAG = EndpointSelectorExample.class.getSimpleName(); + private static final Integer SAMPLE_ENDPOINT_VID = 65521; + + public static Endpoint selectEndpointByVID(CastingPlayer selectedCastingPlayer) { + Endpoint endpoint = null; + if (selectedCastingPlayer != null) { + List endpoints = selectedCastingPlayer.getEndpoints(); + if (endpoints == null) { + Log.e(TAG, "No Endpoints found on CastingPlayer"); + } else { + endpoint = + endpoints + .stream() + .filter(e -> SAMPLE_ENDPOINT_VID.equals(e.getVendorId())) + .findFirst() + .get(); + } + } + return endpoint; + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/MediaPlaybackSubscribeToCurrentStateExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/MediaPlaybackSubscribeToCurrentStateExampleFragment.java new file mode 100644 index 00000000000000..4810deff3cddad --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/MediaPlaybackSubscribeToCurrentStateExampleFragment.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.matter.casting; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.R; +import com.matter.casting.core.CastingApp; +import com.matter.casting.core.CastingPlayer; +import com.matter.casting.core.Endpoint; + +import java.util.Date; + +import chip.devicecontroller.ChipClusters; + +/** A {@link Fragment} to subscribe to CurrentState (from MediaPLayback cluster) using the TV Casting App. */ +public class MediaPlaybackSubscribeToCurrentStateExampleFragment extends Fragment { + private static final String TAG = MediaPlaybackSubscribeToCurrentStateExampleFragment.class.getSimpleName(); + + private final CastingPlayer selectedCastingPlayer; + + private View.OnClickListener subscribeButtonClickListener; + private View.OnClickListener shutdownSubscriptionsButtonClickListener; + + public MediaPlaybackSubscribeToCurrentStateExampleFragment(CastingPlayer selectedCastingPlayer) { + this.selectedCastingPlayer = selectedCastingPlayer; + } + + /** + * Use this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param selectedCastingPlayer CastingPlayer that the casting app connected to + * @return A new instance of fragment MediaPlaybackSubscribeToCurrentStateExampleFragment. + */ + public static MediaPlaybackSubscribeToCurrentStateExampleFragment newInstance( + CastingPlayer selectedCastingPlayer) { + return new MediaPlaybackSubscribeToCurrentStateExampleFragment(selectedCastingPlayer); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Endpoint endpoint = EndpointSelectorExample.selectEndpointByVID(selectedCastingPlayer); + if (endpoint == null) { + Log.e( + TAG, + "No Endpoint with sample vendorID found on CastingPlayer"); + return inflater.inflate(R.layout.fragment_matter_media_playback_subscribe_current_state, container, false); + } + + this.subscribeButtonClickListener = + v -> { + // get ChipClusters.MediaPlaybackCluster from the endpoint + ChipClusters.MediaPlaybackCluster cluster = + endpoint.getCluster(ChipClusters.MediaPlaybackCluster.class); + if (cluster == null) { + Log.e( + TAG, + "Could not get ApplicationBasicCluster for endpoint with ID: " + endpoint.getId()); + return; + } + + // call subscribeCurrentStateAttribute on the cluster object while passing in a + // ChipClusters.IntegerAttributeCallback and [0, 1] for min and max interval params + cluster.subscribeCurrentStateAttribute(new ChipClusters.IntegerAttributeCallback() { + @Override + public void onSuccess(int value) { + Log.d(TAG, "Read success on subscription. Value: " + value + " @ " + new Date()); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView currentStateResult = getView().findViewById(R.id.currentStateResult); + currentStateResult.setText( + "Current State result\nValue: " + value ); + }); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "Read failure on subscription: " + error); + new Handler(Looper.getMainLooper()) + .post( + () -> { + TextView currentStateResult = getView().findViewById(R.id.currentStateResult); + currentStateResult.setText("Current State result\nError: " + error); + }); + } + }, 0, 1); + }; + + this.shutdownSubscriptionsButtonClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.d(TAG, "Shutting down subscriptions"); + CastingApp.getInstance().shutdownAllSubscriptions(); + } + }; + + return inflater.inflate(R.layout.fragment_matter_media_playback_subscribe_current_state, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Log.d(TAG, "MediaPlaybackSubscribeToCurrentStateExampleFragment.onViewCreated called"); + getView().findViewById(R.id.subscribeToCurrentStateButton).setOnClickListener(subscribeButtonClickListener); + getView().findViewById(R.id.shutdownSubscriptionsButton).setOnClickListener(shutdownSubscriptionsButtonClickListener); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java index 6d1b63555aad08..f906b80235700c 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java @@ -17,9 +17,11 @@ package com.matter.casting.core; +import chip.devicecontroller.ChipClusters; import com.matter.casting.support.DeviceTypeStruct; import java.util.List; +/** This represents an Endpoint on a CastingPlayer e.g. a Speaker or a Matter Content App */ public interface Endpoint { int getId(); @@ -29,5 +31,9 @@ public interface Endpoint { List getDeviceTypeList(); + /** Get an instance of a cluster based on its Class */ + T getCluster(Class clusterClass); + + /** Get the CastingPlayer that this Endpoint is a part of. */ CastingPlayer getCastingPlayer(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java index b9dd564d6ff95f..86d0eb016958a7 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java @@ -16,12 +16,23 @@ */ package com.matter.casting.core; +import android.util.Log; +import chip.devicecontroller.ChipClusters; import com.matter.casting.support.DeviceTypeStruct; +import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.MatterError; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public class MatterEndpoint implements Endpoint { private static final String TAG = MatterEndpoint.class.getSimpleName(); + private static final long MAX_WAIT_FOR_DEVICE_PROXY_MS = 5000; protected long _cppEndpoint; @Override @@ -36,6 +47,27 @@ public class MatterEndpoint implements Endpoint { @Override public native List getDeviceTypeList(); + @Override + public T getCluster(Class clusterClass) { + try { + Constructor constructor = clusterClass.getDeclaredConstructor(long.class, int.class); + Long deviceProxy = getDeviceProxy(); + if (deviceProxy == null) { + Log.e(TAG, "Could not get DeviceProxy while constructing cluster object"); + return null; + } + return constructor.newInstance(deviceProxy, getId()); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + Log.e( + TAG, + "Could not create cluster object for " + clusterClass.getSimpleName() + " exc: " + e); + return null; + } + } + @Override public native CastingPlayer getCastingPlayer(); @@ -56,4 +88,32 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(getId()); } + + private Long getDeviceProxy() { + CompletableFuture deviceProxyFuture = new CompletableFuture<>(); + getDeviceProxy( + new MatterCallback() { + @Override + public void handle(Long deviceProxy) { + deviceProxyFuture.complete(deviceProxy); + } + }, + new MatterCallback() { + @Override + public void handle(MatterError response) { + deviceProxyFuture.completeExceptionally( + new RuntimeException("Failed on getDeviceProxy: " + response)); + } + }); + + try { + return deviceProxyFuture.get(MAX_WAIT_FOR_DEVICE_PROXY_MS, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + Log.e(TAG, "Exception while waiting on getDeviceProxy future: " + e); + return null; + } + } + + protected native void getDeviceProxy( + MatterCallback successCallback, MatterCallback failureCallback); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp index 2e28b873599a9a..e0c41ebc92d260 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp @@ -19,7 +19,6 @@ #include "MatterEndpoint-JNI.h" #include "../JNIDACProvider.h" -#include "../support/Converters-JNI.h" #include "../support/MatterCallback-JNI.h" #include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" #include "clusters/Clusters.h" // from tv-casting-common @@ -86,6 +85,30 @@ JNI_METHOD(jobject, getCastingPlayer) return support::convertCastingPlayerFromCppToJava(std::shared_ptr(endpoint->GetCastingPlayer())); } +JNI_METHOD(void, getDeviceProxy) +(JNIEnv * env, jobject thiz, jobject jSuccessCallback, jobject jFailureCallback) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterEndpoint-JNI::getDeviceProxy() called"); + Endpoint * endpoint = support::convertEndpointFromJavaToCpp(thiz); + VerifyOrReturn(endpoint != nullptr, ChipLogError(AppServer, "MatterEndpoint-JNI::getDeviceProxy() endpoint == nullptr")); + + ReturnOnFailure(MatterEndpointJNIMgr().mGetDeviceProxySuccessHandler.SetUp(env, jSuccessCallback)); + ReturnOnFailure(MatterEndpointJNIMgr().mGetDeviceProxyFailureHandler.SetUp(env, jFailureCallback)); + + endpoint->GetCastingPlayer()->FindOrEstablishSession( + nullptr, + [](void * context, chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle) { + ChipLogProgress(AppServer, "MatterEndpointJNI FindOrEstablishSession success"); + OperationalDeviceProxy * device = new OperationalDeviceProxy(&exchangeMgr, sessionHandle); // TODO: delete *device + MatterEndpointJNIMgr().mGetDeviceProxySuccessHandler.Handle(device); + }, + [](void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error) { + ChipLogError(AppServer, "MatterEndpointJNI FindOrEstablishSession failure %" CHIP_ERROR_FORMAT, error.Format()); + MatterEndpointJNIMgr().mGetDeviceProxyFailureHandler.Handle(error); + }); +} + }; // namespace core }; // namespace casting }; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h index f9534435ab1903..2c65ca448b6f29 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h @@ -18,8 +18,11 @@ #pragma once +#include "../support/Converters-JNI.h" +#include "../support/MatterCallback-JNI.h" #include "core/Endpoint.h" // from tv-casting-common +#include #include #include #include @@ -30,6 +33,15 @@ namespace core { class MatterEndpointJNI { +public: + MatterEndpointJNI() : + mGetDeviceProxySuccessHandler([](chip::DeviceProxy * device) -> jobject { + return support::convertLongFromCppToJava(reinterpret_cast(device)); + }) + {} + support::MatterCallbackJNI mGetDeviceProxySuccessHandler; + support::MatterFailureCallbackJNI mGetDeviceProxyFailureHandler; + private: friend MatterEndpointJNI & MatterEndpointJNIMgr(); static MatterEndpointJNI sInstance; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp index 9798f2b48b9359..aa5ef2b25f0096 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp @@ -25,6 +25,30 @@ namespace support { using namespace chip; +jobject convertLongFromCppToJava(jlong value) +{ + ChipLogProgress(AppServer, "convertLongFromCppToJava called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass responseTypeClass = env->FindClass("java/lang/Long"); + if (responseTypeClass == nullptr) + { + ChipLogError(AppServer, "ConvertToJObject: Class for Response Type not found!"); + env->ExceptionClear(); + return nullptr; + } + + jmethodID constructor = env->GetMethodID(responseTypeClass, "", "(J)V"); + if (constructor == nullptr) + { + ChipLogError(AppServer, "Failed to access Long constructor"); + env->ExceptionClear(); + return nullptr; + } + return env->NewObject(responseTypeClass, constructor, value); +} + jobject convertMatterErrorFromCppToJava(CHIP_ERROR inErr) { ChipLogProgress(AppServer, "convertMatterErrorFromCppToJava() called"); diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h index ecc3a95d15bd74..aa96af0668f5d4 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h @@ -29,6 +29,8 @@ namespace matter { namespace casting { namespace support { +jobject convertLongFromCppToJava(jlong value); + jobject convertMatterErrorFromCppToJava(CHIP_ERROR inErr); /** diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h index 3c56c426359d5c..82894e075ea912 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h @@ -43,6 +43,7 @@ class MatterCallbackJNI ChipLogProgress(AppServer, "MatterCallbackJNI::SetUp called"); VerifyOrReturnError(env != nullptr, CHIP_JNI_ERROR_NO_ENV, ChipLogError(AppServer, "JNIEnv was null!")); + mCallbackObject.Reset(); ReturnErrorOnFailure(mCallbackObject.Init(inCallback)); jclass mClazz = env->GetObjectClass(mCallbackObject.ObjectRef()); @@ -53,7 +54,7 @@ class MatterCallbackJNI VerifyOrReturnError(mSuperClazz != nullptr, CHIP_JNI_ERROR_TYPE_NOT_FOUND, ChipLogError(AppServer, "Failed to get callback's parent's Java class")); - mMethod = env->GetMethodID(mClazz, "handleInternal", mMethodSignature); + mMethod = env->GetMethodID(mSuperClazz, "handleInternal", mMethodSignature); VerifyOrReturnError( mMethod != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND, ChipLogError(AppServer, "Failed to access 'handleInternal' method with signature %s", mMethodSignature)); diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml index ae61681a0bbf2f..1969c5eeb13212 100644 --- a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml @@ -27,6 +27,24 @@ android:layout_marginRight="10sp" android:layout_below="@id/action_selector_heading" /> +