From ce99a83278fd23c7d823080388b8bd73941bc18d Mon Sep 17 00:00:00 2001 From: Philip Gregor Date: Thu, 30 May 2024 17:02:34 -0700 Subject: [PATCH] Android tv-casting-app v1.3 Commissioner-Generated passcode flow --- .../com/chip/casting/app/MainActivity.java | 46 +++- .../casting/ActionSelectorFragment.java | 30 ++- ...ationBasicReadVendorIDExampleFragment.java | 18 +- .../casting/ConnectionExampleFragment.java | 236 +++++++++++++++--- ...ntentLauncherLaunchURLExampleFragment.java | 18 +- .../casting/DiscoveryExampleFragment.java | 40 ++- .../casting/EndpointSelectorExample.java | 16 ++ .../matter/casting/InitializationExample.java | 31 ++- ...ubscribeToCurrentStateExampleFragment.java | 17 +- .../com/matter/casting/core/CastingApp.java | 36 ++- .../matter/casting/core/CastingPlayer.java | 110 ++++++-- .../jni/com/matter/casting/core/Endpoint.java | 22 +- .../casting/core/MatterCastingPlayer.java | 125 +++++++--- .../matter/casting/support/AppParameters.java | 3 +- .../casting/support/CommissionableData.java | 8 + .../support/CommissionerDeclaration.java | 131 ++++++++++ .../casting/support/ConnectionCallbacks.java | 64 +++++ .../matter/casting/support/DataProvider.java | 17 +- .../IdentificationDeclarationOptions.java | 98 ++++++++ .../matter/casting/support/TargetAppInfo.java | 27 ++ .../src/main/jni/cpp/core/CastingApp-JNI.cpp | 16 ++ .../jni/cpp/core/MatterCastingPlayer-JNI.cpp | 168 +++++++++---- .../jni/cpp/core/MatterCastingPlayer-JNI.h | 1 + .../main/jni/cpp/support/Converters-JNI.cpp | 140 +++++++++++ .../src/main/jni/cpp/support/Converters-JNI.h | 25 ++ .../main/jni/cpp/support/MatterCallback-JNI.h | 11 + .../res/layout/custom_passcode_dialog.xml | 39 +++ .../App/app/src/main/res/values/strings.xml | 2 +- examples/tv-casting-app/android/BUILD.gn | 4 + .../linux/simple-app-helper.cpp | 12 +- .../tv-casting-common/core/CastingPlayer.cpp | 31 +++ .../tv-casting-common/core/CastingPlayer.h | 15 +- .../core/IdentificationDeclarationOptions.h | 21 +- .../UserDirectedCommissioning.h | 1 + .../UserDirectedCommissioningClient.cpp | 12 +- 35 files changed, 1379 insertions(+), 212 deletions(-) create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionerDeclaration.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/ConnectionCallbacks.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/IdentificationDeclarationOptions.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/custom_passcode_dialog.xml 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 cb04518f5c6259..23776ea6e26805 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 @@ -66,9 +66,14 @@ public void handleCommissioningButtonClicked(DiscoveredNodeData commissioner) { } @Override - public void handleConnectionButtonClicked(CastingPlayer castingPlayer) { - Log.i(TAG, "MainActivity.handleConnectionButtonClicked() called"); - showFragment(ConnectionExampleFragment.newInstance(castingPlayer)); + public void handleConnectionButtonClicked( + CastingPlayer castingPlayer, Boolean useCommissionerGeneratedPasscode) { + Log.i( + TAG, + "MainActivity.handleConnectionButtonClicked() useCommissionerGeneratedPasscode: " + + useCommissionerGeneratedPasscode); + showFragment( + ConnectionExampleFragment.newInstance(castingPlayer, useCommissionerGeneratedPasscode)); } @Override @@ -77,26 +82,38 @@ public void handleCommissioningComplete() { } @Override - public void handleConnectionComplete(CastingPlayer castingPlayer) { - Log.i(TAG, "MainActivity.handleConnectionComplete() called "); - showFragment(ActionSelectorFragment.newInstance(castingPlayer)); + public void handleConnectionComplete( + CastingPlayer castingPlayer, Boolean useCommissionerGeneratedPasscode) { + Log.i( + TAG, + "MainActivity.handleConnectionComplete() useCommissionerGeneratedPasscode: " + + useCommissionerGeneratedPasscode); + showFragment( + ActionSelectorFragment.newInstance(castingPlayer, useCommissionerGeneratedPasscode)); } @Override - public void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCastingPlayer) { - showFragment(ContentLauncherLaunchURLExampleFragment.newInstance(selectedCastingPlayer)); + public void handleContentLauncherLaunchURLSelected( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + showFragment( + ContentLauncherLaunchURLExampleFragment.newInstance( + selectedCastingPlayer, commissionerGeneratedPasscodeExample)); } @Override - public void handleApplicationBasicReadVendorIDSelected(CastingPlayer selectedCastingPlayer) { - showFragment(ApplicationBasicReadVendorIDExampleFragment.newInstance(selectedCastingPlayer)); + public void handleApplicationBasicReadVendorIDSelected( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + showFragment( + ApplicationBasicReadVendorIDExampleFragment.newInstance( + selectedCastingPlayer, commissionerGeneratedPasscodeExample)); } @Override public void handleMediaPlaybackSubscribeToCurrentStateSelected( - CastingPlayer selectedCastingPlayer) { + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { showFragment( - MediaPlaybackSubscribeToCurrentStateExampleFragment.newInstance(selectedCastingPlayer)); + MediaPlaybackSubscribeToCurrentStateExampleFragment.newInstance( + selectedCastingPlayer, commissionerGeneratedPasscodeExample)); } @Override @@ -148,7 +165,10 @@ private boolean initJni() { private void showFragment(Fragment fragment, boolean showOnBack) { Log.d( TAG, - "showFragment() called with " + fragment.getClass().getSimpleName() + " and " + showOnBack); + "showFragment() called with: " + + fragment.getClass().getSimpleName() + + ", and showOnBack: " + + showOnBack); FragmentTransaction fragmentTransaction = getSupportFragmentManager() .beginTransaction() 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 cb5d2170e96d6d..0b6504d716d0d5 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 @@ -31,14 +31,17 @@ public class ActionSelectorFragment extends Fragment { private static final String TAG = ActionSelectorFragment.class.getSimpleName(); private final CastingPlayer selectedCastingPlayer; + private final Boolean commissionerGeneratedPasscodeExample; private View.OnClickListener selectContentLauncherButtonClickListener; private View.OnClickListener selectApplicationBasicButtonClickListener; private View.OnClickListener selectMediaPlaybackButtonClickListener; private View.OnClickListener disconnectButtonClickListener; - public ActionSelectorFragment(CastingPlayer selectedCastingPlayer) { + public ActionSelectorFragment( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { this.selectedCastingPlayer = selectedCastingPlayer; + this.commissionerGeneratedPasscodeExample = commissionerGeneratedPasscodeExample; } /** @@ -46,10 +49,13 @@ public ActionSelectorFragment(CastingPlayer selectedCastingPlayer) { * parameters. * * @param selectedCastingPlayer CastingPlayer that the casting app connected to + * @param commissionerGeneratedPasscodeExample Boolean indicating whether this CastingPlayer was + * commissioned using the Commissioner-Generated passcode commissioning flow * @return A new instance of fragment SelectActionFragment. */ - public static ActionSelectorFragment newInstance(CastingPlayer selectedCastingPlayer) { - return new ActionSelectorFragment(selectedCastingPlayer); + public static ActionSelectorFragment newInstance( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + return new ActionSelectorFragment(selectedCastingPlayer, commissionerGeneratedPasscodeExample); } @Override @@ -64,17 +70,20 @@ public View onCreateView( this.selectContentLauncherButtonClickListener = v -> { Log.d(TAG, "handle() called on selectContentLauncherButtonClickListener"); - callback.handleContentLauncherLaunchURLSelected(selectedCastingPlayer); + callback.handleContentLauncherLaunchURLSelected( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); }; this.selectApplicationBasicButtonClickListener = v -> { Log.d(TAG, "handle() called on selectApplicationBasicButtonClickListener"); - callback.handleApplicationBasicReadVendorIDSelected(selectedCastingPlayer); + callback.handleApplicationBasicReadVendorIDSelected( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); }; this.selectMediaPlaybackButtonClickListener = v -> { Log.d(TAG, "handle() called on selectMediaPlaybackButtonClickListener"); - callback.handleMediaPlaybackSubscribeToCurrentStateSelected(selectedCastingPlayer); + callback.handleMediaPlaybackSubscribeToCurrentStateSelected( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); }; this.disconnectButtonClickListener = @@ -107,13 +116,16 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { /** Interface for notifying the host. */ public interface Callback { /** Notifies listener to trigger transition on selection of Content Launcher cluster */ - void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCastingPlayer); + void handleContentLauncherLaunchURLSelected( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample); /** Notifies listener to trigger transition on selection of Application Basic cluster */ - void handleApplicationBasicReadVendorIDSelected(CastingPlayer selectedCastingPlayer); + void handleApplicationBasicReadVendorIDSelected( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample); /** Notifies listener to trigger transition on selection of Media PLayback cluster */ - void handleMediaPlaybackSubscribeToCurrentStateSelected(CastingPlayer selectedCastingPlayer); + void handleMediaPlaybackSubscribeToCurrentStateSelected( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample); /** 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 index 878c18019f4d09..6fdcb600a30fb3 100644 --- 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 @@ -39,11 +39,14 @@ public class ApplicationBasicReadVendorIDExampleFragment extends Fragment { ApplicationBasicReadVendorIDExampleFragment.class.getSimpleName(); private final CastingPlayer selectedCastingPlayer; + private final Boolean commissionerGeneratedPasscodeExample; private View.OnClickListener readButtonClickListener; - public ApplicationBasicReadVendorIDExampleFragment(CastingPlayer selectedCastingPlayer) { + public ApplicationBasicReadVendorIDExampleFragment( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { this.selectedCastingPlayer = selectedCastingPlayer; + this.commissionerGeneratedPasscodeExample = commissionerGeneratedPasscodeExample; } /** @@ -54,8 +57,9 @@ public ApplicationBasicReadVendorIDExampleFragment(CastingPlayer selectedCasting * @return A new instance of fragment ApplicationBasicReadVendorIDExampleFragment. */ public static ApplicationBasicReadVendorIDExampleFragment newInstance( - CastingPlayer selectedCastingPlayer) { - return new ApplicationBasicReadVendorIDExampleFragment(selectedCastingPlayer); + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + return new ApplicationBasicReadVendorIDExampleFragment( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); } @Override @@ -68,8 +72,12 @@ public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { this.readButtonClickListener = v -> { - Endpoint endpoint = - EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + Endpoint endpoint; + if (commissionerGeneratedPasscodeExample) { + endpoint = EndpointSelectorExample.selectFirstEndpoint(selectedCastingPlayer); + } else { + endpoint = EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + } if (endpoint == null) { Log.e(TAG, "No Endpoint with sample vendorID found on CastingPlayer"); return; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java index 690a9b02e840bc..ae5e60e2a0c7d4 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java @@ -16,20 +16,29 @@ */ package com.matter.casting; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +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.CastingApp; import com.matter.casting.core.CastingPlayer; -import com.matter.casting.support.EndpointFilter; +import com.matter.casting.support.CommissionerDeclaration; +import com.matter.casting.support.ConnectionCallbacks; +import com.matter.casting.support.DataProvider; +import com.matter.casting.support.IdentificationDeclarationOptions; import com.matter.casting.support.MatterCallback; import com.matter.casting.support.MatterError; +import com.matter.casting.support.TargetAppInfo; import java.util.concurrent.Executors; /** A {@link Fragment} to Verify or establish a connection with a selected Casting Player. */ @@ -37,18 +46,28 @@ public class ConnectionExampleFragment extends Fragment { private static final String TAG = ConnectionExampleFragment.class.getSimpleName(); // Time (in sec) to keep the commissioning window open, if commissioning is required. // Must be >= 3 minutes. - private static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; - private static final Integer DESIRED_ENDPOINT_VENDOR_ID = 65521; + private static final short MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; + private static final Integer DESIRED_TARGET_APP_VENDOR_ID = 65521; + // Use this Target Content Application Vendor ID, configured on the tv-app, to demonstrate the + // Commissioner-Generated passcode commissioning flow. + private static final Integer DESIRED_TARGET_APP_VENDOR_ID_FOR_CGP_FLOW = 1111; + private static final String DEFAULT_COMMISSIONER_GENERATED_PASSCODE = "12345678"; + private static final int DEFAULT_DISCRIMINATOR_FOR_CGP_FLOW = 0; private final CastingPlayer targetCastingPlayer; + private final Boolean useCommissionerGeneratedPasscode; private TextView connectionFragmentStatusTextView; private Button connectionFragmentNextButton; - public ConnectionExampleFragment(CastingPlayer targetCastingPlayer) { + public ConnectionExampleFragment( + CastingPlayer targetCastingPlayer, Boolean useCommissionerGeneratedPasscode) { Log.i( TAG, - "ConnectionExampleFragment() called with target CastingPlayer ID: " - + targetCastingPlayer.getDeviceId()); + "ConnectionExampleFragment() Target CastingPlayer ID: " + + targetCastingPlayer.getDeviceId() + + ", useCommissionerGeneratedPasscode: " + + useCommissionerGeneratedPasscode); this.targetCastingPlayer = targetCastingPlayer; + this.useCommissionerGeneratedPasscode = useCommissionerGeneratedPasscode; } /** @@ -57,21 +76,22 @@ public ConnectionExampleFragment(CastingPlayer targetCastingPlayer) { * * @return A new instance of fragment ConnectionExampleFragment. */ - public static ConnectionExampleFragment newInstance(CastingPlayer castingPlayer) { - Log.i(TAG, "newInstance() called"); - return new ConnectionExampleFragment(castingPlayer); + public static ConnectionExampleFragment newInstance( + CastingPlayer castingPlayer, Boolean useCommissionerGeneratedPasscode) { + Log.i(TAG, "newInstance()"); + return new ConnectionExampleFragment(castingPlayer, useCommissionerGeneratedPasscode); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.i(TAG, "onCreate() called"); + Log.i(TAG, "onCreate()"); } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log.i(TAG, "onCreateView() called"); + Log.i(TAG, "onCreateView()"); // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_matter_connection_example, container, false); } @@ -79,23 +99,30 @@ public View onCreateView( @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - Log.i(TAG, "onViewCreated() called"); + Log.i(TAG, "onViewCreated()"); connectionFragmentStatusTextView = getView().findViewById(R.id.connectionFragmentStatusText); - connectionFragmentStatusTextView.setText( - "Verifying or establishing connection with Casting Player with device name: " - + targetCastingPlayer.getDeviceName() - + "\nSetup Passcode: " - + InitializationExample.commissionableDataProvider.get().getSetupPasscode() - + "\nDiscriminator: " - + InitializationExample.commissionableDataProvider.get().getDiscriminator()); + if (useCommissionerGeneratedPasscode) { + connectionFragmentStatusTextView.setText( + "Verifying or establishing connection with Casting Player with device name: " + + targetCastingPlayer.getDeviceName() + + "\n\nAttempting Commissioner-Generated passcode commissioning."); + } else { + connectionFragmentStatusTextView.setText( + "Verifying or establishing connection with Casting Player with device name: " + + targetCastingPlayer.getDeviceName() + + "\nCommissionee-Generated Setup Passcode: " + + InitializationExample.commissionableDataProvider.get().getSetupPasscode() + + "\nDiscriminator: " + + InitializationExample.commissionableDataProvider.get().getDiscriminator()); + } connectionFragmentNextButton = getView().findViewById(R.id.connectionFragmentNextButton); Callback callback = (ConnectionExampleFragment.Callback) this.getActivity(); connectionFragmentNextButton.setOnClickListener( v -> { Log.i(TAG, "onViewCreated() NEXT clicked. Calling handleConnectionComplete()"); - callback.handleConnectionComplete(targetCastingPlayer); + callback.handleConnectionComplete(targetCastingPlayer, useCommissionerGeneratedPasscode); }); Executors.newSingleThreadExecutor() @@ -103,25 +130,42 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { () -> { Log.d(TAG, "onViewCreated() calling CastingPlayer.verifyOrEstablishConnection()"); - EndpointFilter desiredEndpointFilter = new EndpointFilter(); - desiredEndpointFilter.vendorId = DESIRED_ENDPOINT_VENDOR_ID; + IdentificationDeclarationOptions idOptions = new IdentificationDeclarationOptions(); + TargetAppInfo targetAppInfo = new TargetAppInfo(); + targetAppInfo.vendorId = DESIRED_TARGET_APP_VENDOR_ID; - MatterError err = - targetCastingPlayer.verifyOrEstablishConnection( - MIN_CONNECTION_TIMEOUT_SEC, - desiredEndpointFilter, + if (useCommissionerGeneratedPasscode) { + idOptions.mCommissionerPasscode = true; + targetAppInfo.vendorId = DESIRED_TARGET_APP_VENDOR_ID_FOR_CGP_FLOW; + Log.d( + TAG, + "onViewCreated() calling CastingPlayer.verifyOrEstablishConnection() Target Content Application Vendor ID: " + + targetAppInfo.vendorId + + ", useCommissionerGeneratedPasscode: " + + useCommissionerGeneratedPasscode); + } else { + Log.d( + TAG, + "onViewCreated() calling CastingPlayer.verifyOrEstablishConnection() Target Content Application Vendor ID: " + + targetAppInfo.vendorId); + } + + idOptions.addTargetAppInfo(targetAppInfo); + + ConnectionCallbacks connectionCallbacks = + new ConnectionCallbacks( new MatterCallback() { @Override public void handle(Void v) { Log.i( TAG, - "Connected to CastingPlayer with deviceId: " + "Successfully connected to CastingPlayer with deviceId: " + targetCastingPlayer.getDeviceId()); getActivity() .runOnUiThread( () -> { connectionFragmentStatusTextView.setText( - "Connected to Casting Player with device name: " + "Successfully connected to Casting Player with device name: " + targetCastingPlayer.getDeviceName() + "\n\n"); connectionFragmentNextButton.setEnabled(true); @@ -139,7 +183,39 @@ public void handle(MatterError err) { "Casting Player connection failed due to: " + err + "\n\n"); }); } - }); + }, + null); + + // CommissionerDeclaration is only needed for the Commissioner-Generated passcode + // commissioning flow. + if (useCommissionerGeneratedPasscode) { + connectionCallbacks.onCommissionerDeclaration = + new MatterCallback() { + @Override + public void handle(CommissionerDeclaration cd) { + Log.i(TAG, "CastingPlayer CommissionerDeclaration message received: "); + cd.logDetail(); + + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "CommissionerDeclaration message received from Casting Player: \n\n"); + if (cd.getCommissionerPasscode()) { + + displayPasscodeInputDialog(getActivity()); + + connectionFragmentStatusTextView.setText( + "CommissionerDeclaration message received from Casting Player: A passcode is now displayed for the user by the Casting Player. \n\n"); + } + }); + } + }; + } + + MatterError err = + targetCastingPlayer.verifyOrEstablishConnection( + connectionCallbacks, MIN_CONNECTION_TIMEOUT_SEC, idOptions); if (err.hasError()) { getActivity() @@ -152,9 +228,109 @@ public void handle(MatterError err) { }); } + private void displayPasscodeInputDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + + String title = "Enter the Commissioner-Generated Passcode"; + String instructions = + "Input the Commissioner-Generated passcode displayed on the CastingPlayer UX, or use the default provided (12345678)."; + + LayoutInflater inflater = LayoutInflater.from(context); + View dialogView = inflater.inflate(R.layout.custom_passcode_dialog, null); + + TextView titleTextView = dialogView.findViewById(R.id.dialog_title); + TextView instructionsTextView = dialogView.findViewById(R.id.dialog_instructions); + titleTextView.setText(title); + instructionsTextView.setText(instructions); + + // Set up the input dialog with the default passcode + final EditText input = dialogView.findViewById(R.id.passcode_input); + input.setText(DEFAULT_COMMISSIONER_GENERATED_PASSCODE); + + // Set up the buttons + builder.setPositiveButton( + "Continue Connecting", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String passcode = input.getText().toString(); + Log.i( + TAG, + "displayPasscodeInputDialog() User entered Commissioner-Generated passcode: " + + passcode); + + // Display the user entered passcode on the screen + connectionFragmentStatusTextView.setText( + "Continue Connecting with user entered Commissioner-Generated passcode: " + + passcode + + "\n\n"); + + long passcodeLongValue = 12345678; + try { + passcodeLongValue = Long.parseLong(passcode); + } catch (NumberFormatException nfe) { + } + + // Update the CommissionableData DataProvider and AndroidChipPlatform with the user + // entered Commissioner-Generated setup passcode. This is mandatory for + // Commissioner-Generated passcode commissioning. + ((DataProvider) InitializationExample.commissionableDataProvider) + .updateCommissionableDataSetupPasscode( + passcodeLongValue, DEFAULT_DISCRIMINATOR_FOR_CGP_FLOW); + MatterError err = + CastingApp.getInstance().updateAndroidChipPlatformWithCommissionableData(); + + if (err.hasError()) { + connectionFragmentStatusTextView.setText( + "displayPasscodeInputDialog() Casting Player connection failed due to: " + + err + + "\n\n"); + Log.e(TAG, "displayPasscodeInputDialog() calling stopConnecting() due to: " + err); + targetCastingPlayer.stopConnecting(); + } else { + Log.i(TAG, "displayPasscodeInputDialog() calling continueConnecting()"); + connectionFragmentStatusTextView = + getView().findViewById(R.id.connectionFragmentStatusText); + connectionFragmentStatusTextView.setText( + "Continuing connection with Casting Player with device name: " + + targetCastingPlayer.getDeviceName() + + "\nCommissioner-Generated Setup Passcode: " + + InitializationExample.commissionableDataProvider.get().getSetupPasscode() + + "\nDiscriminator: " + + InitializationExample.commissionableDataProvider.get().getDiscriminator()); + + targetCastingPlayer.continueConnecting(); + } + } + }); + + builder.setNegativeButton( + "Cancel", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i( + TAG, + "displayPasscodeInputDialog() user cancelled the Commissioner-Generated Passcode input dialog."); + connectionFragmentStatusTextView.setText( + "Connection attempt with Casting Player cancelled by the user, route back to exit. \n\n"); + targetCastingPlayer.stopConnecting(); + dialog.cancel(); + } + }); + + builder.setView(dialogView); + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + alertDialog + .getWindow() + .setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + /** Interface for notifying the host. */ public interface Callback { /** Notifies listener to trigger transition on completion of connection */ - void handleConnectionComplete(CastingPlayer castingPlayer); + void handleConnectionComplete( + CastingPlayer castingPlayer, Boolean useCommissionerGeneratedPasscode); } } 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 ddf41b349d6890..cdae4c7e2a81ec 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 @@ -39,11 +39,14 @@ public class ContentLauncherLaunchURLExampleFragment extends Fragment { private static final Integer SAMPLE_ENDPOINT_VID = 65521; private final CastingPlayer selectedCastingPlayer; + private final Boolean commissionerGeneratedPasscodeExample; private View.OnClickListener launchUrlButtonClickListener; - public ContentLauncherLaunchURLExampleFragment(CastingPlayer selectedCastingPlayer) { + public ContentLauncherLaunchURLExampleFragment( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { this.selectedCastingPlayer = selectedCastingPlayer; + this.commissionerGeneratedPasscodeExample = commissionerGeneratedPasscodeExample; } /** @@ -54,8 +57,9 @@ public ContentLauncherLaunchURLExampleFragment(CastingPlayer selectedCastingPlay * @return A new instance of fragment ContentLauncherLaunchURLExampleFragment. */ public static ContentLauncherLaunchURLExampleFragment newInstance( - CastingPlayer selectedCastingPlayer) { - return new ContentLauncherLaunchURLExampleFragment(selectedCastingPlayer); + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + return new ContentLauncherLaunchURLExampleFragment( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); } @Override @@ -68,8 +72,12 @@ public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { this.launchUrlButtonClickListener = v -> { - Endpoint endpoint = - EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + Endpoint endpoint; + if (commissionerGeneratedPasscodeExample) { + endpoint = EndpointSelectorExample.selectFirstEndpoint(selectedCastingPlayer); + } else { + endpoint = EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + } if (endpoint == null) { Log.e(TAG, "No Endpoint with sample vendorID found on CastingPlayer"); return; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java index 65d670584971aa..69160b99745741 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java @@ -45,7 +45,7 @@ public class DiscoveryExampleFragment extends Fragment { private static final Long DISCOVERY_TARGET_DEVICE_TYPE = 35L; private static final int DISCOVERY_RUNTIME_SEC = 15; private TextView matterDiscoveryMessageTextView; - private TextView matterDiscoveryErrorMessageTextView; + public static TextView matterDiscoveryErrorMessageTextView; private static final List castingPlayerList = new ArrayList<>(); private static ArrayAdapter arrayAdapter; @@ -221,8 +221,8 @@ public void onPause() { /** Interface for notifying the host. */ public interface Callback { /** Notifies listener of Connection Button click. */ - // TODO: In following PRs. Implement CastingPlayer connection - void handleConnectionButtonClicked(CastingPlayer castingPlayer); + void handleConnectionButtonClicked( + CastingPlayer castingPlayer, Boolean useCommissionerGeneratedPasscode); } private boolean startDiscovery() { @@ -320,6 +320,8 @@ public View getView(int i, View view, ViewGroup viewGroup) { Button playerDescription = view.findViewById(R.id.commissionable_player_description); playerDescription.setText(buttonText); + // OnClickListener for the CastingPLayer button, to be used for the Commissionee-Generated + // passcode commissioning flow. View.OnClickListener clickListener = v -> { CastingPlayer castingPlayer = playerList.get(i); @@ -329,9 +331,37 @@ public View getView(int i, View view, ViewGroup viewGroup) { + castingPlayer.getDeviceId()); DiscoveryExampleFragment.Callback onClickCallback = (DiscoveryExampleFragment.Callback) context; - onClickCallback.handleConnectionButtonClicked(castingPlayer); + onClickCallback.handleConnectionButtonClicked(castingPlayer, false); }; playerDescription.setOnClickListener(clickListener); + + // OnLongClickListener for the CastingPLayer button, to be used for the Commissioner-Generated + // passcode commissioning flow. + View.OnLongClickListener longClickListener = + v -> { + CastingPlayer castingPlayer = playerList.get(i); + if (!castingPlayer.getSupportsCommissionerGeneratedPasscode()) { + Log.e( + TAG, + "OnLongClickListener.onLongClick() called for CastingPlayer with deviceId " + + castingPlayer.getDeviceId() + + ". This CastingPlayer does not support Commissioner-Generated passcode commissioning."); + + DiscoveryExampleFragment.matterDiscoveryErrorMessageTextView.setText( + "The selected Casting Player does not support Commissioner-Generated passcode commissioning"); + return true; + } + Log.d( + TAG, + "OnLongClickListener.onLongClick() called for CastingPlayer with deviceId " + + castingPlayer.getDeviceId() + + ", attempting the Commissioner-Generated passcode commissioning flow."); + DiscoveryExampleFragment.Callback onClickCallback = + (DiscoveryExampleFragment.Callback) context; + onClickCallback.handleConnectionButtonClicked(castingPlayer, true); + return true; + }; + playerDescription.setOnLongClickListener(longClickListener); return view; } @@ -353,7 +383,7 @@ private String getCastingPlayerButtonText(CastingPlayer player) { aux += (aux.isEmpty() ? "" : ", ") + "Resolved IP?: " + (player.getIpAddresses().size() > 0); aux += (aux.isEmpty() ? "" : ", ") - + "Supports Commissioner Generated Passcode: " + + "Supports Commissioner-Generated Passcode: " + (player.getSupportsCommissionerGeneratedPasscode()); aux = aux.isEmpty() ? aux : "\n" + aux; 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 index c2932c59117c64..5a042cbb787e77 100644 --- 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 @@ -31,4 +31,20 @@ public static Endpoint selectFirstEndpointByVID(CastingPlayer selectedCastingPla } return endpoint; } + + /** + * Returns the first Endpoint in the list of Endpoints associated with the selectedCastingPlayer. + */ + public static Endpoint selectFirstEndpoint(CastingPlayer selectedCastingPlayer) { + Endpoint endpoint = null; + if (selectedCastingPlayer != null) { + List endpoints = selectedCastingPlayer.getEndpoints(); + if (endpoints == null || endpoints.isEmpty()) { + Log.e(TAG, "No Endpoints found on CastingPlayer"); + } else { + endpoint = endpoints.get(0); + } + } + return endpoint; + } } diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java index aa602c79a66a94..3882adec759c8a 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java @@ -28,6 +28,10 @@ public class InitializationExample { private static final String TAG = InitializationExample.class.getSimpleName(); + // Dummy values for commissioning demonstration only. These are hard coded in the example tv-app: + // connectedhomeip/examples/tv-app/tv-common/src/AppTv.cpp + private static final long DUMMY_SETUP_PASSCODE = 20202021; + private static final int DUMMY_DISCRIMINATOR = 3874; /** * DataProvider implementation for the Unique ID that is used by the SDK to generate the Rotating @@ -48,12 +52,31 @@ public byte[] get() { * DataProvider implementation for the Commissioning Data used by the SDK when the CastingApp goes * through commissioning */ - static final DataProvider commissionableDataProvider = + public static DataProvider commissionableDataProvider = new DataProvider() { + CommissionableData commissionableData = + new CommissionableData(DUMMY_SETUP_PASSCODE, DUMMY_DISCRIMINATOR); + @Override public CommissionableData get() { - // dummy values for demonstration only - return new CommissionableData(20202021, 3874); + return commissionableData; + } + + /** + * Must be implemented in the CommissionableData DataProvider if the Commissioner-Generated + * passcode commissioning flow is going to be used. In this flow, the setup passcode is + * generated by the CastingPlayer and entered by the user in the tv-casting-app CX. Once it + * is obtained, this function should be called with the Commissioner-Generated passcode to + * update the CommissionableData DataProvider in AppParameters. The client is also + * responsible for calling CastingApp::updateAndroidChipPlatformWithCommissionableData() to + * update the data provider set which was previously set in the AppParameters and + * AndroidChipPlatform at initialization time. + */ + @Override + public void updateCommissionableDataSetupPasscode(long setupPasscode, int discriminator) { + Log.i(TAG, "DataProvider::updateCommissionableDataSetupPasscode()"); + commissionableData.setSetupPasscode(setupPasscode); + commissionableData.setDiscriminator(discriminator); } }; @@ -70,7 +93,7 @@ public CommissionableData get() { */ public static MatterError initAndStart(Context applicationContext) { // Create an AppParameters object to pass in global casting parameters to the SDK - final AppParameters appParameters = + AppParameters appParameters = new AppParameters( applicationContext, new DataProvider() { 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 index 77f5129b6b9e4d..b2e5ba6e3c6714 100644 --- 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 @@ -42,12 +42,15 @@ public class MediaPlaybackSubscribeToCurrentStateExampleFragment extends Fragmen MediaPlaybackSubscribeToCurrentStateExampleFragment.class.getSimpleName(); private final CastingPlayer selectedCastingPlayer; + private final Boolean commissionerGeneratedPasscodeExample; private View.OnClickListener subscribeButtonClickListener; private View.OnClickListener shutdownSubscriptionsButtonClickListener; - public MediaPlaybackSubscribeToCurrentStateExampleFragment(CastingPlayer selectedCastingPlayer) { + public MediaPlaybackSubscribeToCurrentStateExampleFragment( + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { this.selectedCastingPlayer = selectedCastingPlayer; + this.commissionerGeneratedPasscodeExample = commissionerGeneratedPasscodeExample; } /** @@ -58,8 +61,9 @@ public MediaPlaybackSubscribeToCurrentStateExampleFragment(CastingPlayer selecte * @return A new instance of fragment MediaPlaybackSubscribeToCurrentStateExampleFragment. */ public static MediaPlaybackSubscribeToCurrentStateExampleFragment newInstance( - CastingPlayer selectedCastingPlayer) { - return new MediaPlaybackSubscribeToCurrentStateExampleFragment(selectedCastingPlayer); + CastingPlayer selectedCastingPlayer, Boolean commissionerGeneratedPasscodeExample) { + return new MediaPlaybackSubscribeToCurrentStateExampleFragment( + selectedCastingPlayer, commissionerGeneratedPasscodeExample); } @Override @@ -70,7 +74,12 @@ public void onCreate(Bundle savedInstanceState) { @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Endpoint endpoint = EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + Endpoint endpoint; + if (commissionerGeneratedPasscodeExample) { + endpoint = EndpointSelectorExample.selectFirstEndpoint(selectedCastingPlayer); + } else { + endpoint = EndpointSelectorExample.selectFirstEndpointByVID(selectedCastingPlayer); + } if (endpoint == null) { Log.e(TAG, "No Endpoint with sample vendorID found on CastingPlayer"); return inflater.inflate( diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java index f79eab859a8ce3..d912420006a32d 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java @@ -46,6 +46,7 @@ public final class CastingApp { private AppParameters appParameters; private NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState; private ChipAppServer chipAppServer; + private AndroidChipPlatform chipPlatform; private CastingApp() {} @@ -62,7 +63,7 @@ public static CastingApp getInstance() { * @param appParameters */ public MatterError initialize(AppParameters appParameters) { - Log.i(TAG, "CastingApp.initialize called"); + Log.i(TAG, "CastingApp.initialize() called"); if (mState != CastingAppState.UNINITIALIZED) { return MatterError.CHIP_ERROR_INCORRECT_STATE; } @@ -72,7 +73,7 @@ public MatterError initialize(AppParameters appParameters) { new NsdManagerServiceResolver.NsdManagerResolverAvailState(); Context applicationContext = appParameters.getApplicationContext(); - AndroidChipPlatform chipPlatform = + chipPlatform = new AndroidChipPlatform( new AndroidBleManager(), new PreferencesKeyValueStoreManager(appParameters.getApplicationContext()), @@ -93,7 +94,8 @@ public MatterError initialize(AppParameters appParameters) { commissionableData.getDiscriminator()); if (!updated) { Log.e( - TAG, "CastingApp.initApp failed to updateCommissionableDataProviderData on chipPlatform"); + TAG, + "CastingApp.initialize() failed to (updateCommissionableDataProviderData() on chipPlatform"); return MatterError.CHIP_ERROR_INVALID_ARGUMENT; } @@ -106,6 +108,34 @@ public MatterError initialize(AppParameters appParameters) { return err; } + /** + * Updates the Android CHIP platform with the CommissionableData. This function retrieves + * commissionable data from the AppParameters and updates the Android CHIP platform using this + * data. The commissionable data includes information such as the SPAKE2+ verifier, salt, + * iteration count, setup passcode, and discriminator. + * + * @return MatterError.NO_ERROR if the update was successful, + * MatterError.CHIP_ERROR_INVALID_ARGUMENT otherwise. + */ + public MatterError updateAndroidChipPlatformWithCommissionableData() { + Log.i(TAG, "CastingApp.updateAndroidChipPlatformWithCommissionableData()"); + CommissionableData commissionableData = appParameters.getCommissionableDataProvider().get(); + boolean updated = + chipPlatform.updateCommissionableDataProviderData( + commissionableData.getSpake2pVerifierBase64(), + commissionableData.getSpake2pSaltBase64(), + commissionableData.getSpake2pIterationCount(), + commissionableData.getSetupPasscode(), + commissionableData.getDiscriminator()); + if (!updated) { + Log.e( + TAG, + "CastingApp.updateAndroidChipPlatformWithCommissionableData() failed to updateCommissionableDataProviderData() on AndroidChipPlatform"); + return MatterError.CHIP_ERROR_INVALID_ARGUMENT; + } + return MatterError.NO_ERROR; + } + /** * Starts the Matter server that the CastingApp runs on and registers all the necessary delegates */ diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java index 3c3a74032bd313..425fc237e5cce1 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java @@ -16,8 +16,8 @@ */ package com.matter.casting.core; -import com.matter.casting.support.EndpointFilter; -import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.ConnectionCallbacks; +import com.matter.casting.support.IdentificationDeclarationOptions; import com.matter.casting.support.MatterError; import java.net.InetAddress; import java.util.List; @@ -63,39 +63,97 @@ public interface CastingPlayer { int hashCode(); /** - * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. - * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on - * disk, this will execute the user directed commissioning process. - * + * @brief Verifies that a connection exists with this CastingPlayer, or triggers a new + * commissioning session request. If the CastingApp does not have the nodeId and fabricIndex + * of this CastingPlayer cached on disk, this will execute the User Directed Commissioning + * (UDC) process by sending an IdentificationDeclaration message to the Commissioner. For + * certain UDC features, where a Commissioner reply is expected, this API needs to be followed + * up with the continueConnecting() API defiend below. See the Matter UDC specification or + * parameter class definitions for details on features not included in the description below. + * @param connectionCallbacks contains the onSuccess, onFailure and onCommissionerDeclaration + * callbacks defiend in ConnectCallbacks.java. + *

onSuccess (Required): The callback called when the connection is established + * successfully. + *

onFailure (Required): The callback called with MatterError when the connection is fails + * to establish. + *

onCommissionerDeclaration (Optional): The callback called when the Commissionee receives + * a CommissionerDeclaration message from the Commissioner. This callback is needed to support + * UDC features where a reply from the Commissioner is expected. It provides information + * indicating the Commissioner’s pre-commissioning state. + *

For example: During Commissioner-Generated passcode commissioning, the Commissioner + * replies with a CommissionerDeclaration message with PasscodeDialogDisplayed and + * CommissionerPasscode set to true. Given these Commissioner state details, the client is + * expected to perform some actions, detailed in the continueConnecting() API below, and then + * call the continueConnecting() API to complete the process. * @param commissioningWindowTimeoutSec (Optional) time (in sec) to keep the commissioning window - * open, if commissioning is required. Needs to be >= MIN_CONNECTION_TIMEOUT_SEC. - * @param desiredEndpointFilter (Optional) Attributes (such as VendorId) describing an Endpoint - * that the client wants to interact with after commissioning. If this value is passed in, the - * VerifyOrEstablishConnection will force User Directed Commissioning, in case the desired - * Endpoint is not found in the on device CastingStore. - * @param successCallback called when the connection is established successfully - * @param failureCallback called with MatterError when the connection is fails to establish + * open, if commissioning is required. Needs to be >= kCommissioningWindowTimeoutSec. + * @param idOptions (Optional) Parameters in the IdentificationDeclaration message sent by the + * Commissionee to the Commissioner. These parameters specify the information relating to the + * requested commissioning session. + *

For example: To invoke the Commissioner-Generated passcode commissioning flow, the + * client would call this API with IdentificationDeclarationOptions containing + * CommissionerPasscode set to true. See IdentificationDeclarationOptions.java for a complete + * list of optional parameters. + *

Furthermore, attributes (such as VendorId) describe the TargetApp that the client wants + * to interact with after commissioning. If this value is passed in, + * verifyOrEstablishConnection() will force UDC, in case the desired TargetApp is not found in + * the on-device CastingStore. * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a - * MatterError object corresponding to the error + * MatterError object corresponding to the error. */ MatterError verifyOrEstablishConnection( - long commissioningWindowTimeoutSec, - EndpointFilter desiredEndpointFilter, - MatterCallback successCallback, - MatterCallback failureCallback); + ConnectionCallbacks connectionCallbacks, + short commissioningWindowTimeoutSec, + IdentificationDeclarationOptions idOptions); /** - * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. - * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on - * disk, this will execute the user directed commissioning process. + * The simplified version of the verifyOrEstablishConnection() API above. * - * @param successCallback called when the connection is established successfully - * @param failureCallback called with MatterError when the connection is fails to establish + * @param connectionCallbacks contains the onSuccess (Required), onFailure (Required) and + * onCommissionerDeclaration (Optional) callbacks defiend in ConnectCallbacks.java. * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a - * MatterError object corresponding to the error + * MatterError object corresponding to the error. */ - MatterError verifyOrEstablishConnection( - MatterCallback successCallback, MatterCallback failureCallback); + MatterError verifyOrEstablishConnection(ConnectionCallbacks connectionCallbacks); + + /** + * @brief This is a continuation of the Commissioner-Generated passcode commissioning flow started + * via the verifyOrEstablishConnection() API above. It continues the UDC process by sending a + * second IdentificationDeclaration message to Commissioner containing CommissionerPasscode + * and CommissionerPasscodeReady set to true. At this point it is assumed that the following + * have occurred: + *

1. Client (Commissionee) has sent the first IdentificationDeclaration message, via + * verifyOrEstablishConnection(), to the Commissioner containing CommissionerPasscode set to + * true. + *

2. Commissioner generated and displayed a passcode. + *

3. The Commissioner replied with a CommissionerDecelration message with + * PasscodeDialogDisplayed and CommissionerPasscode set to true. + *

4. Client has handled the Commissioner's CommissionerDecelration message. + *

5. Client prompted user to input Passcode from Commissioner. + *

6. Client has updated the commissioning session's PAKE verifier using the user input + * passcode. The client updated the CastingApp's AppParameters + * DataProvider and the AndroidChipPlatform's CommissionableData. This is + * done via the following: a. DataProvider.updateCommissionableDataSetupPasscode(long + * setupPasscode, int discriminator) b. + * CastingApp.getInstance().updateAndroidChipPlatformWithCommissionableData() + *

Note: The same connectionCallbacks and commissioningWindowTimeoutSec parameters passed + * into verifyOrEstablishConnection() will be used. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. + */ + MatterError continueConnecting(); + + /** + * @brief This cancels the Commissioner-Generated passcode commissioning flow started via the + * verifyOrEstablishConnection() API above. It constructs and sends an + * IdentificationDeclaration message to the Commissioner containing CancelPasscode set to + * true. It is used to indicate that the Commissionee user has cancelled the commissioning + * process. This indicates that the Commissioner can dismiss any dialogs corresponding to + * commissioning, such as a Passcode input dialog or a Passcode display dialog. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. + */ + MatterError stopConnecting(); /** @brief Sets the internal connection state of this CastingPlayer to "disconnected" */ void disconnect(); 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 f906b80235700c..d98ee77d5d3806 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 @@ -1,20 +1,16 @@ -/* - * Copyright (c) 2024 Project CHIP Authors - * All rights reserved. +/** + * 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 + *

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 + *

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. + *

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.core; import chip.devicecontroller.ChipClusters; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java index a4f03a00e4f5a2..523e8b01a56bee 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java @@ -16,8 +16,9 @@ */ package com.matter.casting.core; -import com.matter.casting.support.EndpointFilter; -import com.matter.casting.support.MatterCallback; +import android.util.Log; +import com.matter.casting.support.ConnectionCallbacks; +import com.matter.casting.support.IdentificationDeclarationOptions; import com.matter.casting.support.MatterError; import java.net.InetAddress; import java.util.List; @@ -35,7 +36,7 @@ public class MatterCastingPlayer implements CastingPlayer { * Time (in sec) to keep the commissioning window open, if commissioning is required. Must be >= 3 * minutes. */ - public static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; + public static final short MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; private boolean connected; private String deviceId; @@ -162,49 +163,103 @@ public boolean equals(Object o) { } /** - * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. - * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on - * disk, this will execute the user directed commissioning process. - * + * @brief Verifies that a connection exists with this CastingPlayer, or triggers a new + * commissioning session request. If the CastingApp does not have the nodeId and fabricIndex + * of this CastingPlayer cached on disk, this will execute the User Directed Commissioning + * (UDC) process by sending an IdentificationDeclaration message to the Commissioner. For + * certain UDC features, where a Commissioner reply is expected, this API needs to be followed + * up with the continueConnecting() API defiend below. See the Matter UDC specification or + * parameter class definitions for details on features not included in the description below. + * @param connectionCallbacks contains the onSuccess, onFailure and onCommissionerDeclaration + * callbacks defiend in ConnectCallbacks.java. + *

onSuccess (Required): The callback called when the connection is established + * successfully. + *

onFailure (Required): The callback called with MatterError when the connection is fails + * to establish. + *

onCommissionerDeclaration (Optional): The callback called when the Commissionee receives + * a CommissionerDeclaration message from the Commissioner. This callback is needed to support + * UDC features where a reply from the Commissioner is expected. It provides information + * indicating the Commissioner’s pre-commissioning state. + *

For example: During Commissioner-Generated passcode commissioning, the Commissioner + * replies with a CommissionerDeclaration message with PasscodeDialogDisplayed and + * CommissionerPasscode set to true. Given these Commissioner state details, the client is + * expected to perform some actions, detailed in the continueConnecting() API below, and then + * call the continueConnecting() API to complete the process. * @param commissioningWindowTimeoutSec (Optional) time (in sec) to keep the commissioning window - * open, if commissioning is required. Needs to be >= MIN_CONNECTION_TIMEOUT_SEC. - * @param desiredEndpointFilter (Optional) Attributes (such as VendorId) describing an Endpoint - * that the client wants to interact with after commissioning. If this value is passed in, the - * VerifyOrEstablishConnection will force User Directed Commissioning, in case the desired - * Endpoint is not found in the on device CastingStore. - * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed. - * The CompletableFuture will be completed with a Void value if the - * VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be - * completed with an Exception. The Exception will be of type - * com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the - * CastingException will contain the error code and message from the CastingApp. + * open, if commissioning is required. Needs to be >= kCommissioningWindowTimeoutSec. + * @param idOptions (Optional) Parameters in the IdentificationDeclaration message sent by the + * Commissionee to the Commissioner. These parameters specify the information relating to the + * requested commissioning session. + *

For example: To invoke the Commissioner-Generated passcode commissioning flow, the + * client would call this API with IdentificationDeclarationOptions containing + * CommissionerPasscode set to true. See IdentificationDeclarationOptions.java for a complete + * list of optional parameters. + *

Furthermore, attributes (such as VendorId) describe the TargetApp that the client wants + * to interact with after commissioning. If this value is passed in, + * verifyOrEstablishConnection() will force UDC, in case the desired TargetApp is not found in + * the on-device CastingStore. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. */ @Override public native MatterError verifyOrEstablishConnection( - long commissioningWindowTimeoutSec, - EndpointFilter desiredEndpointFilter, - MatterCallback successCallback, - MatterCallback failureCallback); + ConnectionCallbacks connectionCallbacks, + short commissioningWindowTimeoutSec, + IdentificationDeclarationOptions idOptions); /** - * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. - * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on - * disk, this will execute the user directed commissioning process. + * The simplified version of the verifyOrEstablishConnection() API above. * - * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed. - * The CompletableFuture will be completed with a Void value if the - * VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be - * completed with an Exception. The Exception will be of type - * com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the - * CastingException will contain the error code and message from the CastingApp. + * @param connectionCallbacks contains the onSuccess (Required), onFailure (Required) and + * onCommissionerDeclaration (Optional) callbacks defiend in ConnectCallbacks.java. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. */ @Override - public MatterError verifyOrEstablishConnection( - MatterCallback successCallback, MatterCallback failureCallback) { - return verifyOrEstablishConnection( - MIN_CONNECTION_TIMEOUT_SEC, null, successCallback, failureCallback); + public MatterError verifyOrEstablishConnection(ConnectionCallbacks connectionCallbacks) { + Log.d(TAG, "verifyOrEstablishConnection() overload"); + return verifyOrEstablishConnection(connectionCallbacks, MIN_CONNECTION_TIMEOUT_SEC, null); } + /** + * @brief This is a continuation of the Commissioner-Generated passcode commissioning flow started + * via the verifyOrEstablishConnection() API above. It continues the UDC process by sending a + * second IdentificationDeclaration message to Commissioner containing CommissionerPasscode + * and CommissionerPasscodeReady set to true. At this point it is assumed that the following + * have occurred: + *

1. Client (Commissionee) has sent the first IdentificationDeclaration message, via + * verifyOrEstablishConnection(), to the Commissioner containing CommissionerPasscode set to + * true. + *

2. Commissioner generated and displayed a passcode. + *

3. The Commissioner replied with a CommissionerDecelration message with + * PasscodeDialogDisplayed and CommissionerPasscode set to true. + *

4. Client has handled the Commissioner's CommissionerDecelration message. + *

5. Client prompted user to input Passcode from Commissioner. + *

6. Client has updated the commissioning session's PAKE verifier using the user input + * passcode. The client updated the CastingApp's AppParameters + * DataProvider and the AndroidChipPlatform's CommissionableData. This is + * done via the following: a. DataProvider.updateCommissionableDataSetupPasscode(long + * setupPasscode, int discriminator) b. + * CastingApp.getInstance().updateAndroidChipPlatformWithCommissionableData() + *

Note: The same connectionCallbacks and commissioningWindowTimeoutSec parameters passed + * into verifyOrEstablishConnection() will be used. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. + */ + public native MatterError continueConnecting(); + + /** + * @brief This cancels the Commissioner-Generated passcode commissioning flow started via the + * verifyOrEstablishConnection() API above. It constructs and sends an + * IdentificationDeclaration message to the Commissioner containing CancelPasscode set to + * true. It is used to indicate that the Commissionee user has cancelled the commissioning + * process. This indicates that the Commissioner can dismiss any dialogs corresponding to + * commissioning, such as a Passcode input dialog or a Passcode display dialog. + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error. + */ + public native MatterError stopConnecting(); + @Override public native void disconnect(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/AppParameters.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/AppParameters.java index 0734271ca89b88..6b25d7b6aa1981 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/AppParameters.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/AppParameters.java @@ -28,7 +28,8 @@ public class AppParameters { @NonNull private final DataProvider rotatingDeviceIdUniqueIdProvider; - @NonNull private final DataProvider commissionableDataProvider; + // Not final since it needs to be updated during Commissioner-Generated passcode commissioning. + @NonNull private DataProvider commissionableDataProvider; @NonNull private final DACProvider dacProvider; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionableData.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionableData.java index b436c98c533786..cafa42919a121f 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionableData.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionableData.java @@ -39,10 +39,18 @@ public long getSetupPasscode() { return setupPasscode; } + public void setSetupPasscode(long setupPasscode) { + this.setupPasscode = setupPasscode; + } + public int getDiscriminator() { return discriminator; } + public void setDiscriminator(int discriminator) { + this.discriminator = discriminator; + } + @Nullable public String getSpake2pVerifierBase64() { return spake2pVerifierBase64; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionerDeclaration.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionerDeclaration.java new file mode 100644 index 00000000000000..83036768176a70 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/CommissionerDeclaration.java @@ -0,0 +1,131 @@ +/** + * 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.support; + +import android.util.Log; + +/** + * Represents the Commissioner Declaration message sent by a User Directed Commissioning server (Casting Player) to a UDC client (tv-casting-app). + */ +public class CommissionerDeclaration { + static final String TAG = CommissionerDeclaration.class.getSimpleName(); + + public enum CdError { + kNoError(0), + kCommissionableDiscoveryFailed(1), + kPaseConnectionFailed(2), + kPaseAuthFailed(3), + kDacValidationFailed(4), + kAlreadyOnFabric(5), + kOperationalDiscoveryFailed(6), + kCaseConnectionFailed(7), + kCaseAuthFailed(8), + kConfigurationFailed(9), + kBindingConfigurationFailed(10), + kCommissionerPasscodeNotSupported(11), + kInvalidIdentificationDeclarationParams(12), + kAppInstallConsentPending(13), + kAppInstalling(14), + kAppInstallFailed(15), + kAppInstalledRetryNeeded(16), + kCommissionerPasscodeDisabled(17), + kUnexpectedCommissionerPasscodeReady(18); + private final int value; + CdError(int value) { + this.value = value; + } + public int getValue() { + return value; + } + } + + private CdError mErrorCode = CdError.kNoError; + private boolean mNeedsPasscode = false; + private boolean mNoAppsFound = false; + private boolean mPasscodeDialogDisplayed = false; + private boolean mCommissionerPasscode = false; + private boolean mQRCodeDisplayed = false; + + public CommissionerDeclaration(int errorCode, boolean needsPasscode, boolean noAppsFound, + boolean passcodeDialogDisplayed, boolean commissionerPasscode, boolean qrCodeDisplayed) { + mErrorCode = CdError.values()[errorCode]; + mNeedsPasscode = needsPasscode; + mNoAppsFound = noAppsFound; + mPasscodeDialogDisplayed = passcodeDialogDisplayed; + mCommissionerPasscode = commissionerPasscode; + mQRCodeDisplayed = qrCodeDisplayed; + } + + public void setErrorCode(CdError newValue) { + mErrorCode = newValue; + } + + public CdError getErrorCode() { + return mErrorCode; + } + + public void setNeedsPasscode(boolean newValue) { + mNeedsPasscode = newValue; + } + + public boolean getNeedsPasscode() { + return mNeedsPasscode; + } + + public void setNoAppsFound(boolean newValue) { + mNoAppsFound = newValue; + } + + public boolean getNoAppsFound() { + return mNoAppsFound; + } + + public void setPasscodeDialogDisplayed(boolean newValue) { + mPasscodeDialogDisplayed = newValue; + } + + public boolean getPasscodeDialogDisplayed() { + return mPasscodeDialogDisplayed; + } + + public void setCommissionerPasscode(boolean newValue) { + mCommissionerPasscode = newValue; + } + + public boolean getCommissionerPasscode() { + return mCommissionerPasscode; + } + + public void setQRCodeDisplayed(boolean newValue) { + mQRCodeDisplayed = newValue; + } + + public boolean getQRCodeDisplayed() { + return mQRCodeDisplayed; + } + + public void logDetail() { + Log.d(TAG, "CommissionerDeclaration::logDetail() - java"); + Log.d(TAG, "CommissionerDeclaration::mErrorCode: " + mErrorCode.name()); + Log.d(TAG, "CommissionerDeclaration::mNeedsPasscode: " + mNeedsPasscode); + Log.d(TAG, "CommissionerDeclaration::mNoAppsFound: " + mNoAppsFound); + Log.d(TAG, "CommissionerDeclaration::mPasscodeDialogDisplayed: " + mPasscodeDialogDisplayed); + Log.d(TAG, "CommissionerDeclaration::mCommissionerPasscode: " + mCommissionerPasscode); + Log.d(TAG, "CommissionerDeclaration::mQRCodeDisplayed: " + mQRCodeDisplayed); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/ConnectionCallbacks.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/ConnectionCallbacks.java new file mode 100644 index 00000000000000..02daf48311c07c --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/ConnectionCallbacks.java @@ -0,0 +1,64 @@ +/** + * 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.support; + +import com.matter.casting.support.CommissionerDeclaration; +import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.MatterError; + +/** + * @brief A container struct for User Directed Commissioning (UDC) callbacks. + */ +public class ConnectionCallbacks { + + /** + * The callback called when the connection is established successfully. + */ + public final MatterCallback onSuccess; + + /** + * The callback called with MatterError when the connection is fails to establish. + */ + public final MatterCallback onFailure; + + /** + * The callback called when the Commissionee receives a CommissionerDeclaration message from the Commissioner. + */ + public MatterCallback onCommissionerDeclaration; + + public ConnectionCallbacks( + MatterCallback onSuccess, + MatterCallback onFailure, + MatterCallback onCommissionerDeclaration) { + this.onSuccess = onSuccess; + this.onFailure = onFailure; + this.onCommissionerDeclaration = onCommissionerDeclaration; + } + + public MatterCallback getOnSuccess() { + return onSuccess; + } + + public MatterCallback getOnFailure() { + return onFailure; + } + + public MatterCallback getOnCommissionerDeclaration() { + return onCommissionerDeclaration; + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DataProvider.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DataProvider.java index eb701413bcd477..773358a39f9b6a 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DataProvider.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DataProvider.java @@ -19,10 +19,10 @@ import android.util.Log; -public abstract class DataProvider { - private static final String TAG = DataProvider.class.getSimpleName(); +public interface DataProvider { + public static final String TAG = DataProvider.class.getSimpleName(); - protected T _get() { + default T _get() { T val = null; try { val = get(); @@ -33,4 +33,15 @@ protected T _get() { } public abstract T get(); + + /** + * Must be implemented in the CommissionableData DataProvider if the Commissioner-Generated + * passcode commissioning flow is going to be used. In this flow, the setup passcode is generated + * by the CastingPlayer and entered by the user in the tv-casting-app CX. Once it is obtained, + * this function should be called with the Commissioner-Generated passcode to update the + * CommissionableData DataProvider in AppParameters. The client is also responsible for calling + * CastingApp::updateAndroidChipPlatformWithCommissionableData() to update the data provider set + * which was previously set in the AppParameters and AndroidChipPlatform at initialization time. + */ + default void updateCommissionableDataSetupPasscode(long setupPasscode, int discriminator) {} } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/IdentificationDeclarationOptions.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/IdentificationDeclarationOptions.java new file mode 100644 index 00000000000000..c9ef2b4c544687 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/IdentificationDeclarationOptions.java @@ -0,0 +1,98 @@ +/** + * 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.support; + +import java.util.List; +import java.util.ArrayList; +import android.util.Log; +import com.matter.casting.support.TargetAppInfo; + +/** + * This class contains the optional parameters used in the IdentificationDeclaration Message, sent by the Commissionee to the + * Commissioner. The options specify information relating to the requested UDC commissioning session. + */ +public class IdentificationDeclarationOptions { + static final String TAG = IdentificationDeclarationOptions.class.getSimpleName(); + private static final int CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS = 10; + + public IdentificationDeclarationOptions() {} + + /** + * Feature: Target Content Application + * Flag to instruct the Commissioner not to display a Passcode input dialog, and instead send a CommissionerDeclaration message + * if a commissioning Passcode is needed. + */ + public boolean mNoPasscode = false; + /** + * Feature: Coordinate Passcode Dialogs + * Flag to instruct the Commissioner to send a CommissionerDeclaration message when the Passcode input dialog on the + * Commissioner has been shown to the user. + */ + public boolean mCdUponPasscodeDialog = false; + /** + * Feature: Commissioner-Generated Passcode + * Flag to instruct the Commissioner to use the Commissioner-generated Passcode for commissioning. + */ + public boolean mCommissionerPasscode = false; + /** + * Feature: Commissioner-Generated Passcode + * Flag to indicate whether or not the Commissionee has obtained the Commissioner Passcode from the user and is therefore ready + * for commissioning. + */ + public boolean mCommissionerPasscodeReady = false; + /** + * Feature: Coordinate Passcode Dialogs + * Flag to indicate when the Commissionee user has decided to exit the commissioning process. + */ + public boolean mCancelPasscode = false; + /** + * Feature: Target Content Application + * The set of content app Vendor IDs (and optionally, Product IDs) that can be used for authentication. + * Also, if TargetAppInfo is passed in, VerifyOrEstablishConnection() will force User Directed Commissioning, in case the + * desired TargetApp is not found in the on-device CastingStore. + */ + private List mTargetAppInfos = new ArrayList<>(); + + public boolean addTargetAppInfo(TargetAppInfo targetAppInfo) { + Log.d(TAG, "addTargetAppInfo()"); + if (mTargetAppInfos.size() >= CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS) { + Log.e(TAG, "addTargetAppInfo() failed to add TargetAppInfo, max list size is {0}" + CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS); + return false; + } + mTargetAppInfos.add(targetAppInfo); + return true; + } + + public List getTargetAppInfoList() { + return mTargetAppInfos; + } + + public void logDetail() { + Log.d(TAG, "IdentificationDeclarationOptions::logDetail() - java"); + Log.d(TAG, "IdentificationDeclarationOptions::mNoPasscode: " + mNoPasscode); + Log.d(TAG, "IdentificationDeclarationOptions::mCdUponPasscodeDialog: " + mCdUponPasscodeDialog); + Log.d(TAG, "IdentificationDeclarationOptions::mCommissionerPasscode: " + mCommissionerPasscode); + Log.d(TAG, "IdentificationDeclarationOptions::mCommissionerPasscodeReady: " + mCommissionerPasscodeReady); + Log.d(TAG, "IdentificationDeclarationOptions::mCancelPasscode: " + mCancelPasscode); + Log.d(TAG, "IdentificationDeclarationOptions::mTargetAppInfos list: "); + + for (TargetAppInfo targetAppInfo : mTargetAppInfos) { + Log.d(TAG, "\t\tTargetAppInfo - Vendor ID: " + targetAppInfo.vendorId + ", Product ID: " + targetAppInfo.productId); + } + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java new file mode 100644 index 00000000000000..06ed126583c8e6 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java @@ -0,0 +1,27 @@ +/** + * 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.support; + +/** + * Feature: Target Content Application + * The set of content app Vendor IDs (and optionally, Product IDs) that can be used for authentication. + */ +public class TargetAppInfo { + public int vendorId = 0; + public int productId = 0; +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp index 81a42115070da7..56a1a831904cca 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp @@ -24,6 +24,7 @@ // from tv-casting-common #include "core/CastingApp.h" +#include "core/CommissionerDeclarationHandler.h" #include "support/ChipDeviceEventHandler.h" #include @@ -102,6 +103,16 @@ JNI_METHOD(jobject, finishStartup)(JNIEnv *, jobject) VerifyOrReturnValue(err == CHIP_NO_ERROR, support::convertMatterErrorFromCppToJava(err), ChipLogError(AppServer, "Failed to register ChipDeviceEventHandler %" CHIP_ERROR_FORMAT, err.Format())); + ChipLogProgress(AppServer, + "JNI_METHOD CastingAppJNI::finishStartup() calling " + "GetUserDirectedCommissioningClient()->SetCommissionerDeclarationHandler()"); +#if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + // Set a handler for Commissioner's CommissionerDeclaration messages. This is set in + // connectedhomeip/src/protocols/user_directed_commissioning/UserDirectedCommissioning.h + chip::Server::GetInstance().GetUserDirectedCommissioningClient()->SetCommissionerDeclarationHandler( + CommissionerDeclarationHandler::GetInstance()); +#endif // CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } @@ -110,6 +121,11 @@ JNI_METHOD(jobject, shutdownAllSubscriptions)(JNIEnv * env, jobject) chip::DeviceLayer::StackLock lock; ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::shutdownAllSubscriptions called"); +#if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + // Remove the handler previously set for Commissioner's CommissionerDeclaration messages. + chip::Server::GetInstance().GetUserDirectedCommissioningClient()->SetCommissionerDeclarationHandler(nullptr); +#endif // CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + CHIP_ERROR err = matter::casting::core::CastingApp::GetInstance()->ShutdownAllSubscriptions(); return support::convertMatterErrorFromCppToJava(err); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp index b2597c883406d8..a2b0565f30946d 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp @@ -21,10 +21,12 @@ #include "../JNIDACProvider.h" #include "../support/Converters-JNI.h" #include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" -#include "core/CastingApp.h" // from tv-casting-common -#include "core/CastingPlayer.h" // from tv-casting-common -#include "core/CastingPlayerDiscovery.h" // from tv-casting-common -#include "core/ConnectionCallbacks.h" // from tv-casting-common +#include "core/CastingApp.h" // from tv-casting-common +#include "core/CastingPlayer.h" // from tv-casting-common +#include "core/CastingPlayerDiscovery.h" // from tv-casting-common +#include "core/CommissionerDeclarationHandler.h" // from tv-casting-common +#include "core/ConnectionCallbacks.h" // from tv-casting-common +#include "core/IdentificationDeclarationOptions.h" // from tv-casting-common #include #include @@ -44,55 +46,67 @@ namespace core { MatterCastingPlayerJNI MatterCastingPlayerJNI::sInstance; JNI_METHOD(jobject, verifyOrEstablishConnection) -(JNIEnv * env, jobject thiz, jlong commissioningWindowTimeoutSec, jobject desiredEndpointFilterJavaObject, jobject jSuccessCallback, - jobject jFailureCallback) +(JNIEnv * env, jobject thiz, jobject jconnectionCallbacks, jlong commissioningWindowTimeoutSec, + jobject jIdentificationDeclarationOptions) { chip::DeviceLayer::StackLock lock; - ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() called with a timeout of: %ld seconds", - static_cast(commissioningWindowTimeoutSec)); + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() called with a timeout of: %d seconds", + static_cast(commissioningWindowTimeoutSec)); CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); VerifyOrReturnValue(castingPlayer != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); - matter::casting::core::IdentificationDeclarationOptions idOptions; - - // TODO: In the following PRs. Replace EndpointFilter Java class with IdentificationDeclarationOptions Java class. - matter::casting::core::EndpointFilter desiredEndpointFilter; - if (desiredEndpointFilterJavaObject != nullptr) + // Find the ConnectionCallbacks class, get the field IDs of the connection callbacks and extract the callback objects. + jclass connectionCallbacksClass = env->GetObjectClass(jconnectionCallbacks); + VerifyOrReturnValue( + connectionCallbacksClass != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT), + ChipLogError(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() connectionCallbacksClass == nullptr ")); + + jfieldID successCallbackFieldID = + env->GetFieldID(connectionCallbacksClass, "onSuccess", "Lcom/matter/casting/support/MatterCallback;"); + jfieldID failureCallbackFieldID = + env->GetFieldID(connectionCallbacksClass, "onFailure", "Lcom/matter/casting/support/MatterCallback;"); + jfieldID commissionerDeclarationCallbackFieldID = + env->GetFieldID(connectionCallbacksClass, "onCommissionerDeclaration", "Lcom/matter/casting/support/MatterCallback;"); + + jobject jSuccessCallback = env->GetObjectField(jconnectionCallbacks, successCallbackFieldID); + jobject jFailureCallback = env->GetObjectField(jconnectionCallbacks, failureCallbackFieldID); + jobject jCommissionerDeclarationCallback = env->GetObjectField(jconnectionCallbacks, commissionerDeclarationCallbackFieldID); + + VerifyOrReturnValue( + jSuccessCallback != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT), + ChipLogError(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() jSuccessCallback == nullptr but is mandatory ")); + VerifyOrReturnValue( + jSuccessCallback != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT), + ChipLogError(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() jSuccessCallback == nullptr but is mandatory ")); + + // jIdentificationDeclarationOptions is optional + matter::casting::core::IdentificationDeclarationOptions * idOptions = nullptr; + if (jIdentificationDeclarationOptions == nullptr) { - chip::Protocols::UserDirectedCommissioning::TargetAppInfo targetAppInfo; - - // Convert the EndpointFilter Java class to a C++ EndpointFilter - jclass endpointFilterJavaClass = env->GetObjectClass(desiredEndpointFilterJavaObject); - jfieldID vendorIdFieldId = env->GetFieldID(endpointFilterJavaClass, "vendorId", "Ljava/lang/Integer;"); - jfieldID productIdFieldId = env->GetFieldID(endpointFilterJavaClass, "productId", "Ljava/lang/Integer;"); - jobject vendorIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, vendorIdFieldId); - jobject productIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, productIdFieldId); - // jfieldID requiredDeviceTypesFieldId = env->GetFieldID(endpointFilterJavaClass, "requiredDeviceTypes", - // "Ljava/util/List;"); - - // Value of 0 means unspecified - targetAppInfo.vendorId = vendorIdIntegerObject != nullptr - ? static_cast(env->CallIntMethod( - vendorIdIntegerObject, env->GetMethodID(env->GetObjectClass(vendorIdIntegerObject), "intValue", "()I"))) - : 0; - targetAppInfo.productId = productIdIntegerObject != nullptr - ? static_cast(env->CallIntMethod( - productIdIntegerObject, env->GetMethodID(env->GetObjectClass(productIdIntegerObject), "intValue", "()I"))) - : 0; - - CHIP_ERROR result = idOptions.addTargetAppInfo(targetAppInfo); - if (result != CHIP_NO_ERROR) - { - ChipLogError(AppServer, - "MatterCastingPlayer-JNI::verifyOrEstablishConnection() failed to add targetAppInfo: %" CHIP_ERROR_FORMAT, - result.Format()); - } + ChipLogProgress(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() Optional jIdentificationDeclarationOptions not " + "provided by the client"); + } + else + { + ChipLogProgress( + AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() jIdentificationDeclarationOptions was provided by client"); + idOptions = support::convertIdentificationDeclarationOptionsFromJavaToCpp(jIdentificationDeclarationOptions); + VerifyOrReturnValue(idOptions != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT), + ChipLogError(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() " + "convertIdentificationDeclarationOptionsFromJavaToCpp() error")); + idOptions->LogDetail(); } MatterCastingPlayerJNIMgr().mConnectionSuccessHandler.SetUp(env, jSuccessCallback); MatterCastingPlayerJNIMgr().mConnectionFailureHandler.SetUp(env, jFailureCallback); + // auto connectCallback = [](CHIP_ERROR err, CastingPlayer * playerPtr) { ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() ConnectCallback()"); if (err == CHIP_NO_ERROR) @@ -115,13 +129,77 @@ JNI_METHOD(jobject, verifyOrEstablishConnection) } }; - // TODO: In the following PRs. Add optional CommissionerDeclarationHandler callback parameter for the Commissioner-Generated - // passcode commissioning flow. + // jCommissionerDeclarationCallback is optional + if (jCommissionerDeclarationCallback == nullptr) + { + ChipLogProgress(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() optional jCommissionerDeclarationCallback was not " + "provided by the client"); + } + else + { + MatterCastingPlayerJNIMgr().mCommissionerDeclarationHandler.SetUp(env, jCommissionerDeclarationCallback); + } + + auto commissionerDeclarationCallback = [](const chip::Transport::PeerAddress & source, + chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration cd) { + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() CommissionerDeclarationCallback()"); + cd.DebugLog(); + + char addressStr[chip::Transport::PeerAddress::kMaxToStringSize]; + source.ToString(addressStr, sizeof(addressStr)); + ChipLogProgress(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() CommissionerDeclarationCallback() source: %s", + addressStr); + + // Call the Java CommissionerDeclarationCallback if it was provided by the client. + if (!MatterCastingPlayerJNIMgr().mCommissionerDeclarationHandler.IsSetUp()) + { + ChipLogError(AppServer, + "MatterCastingPlayer-JNI::verifyOrEstablishConnection() CommissionerDeclarationCallback() received from " + "but Java callback is not set"); + } + else + { + MatterCastingPlayerJNIMgr().mCommissionerDeclarationHandler.Handle(cd); + } + }; + matter::casting::core::ConnectionCallbacks connectionCallbacks; - connectionCallbacks.mOnConnectionComplete = connectCallback; + connectionCallbacks.mOnConnectionComplete = connectCallback; + connectionCallbacks.mCommissionerDeclarationCallback = commissionerDeclarationCallback; castingPlayer->VerifyOrEstablishConnection(connectionCallbacks, static_cast(commissioningWindowTimeoutSec), - idOptions); + *idOptions); + + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, continueConnecting) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::continueConnecting()"); + + CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturnValue(castingPlayer != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); + + castingPlayer->ContinueConnecting(); + + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, stopConnecting) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::stopConnecting()"); + + CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturnValue(castingPlayer != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); + + castingPlayer->StopConnecting(); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h index 7f58162fff52b3..559fcc6a926b61 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h @@ -33,6 +33,7 @@ class MatterCastingPlayerJNI MatterCastingPlayerJNI() : mConnectionSuccessHandler([](void *) { return nullptr; }) {} support::MatterCallbackJNI mConnectionSuccessHandler; support::MatterFailureCallbackJNI mConnectionFailureHandler; + support::MatterCommissionerDeclarationCallbackJNI mCommissionerDeclarationHandler; private: friend MatterCastingPlayerJNI & MatterCastingPlayerJNIMgr(); 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 00ca47216ae804..a3a4e9eb05d76a 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 @@ -366,6 +366,146 @@ jobject convertLongFromCppToJava(uint64_t responseData) return env->NewObject(responseTypeClass, constructor, responseData); } +chip::Protocols::UserDirectedCommissioning::TargetAppInfo * convertTargetAppInfoFromJavaToCpp(jobject jTargetAppInfo) +{ + ChipLogProgress(AppServer, "convertTargetAppInfoFromJavaToCpp() called"); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, + ChipLogError(AppServer, "convertTargetAppInfoFromJavaToCpp() Could not get JNIEnv for current thread")); + jclass targetAppInfoClass = env->GetObjectClass(jTargetAppInfo); + VerifyOrReturnValue(targetAppInfoClass != nullptr, nullptr, + ChipLogError(AppServer, "convertTargetAppInfoFromJavaToCpp() TargetAppInfo class not found!")); + + jfieldID vendorIdField = env->GetFieldID(targetAppInfoClass, "vendorId", "I"); + jfieldID productIdField = env->GetFieldID(targetAppInfoClass, "productId", "I"); + VerifyOrReturnValue(vendorIdField != nullptr, nullptr, + ChipLogError(AppServer, "convertTargetAppInfoFromJavaToCpp() vendorIdField not found!")); + VerifyOrReturnValue(productIdField != nullptr, nullptr, + ChipLogError(AppServer, "convertTargetAppInfoFromJavaToCpp() productIdField not found!")); + + chip::Protocols::UserDirectedCommissioning::TargetAppInfo * cppTargetAppInfo = + new chip::Protocols::UserDirectedCommissioning::TargetAppInfo(); + + cppTargetAppInfo->vendorId = static_cast(env->GetIntField(jTargetAppInfo, vendorIdField)); + cppTargetAppInfo->productId = static_cast(env->GetIntField(jTargetAppInfo, productIdField)); + + env->DeleteLocalRef(targetAppInfoClass); + + return reinterpret_cast(cppTargetAppInfo); +} + +matter::casting::core::IdentificationDeclarationOptions * convertIdentificationDeclarationOptionsFromJavaToCpp(jobject jIdOptions) +{ + ChipLogProgress(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() called"); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue( + env != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() Could not get JNIEnv for current thread")); + + jclass idOptionsClass = env->GetObjectClass(jIdOptions); + VerifyOrReturnValue( + idOptionsClass != nullptr, nullptr, + ChipLogError(AppServer, + "convertIdentificationDeclarationOptionsFromJavaToCpp() IdentificationDeclarationOptions class not found!")); + + jfieldID noPasscodeField = env->GetFieldID(idOptionsClass, "mNoPasscode", "Z"); + jfieldID cdUponPasscodeDialogField = env->GetFieldID(idOptionsClass, "mCdUponPasscodeDialog", "Z"); + jfieldID commissionerPasscodeField = env->GetFieldID(idOptionsClass, "mCommissionerPasscode", "Z"); + jfieldID commissionerPasscodeReadyField = env->GetFieldID(idOptionsClass, "mCommissionerPasscodeReady", "Z"); + jfieldID cancelPasscodeField = env->GetFieldID(idOptionsClass, "mCancelPasscode", "Z"); + jfieldID targetAppInfosField = env->GetFieldID(idOptionsClass, "mTargetAppInfos", "Ljava/util/List;"); + VerifyOrReturnValue( + noPasscodeField != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() noPasscodeField not found!")); + VerifyOrReturnValue( + cdUponPasscodeDialogField != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() cdUponPasscodeDialogField not found!")); + VerifyOrReturnValue( + commissionerPasscodeField != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() commissionerPasscodeField not found!")); + VerifyOrReturnValue( + commissionerPasscodeReadyField != nullptr, nullptr, + ChipLogError(AppServer, + "convertIdentificationDeclarationOptionsFromJavaToCpp() commissionerPasscodeReadyField not found!")); + VerifyOrReturnValue( + cancelPasscodeField != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() cancelPasscodeField not found!")); + VerifyOrReturnValue( + targetAppInfosField != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() targetAppInfosField not found!")); + + matter::casting::core::IdentificationDeclarationOptions * cppIdOptions = + new matter::casting::core::IdentificationDeclarationOptions(); + + cppIdOptions->mNoPasscode = env->GetBooleanField(jIdOptions, noPasscodeField); + cppIdOptions->mCdUponPasscodeDialog = env->GetBooleanField(jIdOptions, cdUponPasscodeDialogField); + cppIdOptions->mCommissionerPasscode = env->GetBooleanField(jIdOptions, commissionerPasscodeField); + cppIdOptions->mCommissionerPasscodeReady = env->GetBooleanField(jIdOptions, commissionerPasscodeReadyField); + cppIdOptions->mCancelPasscode = env->GetBooleanField(jIdOptions, cancelPasscodeField); + + jobject targetAppInfosList = env->GetObjectField(jIdOptions, targetAppInfosField); + VerifyOrReturnValue( + targetAppInfosList != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() targetAppInfosList not found!")); + jclass listClass = env->FindClass("java/util/List"); + jmethodID sizeMethod = env->GetMethodID(listClass, "size", "()I"); + jmethodID getMethod = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"); + + jint size = env->CallIntMethod(targetAppInfosList, sizeMethod); + + for (jint i = 0; i < size; i++) + { + jobject jTargetAppInfo = env->CallObjectMethod(targetAppInfosList, getMethod, i); + + chip::Protocols::UserDirectedCommissioning::TargetAppInfo * cppTargetAppInfo = + convertTargetAppInfoFromJavaToCpp(jTargetAppInfo); + VerifyOrReturnValue( + cppTargetAppInfo != nullptr, nullptr, + ChipLogError(AppServer, "convertIdentificationDeclarationOptionsFromJavaToCpp() Could not convert jTargetAppInfo")); + + cppIdOptions->addTargetAppInfo(*cppTargetAppInfo); + + env->DeleteLocalRef(jTargetAppInfo); + } + + env->DeleteLocalRef(targetAppInfosList); + env->DeleteLocalRef(listClass); + env->DeleteLocalRef(idOptionsClass); + + return reinterpret_cast(cppIdOptions); +} + +jobject +convertCommissionerDeclarationFromCppToJava(const chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration & cppCd) +{ + ChipLogProgress(AppServer, "convertCommissionerDeclarationFromCppToJava() called"); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue( + env != nullptr, nullptr, + ChipLogError(AppServer, "convertCommissionerDeclarationFromCppToJava() Could not get JNIEnv for current thread")); + + jclass jCommissionerDeclarationClass; + CHIP_ERROR err = chip::JniReferences::GetInstance().GetLocalClassRef(env, "com/matter/casting/support/CommissionerDeclaration", + jCommissionerDeclarationClass); + VerifyOrReturnValue(err == CHIP_NO_ERROR, nullptr); + + jmethodID jCommissionerDeclarationConstructor = env->GetMethodID(jCommissionerDeclarationClass, "", "(IZZZZZ)V"); + if (jCommissionerDeclarationConstructor == nullptr) + { + ChipLogError(AppServer, + "convertCommissionerDeclarationFromCppToJava() Failed to access Java CommissionerDeclaration constructor"); + env->ExceptionClear(); + return nullptr; + } + + return env->NewObject(jCommissionerDeclarationClass, jCommissionerDeclarationConstructor, + static_cast(cppCd.GetErrorCode()), cppCd.GetNeedsPasscode(), cppCd.GetNoAppsFound(), + cppCd.GetPasscodeDialogDisplayed(), cppCd.GetCommissionerPasscode(), cppCd.GetQRCodeDisplayed()); +} + }; // namespace support }; // namespace casting }; // namespace matter 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 aa96af0668f5d4..d5ef62e349ae61 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 @@ -19,7 +19,10 @@ #include "core/BaseCluster.h" #include "core/CastingPlayer.h" #include "core/Command.h" +#include "core/CommissionerDeclarationHandler.h" +#include "core/ConnectionCallbacks.h" #include "core/Endpoint.h" +#include "core/IdentificationDeclarationOptions.h" #include @@ -74,6 +77,28 @@ void * convertCommandFromJavaToCpp(jobject jCommandObject); jobject convertLongFromCppToJava(uint64_t responseData); +/** + * @brief Converts a Java TargetAppInfo into a native MatterTargetAppInfo. + * + * @return pointer to the TargetAppInfo jobject if created successfully, nullptr otherwise. + */ +chip::Protocols::UserDirectedCommissioning::TargetAppInfo * convertTargetAppInfoFromJavaToCpp(jobject jTargetAppInfo); + +/** + * @brief Converts a Java IdentificationDeclarationOptions into a native IdentificationDeclarationOptions + * + * @return pointer to the IdentificationDeclarationOptions if created successfully, nullptr otherwise. + */ +matter::casting::core::IdentificationDeclarationOptions * convertIdentificationDeclarationOptionsFromJavaToCpp(jobject jIdOptions); + +/** + * @brief Converts a native CommissioningDeclaration into a MatterCommissioningDeclaration jobject + * + * @return pointer to the CommissioningDeclaration jobject if created successfully, nullptr otherwise. + */ +jobject +convertCommissionerDeclarationFromCppToJava(const chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration & cppCd); + }; // namespace support }; // namespace casting }; // namespace matter 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 82894e075ea912..d67c9229c8d377 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 @@ -62,6 +62,8 @@ class MatterCallbackJNI return CHIP_NO_ERROR; } + bool IsSetUp() const { return mCallbackObject.HasValidObjectRef() && mMethod != nullptr; } + void Handle(T responseData) { ChipLogProgress(AppServer, "MatterCallbackJNI::Handle called"); @@ -91,6 +93,15 @@ class MatterFailureCallbackJNI : public MatterCallbackJNI MatterFailureCallbackJNI() : MatterCallbackJNI(matter::casting::support::convertMatterErrorFromCppToJava) {} }; +class MatterCommissionerDeclarationCallbackJNI + : public MatterCallbackJNI +{ +public: + MatterCommissionerDeclarationCallbackJNI() : + MatterCallbackJNI(matter::casting::support::convertCommissionerDeclarationFromCppToJava) + {} +}; + }; // namespace support }; // namespace casting }; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/custom_passcode_dialog.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/custom_passcode_dialog.xml new file mode 100644 index 00000000000000..8c7d2ff04d7e25 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/custom_passcode_dialog.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml b/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml index 4485b6a31ab788..c5751d271deb19 100644 --- a/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml +++ b/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml @@ -33,7 +33,7 @@ Initializing Discovering Casting Players on-network: Discovery Stopped - No errors to report + Long click on a Casting Player to try the Commissioner-Generated passcode commissioning flow if supported. Start discovery error. - Start discovery error. Discovery ongoing, stop before starting. - Stop discovery error. - diff --git a/examples/tv-casting-app/android/BUILD.gn b/examples/tv-casting-app/android/BUILD.gn index 416570417b0794..48d68100930a91 100644 --- a/examples/tv-casting-app/android/BUILD.gn +++ b/examples/tv-casting-app/android/BUILD.gn @@ -114,12 +114,16 @@ android_library("java") { "App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayerDiscovery.java", "App/app/src/main/jni/com/matter/casting/support/AppParameters.java", "App/app/src/main/jni/com/matter/casting/support/CommissionableData.java", + "App/app/src/main/jni/com/matter/casting/support/CommissionerDeclaration.java", + "App/app/src/main/jni/com/matter/casting/support/ConnectionCallbacks.java", "App/app/src/main/jni/com/matter/casting/support/DACProvider.java", "App/app/src/main/jni/com/matter/casting/support/DataProvider.java", "App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java", "App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java", + "App/app/src/main/jni/com/matter/casting/support/IdentificationDeclarationOptions.java", "App/app/src/main/jni/com/matter/casting/support/MatterCallback.java", "App/app/src/main/jni/com/matter/casting/support/MatterError.java", + "App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java", ] javac_flags = [ "-Xlint:deprecation" ] diff --git a/examples/tv-casting-app/linux/simple-app-helper.cpp b/examples/tv-casting-app/linux/simple-app-helper.cpp index beef624eea5ff8..4432b11c850174 100644 --- a/examples/tv-casting-app/linux/simple-app-helper.cpp +++ b/examples/tv-casting-app/linux/simple-app-helper.cpp @@ -344,7 +344,7 @@ void CommissionerDeclarationCallback(const chip::Transport::PeerAddress & source { ChipLogProgress(AppServer, "---- Awaiting user input ----"); ChipLogProgress(AppServer, "Input the Commissioner-Generated passcode displayed on the CastingPlayer UX."); - ChipLogProgress(AppServer, "Input 1245678 to use the default passcode."); + ChipLogProgress(AppServer, "Input 12345678 to use the default passcode."); ChipLogProgress(AppServer, "Example: cast setcommissionerpasscode 12345678"); ChipLogProgress(AppServer, "---- Awaiting user input ----"); gAwaitingCommissionerPasscodeInput = true; @@ -458,7 +458,7 @@ CHIP_ERROR CommandHandler(int argc, char ** argv) uint32_t passcode = (uint32_t) strtol(argv[1], &eptr, 10); if (gAwaitingCommissionerPasscodeInput) { - ChipLogProgress(AppServer, "CommandHandler() setcommissionerpasscode user enterd passcode: %d", passcode); + ChipLogProgress(AppServer, "CommandHandler() setcommissionerpasscode user entered passcode: %d", passcode); gAwaitingCommissionerPasscodeInput = false; // Per connectedhomeip/examples/platform/linux/LinuxCommissionableDataProvider.h: We don't support overriding the @@ -495,6 +495,11 @@ CHIP_ERROR CommandHandler(int argc, char ** argv) "CommandHandler() setcommissionerpasscode, no Commissioner-Generated passcode input expected at this time."); } } + if (strcmp(argv[0], "stop-connecting") == 0) + { + ChipLogProgress(AppServer, "CommandHandler() stop-connecting"); + targetCastingPlayer->StopConnecting(); + } if (strcmp(argv[0], "print-bindings") == 0) { PrintBindings(); @@ -536,6 +541,9 @@ CHIP_ERROR PrintAllCommands() " setcommissionerpasscode Set the commissioning session's passcode to the " "Commissioner-Generated passcode. Used for the the Commissioner-Generated passcode commissioning flow. Usage: " "cast setcommissionerpasscode 12345678\r\n"); + streamer_printf(sout, + " stop-connecting Stop connecting to Casting Player upon " + "Commissioner-Generated passcode commissioning flow passcode input request. Usage: cast stop-connecting\r\n"); streamer_printf(sout, "\r\n"); return CHIP_NO_ERROR; diff --git a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp index ff204923da5d40..15b7e0598450df 100644 --- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp +++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp @@ -60,6 +60,10 @@ void CastingPlayer::VerifyOrEstablishConnection(ConnectionCallbacks connectionCa // information indicating the Commissioner's pre-commissioning state. if (connectionCallbacks.mCommissionerDeclarationCallback != nullptr) { + ChipLogProgress(AppServer, + "CastingPlayer::VerifyOrEstablishConnection() Setting CommissionerDeclarationCallback in " + "CommissionerDeclarationHandler"); + // Set the callback for handling CommissionerDeclaration messages. matter::casting::core::CommissionerDeclarationHandler::GetInstance()->SetCommissionerDeclarationCallback( connectionCallbacks.mCommissionerDeclarationCallback); } @@ -210,6 +214,33 @@ void CastingPlayer::ContinueConnecting() } } +void CastingPlayer::StopConnecting() +{ + ChipLogProgress(AppServer, "CastingPlayer::StopConnecting()"); + CHIP_ERROR err = CHIP_NO_ERROR; + mIdOptions.resetState(); + mIdOptions.mCancelPasscode = true; + + ChipLogProgress(AppServer, "CastingPlayer::StopConnecting() calling SendUserDirectedCommissioningRequest()"); +#if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + SuccessOrExit(err = SendUserDirectedCommissioningRequest()); +#endif // CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT + + support::ChipDeviceEventHandler::SetUdcStatus(false); + mConnectionState = CASTING_PLAYER_NOT_CONNECTED; + mCommissioningWindowTimeoutSec = kCommissioningWindowTimeoutSec; + mTargetCastingPlayer = nullptr; + + ChipLogProgress(AppServer, "CastingPlayer::StopConnecting() User Directed Commissioning stopped"); + +exit: + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "CastingPlayer::StopConnecting() failed with %" CHIP_ERROR_FORMAT, err.Format()); + resetState(err); + } +} + void CastingPlayer::resetState(CHIP_ERROR err) { ChipLogProgress(AppServer, "CastingPlayer::resetState()"); diff --git a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h index d5bbcd46006920..82978d6956b16b 100644 --- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h +++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h @@ -176,9 +176,9 @@ class CastingPlayer : public std::enable_shared_from_this * 2. Commissioner generated and displayed a passcode. * 3. The Commissioner replied with a CommissionerDecelration message with PasscodeDialogDisplayed and CommissionerPasscode set * to true. - * 3. Client has handled the Commissioner's CommissionerDecelration message. - * 4. Client prompted user to input Passcode from Commissioner. - * 5. Client has updated the commissioning session's PAKE verifier using the user input Passcode by updating the CastingApps + * 4. Client has handled the Commissioner's CommissionerDecelration message. + * 5. Client prompted user to input Passcode from Commissioner. + * 6. Client has updated the commissioning session's PAKE verifier using the user input Passcode by updating the CastingApp's * CommissionableDataProvider * (matter::casting::core::CastingApp::GetInstance()->UpdateCommissionableDataProvider(CommissionableDataProvider)). * @@ -187,6 +187,15 @@ class CastingPlayer : public std::enable_shared_from_this */ void ContinueConnecting(); + /** + * @brief This cancels the Commissioner-Generated passcode commissioning flow started via the VerifyOrEstablishConnection() API + * above. It constructs and sends an IdentificationDeclaration message to the Commissioner containing CancelPasscode set to + * true. It is used to indicate that the Commissionee user has cancelled the commissioning process. This indicates that the + * Commissioner can dismiss any dialogs corresponding to commissioning, such as a Passcode input dialog or a Passcode display + * dialog. + */ + void StopConnecting(); + /** * @brief Sets the internal connection state of this CastingPlayer to "disconnected" */ diff --git a/examples/tv-casting-app/tv-casting-common/core/IdentificationDeclarationOptions.h b/examples/tv-casting-app/tv-casting-common/core/IdentificationDeclarationOptions.h index d0e8f8a4411b69..23d361efb404e2 100644 --- a/examples/tv-casting-app/tv-casting-common/core/IdentificationDeclarationOptions.h +++ b/examples/tv-casting-app/tv-casting-common/core/IdentificationDeclarationOptions.h @@ -85,6 +85,16 @@ class IdentificationDeclarationOptions std::vector getTargetAppInfoList() const { return mTargetAppInfos; } + void resetState() + { + mNoPasscode = false; + mCdUponPasscodeDialog = false; + mCommissionerPasscode = false; + mCommissionerPasscodeReady = false; + mCancelPasscode = false; + mTargetAppInfos.clear(); + } + /** * @brief Builds an IdentificationDeclaration message to be sent to a CastingPlayer, given the options state specified in this * object. @@ -107,7 +117,6 @@ class IdentificationDeclarationOptions { id.SetCommissionerPasscodeReady(true); id.SetInstanceName(mCommissioneeInstanceName); - mCommissionerPasscodeReady = false; } else { @@ -136,7 +145,7 @@ class IdentificationDeclarationOptions void LogDetail() { - ChipLogDetail(AppServer, "IdentificationDeclarationOptions::LogDetail()"); + ChipLogDetail(AppServer, "IdentificationDeclarationOptions::LogDetail() - cpp"); ChipLogDetail(AppServer, "IdentificationDeclarationOptions::mNoPasscode: %s", mNoPasscode ? "true" : "false"); ChipLogDetail(AppServer, "IdentificationDeclarationOptions::mCdUponPasscodeDialog: %s", @@ -148,6 +157,14 @@ class IdentificationDeclarationOptions ChipLogDetail(AppServer, "IdentificationDeclarationOptions::mCancelPasscode: %s", mCancelPasscode ? "true" : "false"); ChipLogDetail(AppServer, "IdentificationDeclarationOptions::mCommissioneeInstanceName: %s", mCommissioneeInstanceName); + + ChipLogDetail(AppServer, "IdentificationDeclarationOptions::TargetAppInfos list:"); + for (size_t i = 0; i < mTargetAppInfos.size(); i++) + { + const chip::Protocols::UserDirectedCommissioning::TargetAppInfo & info = mTargetAppInfos[i]; + ChipLogDetail(AppServer, "\t\tTargetAppInfo %d, Vendor ID: %u, Product ID: %u", int(i + 1), info.vendorId, + info.productId); + } } private: diff --git a/src/protocols/user_directed_commissioning/UserDirectedCommissioning.h b/src/protocols/user_directed_commissioning/UserDirectedCommissioning.h index 7a496e5a2f0a33..cf9a55365b9191 100644 --- a/src/protocols/user_directed_commissioning/UserDirectedCommissioning.h +++ b/src/protocols/user_directed_commissioning/UserDirectedCommissioning.h @@ -535,6 +535,7 @@ class DLL_EXPORT UserDirectedCommissioningClient : public TransportMgrDelegate */ void SetCommissionerDeclarationHandler(CommissionerDeclarationHandler * commissionerDeclarationHandler) { + ChipLogProgress(AppServer, "UserDirectedCommissioningClient::SetCommissionerDeclarationHandler()"); mCommissionerDeclarationHandler = commissionerDeclarationHandler; } diff --git a/src/protocols/user_directed_commissioning/UserDirectedCommissioningClient.cpp b/src/protocols/user_directed_commissioning/UserDirectedCommissioningClient.cpp index 6d7c315ffb5308..aeb5691be2554c 100644 --- a/src/protocols/user_directed_commissioning/UserDirectedCommissioningClient.cpp +++ b/src/protocols/user_directed_commissioning/UserDirectedCommissioningClient.cpp @@ -242,7 +242,7 @@ void UserDirectedCommissioningClient::OnMessageReceived(const Transport::PeerAdd char addrBuffer[chip::Transport::PeerAddress::kMaxToStringSize]; source.ToString(addrBuffer); - ChipLogProgress(AppServer, "UserDirectedCommissioningClient::OnMessageReceived from %s", addrBuffer); + ChipLogProgress(AppServer, "UserDirectedCommissioningClient::OnMessageReceived() from %s", addrBuffer); PacketHeader packetHeader; @@ -250,14 +250,16 @@ void UserDirectedCommissioningClient::OnMessageReceived(const Transport::PeerAdd if (packetHeader.IsEncrypted()) { - ChipLogError(AppServer, "UDC encryption flag set - ignoring"); + ChipLogError(AppServer, "UserDirectedCommissioningClient::OnMessageReceived() UDC encryption flag set - ignoring"); return; } PayloadHeader payloadHeader; ReturnOnFailure(payloadHeader.DecodeAndConsume(msg)); - ChipLogProgress(AppServer, "CommissionerDeclaration DataLength()=%" PRIu32, static_cast(msg->DataLength())); + ChipLogProgress(AppServer, + "UserDirectedCommissioningClient::OnMessageReceived() CommissionerDeclaration DataLength() = %" PRIu32, + static_cast(msg->DataLength())); uint8_t udcPayload[IdentificationDeclaration::kUdcTLVDataMaxBytes]; size_t udcPayloadLength = std::min(msg->DataLength(), sizeof(udcPayload)); @@ -272,6 +274,10 @@ void UserDirectedCommissioningClient::OnMessageReceived(const Transport::PeerAdd { mCommissionerDeclarationHandler->OnCommissionerDeclarationMessage(source, cd); } + else + { + ChipLogProgress(AppServer, "UserDirectedCommissioningClient::OnMessageReceived() No registered handler for UDC messages"); + } } } // namespace UserDirectedCommissioning