diff --git a/examples/tv-casting-app/android/App/app/src/main/AndroidManifest.xml b/examples/tv-casting-app/android/App/app/src/main/AndroidManifest.xml index 55c3e48357d58b..e551de818eb5b0 100644 --- a/examples/tv-casting-app/android/App/app/src/main/AndroidManifest.xml +++ b/examples/tv-casting-app/android/App/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com"> @@ -10,7 +10,6 @@ - - diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CastingContext.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CastingContext.java index 2ad1d29faf4382..acab829034bd56 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CastingContext.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CastingContext.java @@ -3,6 +3,7 @@ import android.content.Context; import android.widget.LinearLayout; import androidx.fragment.app.FragmentActivity; +import com.R; public class CastingContext { private FragmentActivity fragmentActivity; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java index 16070eb1dfe59e..340563c28b322c 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CertTestFragment.java @@ -10,6 +10,7 @@ import android.widget.ListView; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.R; import com.chip.casting.ContentApp; import com.chip.casting.ContentLauncherTypes; import com.chip.casting.FailureCallback; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CommissionerDiscoveryFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CommissionerDiscoveryFragment.java index dbb95e88806a92..3a119940b64839 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CommissionerDiscoveryFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CommissionerDiscoveryFragment.java @@ -14,6 +14,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.R; import com.chip.casting.DiscoveredNodeData; import com.chip.casting.FailureCallback; import com.chip.casting.MatterError; @@ -67,6 +68,7 @@ public View onCreateView( @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + Log.d(TAG, "onViewCreated() called"); super.onViewCreated(view, savedInstanceState); Button manualCommissioningButton = getView().findViewById(R.id.manualCommissioningButton); @@ -103,7 +105,10 @@ public void onClick(View v) { new SuccessCallback() { @Override public void handle(DiscoveredNodeData discoveredNodeData) { - Log.d(TAG, "Discovered a Video Player Commissioner: " + discoveredNodeData); + Log.d( + TAG, + "SuccessCallback handle() Discovered a Video Player Commissioner: " + + discoveredNodeData); new Handler(Looper.getMainLooper()) .post( () -> { @@ -129,7 +134,10 @@ public void handle(DiscoveredNodeData discoveredNodeData) { new FailureCallback() { @Override public void handle(MatterError matterError) { - Log.e(TAG, "Error occurred during video player commissioner discovery: " + matterError); + Log.e( + TAG, + "FailureCallback handle() Error occurred during video player commissioner discovery: " + + matterError); if (MatterError.DISCOVERY_SERVICE_LOST == matterError) { Log.d(TAG, "Attempting to restart service"); tvCastingApp.discoverVideoPlayerCommissioners(successCallback, this); @@ -148,7 +156,7 @@ public void handle(MatterError matterError) { @Override public void onResume() { super.onResume(); - Log.d(TAG, "Auto discovering"); + Log.d(TAG, "onResume() called. Auto discovering"); poller = executor.scheduleAtFixedRate( diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ConnectionFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ConnectionFragment.java index d2a0f7952b6485..09ba31b2c8776c 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ConnectionFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ConnectionFragment.java @@ -8,6 +8,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.R; import com.chip.casting.CommissioningCallbacks; import com.chip.casting.ContentApp; import com.chip.casting.DiscoveredNodeData; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ContentLauncherFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ContentLauncherFragment.java index 2c3db2bc01f6d2..d1c3c9f957f394 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ContentLauncherFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ContentLauncherFragment.java @@ -9,6 +9,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.R; import com.chip.casting.ContentApp; import com.chip.casting.MatterCallbackHandler; import com.chip.casting.MatterError; 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 de1b547f929dd5..c0b1bcb5a860df 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 @@ -5,18 +5,22 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; +import com.R; import com.chip.casting.AppParameters; import com.chip.casting.DiscoveredNodeData; import com.chip.casting.TvCastingApp; import com.chip.casting.util.GlobalCastingConstants; import com.chip.casting.util.PreferencesConfigurationManager; +import com.matter.casting.DiscoveryExampleFragment; import com.matter.casting.InitializationExample; +import com.matter.casting.core.CastingPlayer; import java.util.Random; public class MainActivity extends AppCompatActivity implements CommissionerDiscoveryFragment.Callback, ConnectionFragment.Callback, - SelectClusterFragment.Callback { + SelectClusterFragment.Callback, + DiscoveryExampleFragment.Callback { private static final String TAG = MainActivity.class.getSimpleName(); @@ -36,7 +40,12 @@ protected void onCreate(Bundle savedInstanceState) { return; } - Fragment fragment = CommissionerDiscoveryFragment.newInstance(tvCastingApp); + Fragment fragment = null; + if (GlobalCastingConstants.ChipCastingSimplified) { + fragment = DiscoveryExampleFragment.newInstance(); + } else { + fragment = CommissionerDiscoveryFragment.newInstance(tvCastingApp); + } getSupportFragmentManager() .beginTransaction() .add(R.id.main_fragment_container, fragment, fragment.getClass().getSimpleName()) @@ -48,6 +57,13 @@ public void handleCommissioningButtonClicked(DiscoveredNodeData commissioner) { showFragment(ConnectionFragment.newInstance(tvCastingApp, commissioner)); } + @Override + public void handleConnectionButtonClicked(CastingPlayer player) { + Log.i(TAG, "MainActivity.handleConnectionButtonClicked() called"); + // TODO: In future PR, show fragment that connects to the player. + // showFragment(ConnectionFragment.newInstance(CastingPlayer player)); + } + @Override public void handleCommissioningComplete() { showFragment(SelectClusterFragment.newInstance(tvCastingApp)); diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MediaPlaybackFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MediaPlaybackFragment.java index 325b33e85b79f4..a9c38443a7d91e 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MediaPlaybackFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MediaPlaybackFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import com.R; import com.chip.casting.ContentApp; import com.chip.casting.FailureCallback; import com.chip.casting.MatterError; diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/SelectClusterFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/SelectClusterFragment.java index 7f5277aeb81138..f89fc36009e404 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/SelectClusterFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/SelectClusterFragment.java @@ -7,6 +7,7 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.R; import com.chip.casting.TvCastingApp; /** An interstitial {@link Fragment} to select one of the supported media actions to perform */ 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 new file mode 100644 index 00000000000000..4c5d68fdb60654 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.matter.casting; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.R; +import com.matter.casting.core.CastingPlayer; +import com.matter.casting.core.CastingPlayerDiscovery; +import com.matter.casting.core.MatterCastingPlayerDiscovery; +import com.matter.casting.support.MatterError; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; + +public class DiscoveryExampleFragment extends Fragment { + private static final String TAG = DiscoveryExampleFragment.class.getSimpleName(); + // 35 represents device type of Matter Casting Player + private static final Long DISCOVERY_TARGET_DEVICE_TYPE = 35L; + private static final int DISCOVERY_RUNTIME_SEC = 15; + private TextView matterDiscoveryMessageTextView; + private static final ScheduledExecutorService executorService = + Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture scheduledFutureTask; + private static final List castingPlayerList = new ArrayList<>(); + private static ArrayAdapter arrayAdapter; + + // Get a singleton instance of the MatterCastingPlayerDiscovery + private static final CastingPlayerDiscovery matterCastingPlayerDiscovery = + MatterCastingPlayerDiscovery.getInstance(); + + /** + * Implementation of a CastingPlayerChangeListener used to listen to changes in the discovered + * CastingPlayers + */ + private static final CastingPlayerDiscovery.CastingPlayerChangeListener + castingPlayerChangeListener = + new CastingPlayerDiscovery.CastingPlayerChangeListener() { + private final String TAG = + CastingPlayerDiscovery.CastingPlayerChangeListener.class.getSimpleName(); + + @Override + public void onAdded(CastingPlayer castingPlayer) { + Log.i( + TAG, + "onAdded() Discovered CastingPlayer deviceId: " + castingPlayer.getDeviceId()); + // Display CastingPlayer info on the screen + new Handler(Looper.getMainLooper()) + .post( + () -> { + arrayAdapter.add(castingPlayer); + }); + } + + @Override + public void onChanged(CastingPlayer castingPlayer) { + Log.i( + TAG, + "onChanged() Discovered changes to CastingPlayer with deviceId: " + + castingPlayer.getDeviceId()); + // Update the CastingPlayer on the screen + new Handler(Looper.getMainLooper()) + .post( + () -> { + final Optional playerInList = + castingPlayerList + .stream() + .filter(node -> castingPlayer.equals(node)) + .findFirst(); + if (playerInList.isPresent()) { + Log.d( + TAG, + "onChanged() Updating existing CastingPlayer entry " + + playerInList.get().getDeviceId() + + " in castingPlayerList list"); + arrayAdapter.remove(playerInList.get()); + } + arrayAdapter.add(castingPlayer); + }); + } + + @Override + public void onRemoved(CastingPlayer castingPlayer) { + Log.i( + TAG, + "onRemoved() Removed CastingPlayer with deviceId: " + + castingPlayer.getDeviceId()); + // Remove CastingPlayer from the screen + new Handler(Looper.getMainLooper()) + .post( + () -> { + final Optional playerInList = + castingPlayerList + .stream() + .filter(node -> castingPlayer.equals(node)) + .findFirst(); + if (playerInList.isPresent()) { + Log.d( + TAG, + "onRemoved() Removing existing CastingPlayer entry " + + playerInList.get().getDeviceId() + + " in castingPlayerList list"); + arrayAdapter.remove(playerInList.get()); + } + }); + } + }; + + public static DiscoveryExampleFragment newInstance() { + Log.i(TAG, "newInstance() called"); + return new DiscoveryExampleFragment(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.i(TAG, "onCreate() called"); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Log.i(TAG, "onCreateView() called"); + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_matter_discovery_example, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Log.i(TAG, "onViewCreated() called"); + + matterDiscoveryMessageTextView = + getActivity().findViewById(R.id.matterDiscoveryMessageTextView); + matterDiscoveryMessageTextView.setText( + getString(R.string.matter_discovery_message_initializing_text)); + + arrayAdapter = new CastingPlayerArrayAdapter(getActivity(), castingPlayerList); + final ListView list = getActivity().findViewById(R.id.castingPlayerList); + list.setAdapter(arrayAdapter); + + Log.d(TAG, "onViewCreated() creating callbacks"); + + // TODO: In following PRs. Enable startDiscoveryButton and stopDiscoveryButton when + // stopDiscovery is implemented in the core Matter SDK DNS-SD API. Enable in + // fragment_matter_discovery_example.xml + Button startDiscoveryButton = getView().findViewById(R.id.startDiscoveryButton); + startDiscoveryButton.setOnClickListener( + v -> { + Log.i( + TAG, "onViewCreated() startDiscoveryButton button clicked. Calling startDiscovery()"); + if (!startDiscovery()) { + Log.e(TAG, "onViewCreated() startDiscovery() call Failed"); + } + }); + + Button stopDiscoveryButton = getView().findViewById(R.id.stopDiscoveryButton); + stopDiscoveryButton.setOnClickListener( + v -> { + Log.i(TAG, "onViewCreated() stopDiscoveryButton button clicked. Calling stopDiscovery()"); + stopDiscovery(); + Log.i(TAG, "onViewCreated() stopDiscoveryButton button clicked. Canceling future task"); + scheduledFutureTask.cancel(true); + }); + + Button clearDiscoveryResultsButton = getView().findViewById(R.id.clearDiscoveryResultsButton); + clearDiscoveryResultsButton.setOnClickListener( + v -> { + Log.i( + TAG, "onViewCreated() clearDiscoveryResultsButton button clicked. Clearing results"); + arrayAdapter.clear(); + }); + } + + @Override + public void onResume() { + super.onResume(); + Log.i(TAG, "onResume() called. Calling startDiscovery()"); + if (!startDiscovery()) { + Log.e(TAG, "onResume() Warning: startDiscovery() call Failed"); + } + } + + @Override + public void onPause() { + super.onPause(); + Log.i(TAG, "onPause() called"); + // stopDiscovery(); + // Don't crash the app + if (scheduledFutureTask != null) { + scheduledFutureTask.cancel(true); + } + } + + /** 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); + } + + private boolean startDiscovery() { + Log.i(TAG, "startDiscovery() called"); + + arrayAdapter.clear(); + + // Add the implemented CastingPlayerChangeListener to listen to changes in the discovered + // CastingPlayers + MatterError err = + matterCastingPlayerDiscovery.addCastingPlayerChangeListener(castingPlayerChangeListener); + if (err.hasError()) { + Log.e(TAG, "startDiscovery() addCastingPlayerChangeListener() called, err Add: " + err); + return false; + } + // Start discovery + Log.i(TAG, "startDiscovery() calling CastingPlayerDiscovery.startDiscovery()"); + err = matterCastingPlayerDiscovery.startDiscovery(DISCOVERY_TARGET_DEVICE_TYPE); + if (err.hasError()) { + Log.e(TAG, "startDiscovery() startDiscovery() called, err Start: " + err); + return false; + } + + Log.i(TAG, "startDiscovery() started discovery"); + + matterDiscoveryMessageTextView.setText( + getString(R.string.matter_discovery_message_discovering_text)); + Log.d( + TAG, + "startDiscovery() text set to: " + + getString(R.string.matter_discovery_message_discovering_text)); + + // TODO: In following PRs. Enable this to auto-stop discovery after stopDiscovery is + // implemented in the core Matter SKD DNS-SD API. + // Schedule a service to stop discovery and remove the CastingPlayerChangeListener + // Safe to call if discovery is not running + // scheduledFutureTask = + // executorService.schedule( + // () -> { + // Log.i( + // TAG, + // "startDiscovery() executorService " + // + DISCOVERY_RUNTIME_SEC + // + " seconds timer expired. Auto-calling stopDiscovery()"); + // stopDiscovery(); + // }, + // DISCOVERY_RUNTIME_SEC, + // TimeUnit.SECONDS); + + return true; + } + + private void stopDiscovery() { + Log.i(TAG, "stopDiscovery() called"); + + // Stop discovery + MatterError err = matterCastingPlayerDiscovery.stopDiscovery(); + if (err.hasError()) { + Log.e( + TAG, + "stopDiscovery() MatterCastingPlayerDiscovery.stopDiscovery() called, err Stop: " + err); + } else { + // TODO: In following PRs. Implement stop discovery in the Android core API. + Log.d(TAG, "stopDiscovery() MatterCastingPlayerDiscovery.stopDiscovery() success"); + } + + matterDiscoveryMessageTextView.setText( + getString(R.string.matter_discovery_message_stopped_text)); + Log.d( + TAG, + "stopDiscovery() text set to: " + + getString(R.string.matter_discovery_message_stopped_text)); + + // Remove the CastingPlayerChangeListener + Log.i(TAG, "stopDiscovery() removing CastingPlayerChangeListener"); + err = + matterCastingPlayerDiscovery.removeCastingPlayerChangeListener(castingPlayerChangeListener); + if (err.hasError()) { + Log.e( + TAG, + "stopDiscovery() matterCastingPlayerDiscovery.removeCastingPlayerChangeListener() called, err Remove: " + + err); + } + } +} + +class CastingPlayerArrayAdapter extends ArrayAdapter { + private final List playerList; + private final Context context; + private LayoutInflater inflater; + private static final String TAG = CastingPlayerArrayAdapter.class.getSimpleName(); + + public CastingPlayerArrayAdapter(Context context, List playerList) { + super(context, 0, playerList); + Log.i(TAG, "CastingPlayerArrayAdapter() constructor called"); + this.context = context; + this.playerList = playerList; + inflater = (LayoutInflater.from(context)); + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + view = inflater.inflate(R.layout.commissionable_player_list_item, null); + String buttonText = getCastingPlayerButtonText(playerList.get(i)); + Button playerDescription = view.findViewById(R.id.commissionable_player_description); + playerDescription.setText(buttonText); + + View.OnClickListener clickListener = + v -> { + CastingPlayer castingPlayer = playerList.get(i); + Log.d( + TAG, + "OnItemClickListener.onClick() called for castingPlayer with deviceId: " + + castingPlayer.getDeviceId()); + DiscoveryExampleFragment.Callback callback1 = (DiscoveryExampleFragment.Callback) context; + // TODO: In following PRs. Implement CastingPlayer connection + // callback1.handleCommissioningButtonClicked(castingPlayer); + }; + playerDescription.setOnClickListener(clickListener); + return view; + } + + private String getCastingPlayerButtonText(CastingPlayer player) { + String main = player.getDeviceName() != null ? player.getDeviceName() : ""; + String aux = "" + (player.getDeviceId() != null ? "Device ID: " + player.getDeviceId() : ""); + aux += + player.getProductId() > 0 + ? (aux.isEmpty() ? "" : ", ") + "Product ID: " + player.getProductId() + : ""; + aux += + player.getVendorId() > 0 + ? (aux.isEmpty() ? "" : ", ") + "Vendor ID: " + player.getVendorId() + : ""; + aux += + player.getDeviceType() > 0 + ? (aux.isEmpty() ? "" : ", ") + "Device Type: " + player.getDeviceType() + : ""; + aux += (aux.isEmpty() ? "" : ", ") + "Resolved IP?: " + (player.getIpAddresses().size() > 0); + + aux = aux.isEmpty() ? aux : "\n" + aux; + return main + aux; + } +} 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 new file mode 100644 index 00000000000000..71ea4767a03994 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 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.core; + +import java.net.InetAddress; +import java.util.List; + +/** + * The CastingPlayer interface defines a Matter commissioner that is able to play media to a + * physical output or to a display screen which is part of the device (e.g. TV). It is discovered on + * the local network using Matter Commissioner discovery over DNS. It contains all the information + * about the service discovered/resolved. + */ +public interface CastingPlayer { + boolean isConnected(); + + String getDeviceId(); + + String getHostName(); + + String getDeviceName(); + + String getInstanceName(); + + List getIpAddresses(); + + int getPort(); + + int getVendorId(); + + int getProductId(); + + long getDeviceType(); + + @Override + String toString(); + + @Override + boolean equals(Object o); + + @Override + int hashCode(); + + // TODO: Implement in following PRs. Related to player connection implementation. + // List getEndpoints(); + // + // ConnectionState getConnectionState(); + // + // CompletableFuture connect(long timeout); + // + // static class ConnectionState extends Observable { + // private boolean connected; + // + // void setConnected(boolean connected) { + // this.connected = connected; + // setChanged(); + // notifyObservers(this.connected); + // } + // + // boolean isConnected() { + // return connected; + // } + // } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayerDiscovery.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayerDiscovery.java new file mode 100644 index 00000000000000..fcde7bc55adbe0 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayerDiscovery.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 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.core; + +import android.util.Log; +import com.matter.casting.support.MatterError; +import java.util.List; + +/** + * The CastingPlayerDiscovery interface defines the client API to control Matter Casting Player + * discovery over DNS-SD, and to collect discovery results. This interface defines the methods to + * add and remove a CastingPlayerChangeListener. It also defines the CastingPlayerChangeListener + * handler class which must be implemented by the API client. The handler contains the methods + * called when Casting Players are discovered, updated, or lost from the network. + */ +public interface CastingPlayerDiscovery { + + /** + * @return a list of Casting Players discovered during the current discovery session. This list is + * cleared when discovery stops. + */ + List getCastingPlayers(); + + /** + * Starts Casting Players discovery or returns an error. + * + * @param discoveryTargetDeviceType the target device type to be discovered using DNS-SD. For + * example: 35 represents device type of Matter Casting Video Player. If "null" is passed in, + * discovery will default to all "_matterd._udp" device types. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + MatterError startDiscovery(Long discoveryTargetDeviceType); + + /** + * Stops Casting Players discovery or returns an error. + * + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + MatterError stopDiscovery(); + + /** + * Adds a CastingPlayerChangeListener instance to be used during discovery. The + * CastingPlayerChangeListener defines the handler methods for when Casting Players are + * discovered, updated, or lost from the network. Should be called prior to calling + * MatterCastingPlayerDiscovery.startDiscovery(). + * + * @param listener an instance of the CastingPlayerChangeListener to be implemented by the APIs + * consumer. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + MatterError addCastingPlayerChangeListener(CastingPlayerChangeListener listener); + + /** + * Removes CastingPlayerChangeListener added by addCastingPlayerChangeListener(). + * + * @param listener the specific instance of CastingPlayerChangeListener to be removed. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + MatterError removeCastingPlayerChangeListener(CastingPlayerChangeListener listener); + + /** + * The CastingPlayerChangeListener can discover CastingPlayers by implementing the onAdded(), + * onChanged() and onRemoved() callbacks which are called as CastingPlayers, are discovered, + * updated, or lost from the network. The onAdded(), onChanged() and onRemoved() handlers must be + * implemented by the API client. + */ + abstract class CastingPlayerChangeListener { + static final String TAG = CastingPlayerChangeListener.class.getSimpleName(); + + /** + * This handler is called when a Casting Player is added to the network. + * + * @param castingPlayer the Casting Player added. + */ + public abstract void onAdded(CastingPlayer castingPlayer); + + /** + * This handler is called when a Casting Player previously detected on the network is changed. + * + * @param castingPlayer the Casting Player changed. + */ + public abstract void onChanged(CastingPlayer castingPlayer); + + /** + * This handler is called when a Casting Player previously detected on the network is removed. + * + * @param castingPlayer the Casting Player removed. + */ + public abstract void onRemoved(CastingPlayer castingPlayer); + + /** + * The following methods are used to catch possible exceptions thrown by the methods above + * (onAdded(), onChanged() and onRemoved()), when not implemented correctly by the client. + */ + protected final void _onAdded(CastingPlayer castingPlayer) { + try { + onAdded(castingPlayer); + } catch (Throwable t) { + Log.e(TAG, "_onAdded() Caught an unhandled Throwable from the client: " + t); + } + }; + + protected final void _onChanged(CastingPlayer castingPlayer) { + try { + onChanged(castingPlayer); + } catch (Throwable t) { + Log.e(TAG, "_onChanged() Caught an unhandled Throwable from the client: " + t); + } + }; + + protected final void _onRemoved(CastingPlayer castingPlayer) { + try { + onRemoved(castingPlayer); + } catch (Throwable t) { + Log.e(TAG, "_onRemoved() Caught an unhandled Throwable from the client: " + t); + } + }; + } +} 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 new file mode 100644 index 00000000000000..ecc76a575a115b --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 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.core; + +import java.net.InetAddress; +import java.util.List; +import java.util.Objects; + +/** + * A Matter Casting Player represents a Matter commissioner that is able to play media to a physical + * output or to a display screen which is part of the device (e.g. TV). It is discovered on the + * local network using Matter Commissioner discovery over DNS. It contains all the information about + * the service discovered/resolved. + */ +public class MatterCastingPlayer implements CastingPlayer { + private boolean connected; + private String deviceId; + private String deviceName; + private String hostName; + private String instanceName; + private List ipAddresses; + private int port; + private int productId; + private int vendorId; + private long deviceType; + + public MatterCastingPlayer( + boolean connected, + String deviceId, + String hostName, + String deviceName, + String instanceName, + List ipAddresses, + int port, + int productId, + int vendorId, + long deviceType) { + this.connected = connected; + this.deviceId = deviceId; + this.hostName = hostName; + this.deviceName = deviceName; + this.instanceName = instanceName; + this.ipAddresses = ipAddresses; + this.port = port; + this.productId = productId; + this.vendorId = vendorId; + this.deviceType = deviceType; + } + + /** + * @return a boolean indicating whether a Casting Player instance is connected to the TV Casting + * App. + */ + @Override + public boolean isConnected() { + return this.connected; + } + + /** + * @return a String representing the Casting Player device ID which is a concatenation of the + * device's IP address and port number. + */ + @Override + public String getDeviceId() { + return this.deviceId; + } + + @Override + public String getHostName() { + return this.hostName; + } + + @Override + public String getDeviceName() { + return this.deviceName; + } + + @Override + public String getInstanceName() { + return this.instanceName; + } + + /** @return a list of valid IP addresses for this Casting PLayer. */ + @Override + public List getIpAddresses() { + return this.ipAddresses; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public int getVendorId() { + return this.vendorId; + } + + @Override + public int getProductId() { + return this.productId; + } + + @Override + public long getDeviceType() { + return this.deviceType; + } + + @Override + public String toString() { + return this.deviceId; + } + + @Override + public int hashCode() { + return this.deviceId.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MatterCastingPlayer that = (MatterCastingPlayer) o; + return Objects.equals(this.deviceId, that.deviceId); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayerDiscovery.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayerDiscovery.java new file mode 100644 index 00000000000000..bf7df60ef74d79 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayerDiscovery.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 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.core; + +import com.matter.casting.support.MatterError; +import java.util.List; + +/** + * MatterCastingPlayerDiscovery provides an API to control Matter Casting Player discovery over + * DNS-SD, and to collect discovery results. This class provides methods to add and remove a + * CastingPlayerChangeListener, which contains the handlers for when Casting Players are discovered, + * updated, or lost from the network. This class is a singleton. + */ +public final class MatterCastingPlayerDiscovery implements CastingPlayerDiscovery { + private static final String TAG = MatterCastingPlayerDiscovery.class.getSimpleName(); + private static MatterCastingPlayerDiscovery matterCastingPlayerDiscoveryInstance; + + // Methods: + public static MatterCastingPlayerDiscovery getInstance() { + if (matterCastingPlayerDiscoveryInstance == null) { + matterCastingPlayerDiscoveryInstance = new MatterCastingPlayerDiscovery(); + } + return matterCastingPlayerDiscoveryInstance; + }; + + /** + * @return a list of Casting Players discovered during the current discovery session. This list is + * cleared when discovery stops. + */ + @Override + public native List getCastingPlayers(); + + /** + * Starts Casting Players discovery or returns an error. + * + * @param discoveryTargetDeviceType the target device type to be discovered using DNS-SD. For + * example: 35 represents device type of Matter Casting Video Player. If "null" is passed in, + * discovery will default to all "_matterd._udp" device types. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + @Override + public native MatterError startDiscovery(Long discoveryTargetDeviceType); + + /** + * Stops Casting Players discovery or returns an error. + * + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + @Override + public native MatterError stopDiscovery(); + + /** + * Adds a CastingPlayerChangeListener instance to be used during discovery. The + * CastingPlayerChangeListener defines the handler methods for when Casting Players are + * discovered, updated, or lost from the network. Should be called prior to calling + * MatterCastingPlayerDiscovery.startDiscovery(). + * + * @param listener an instance of the CastingPlayerChangeListener to be implemented by the APIs + * consumer. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + @Override + public native MatterError addCastingPlayerChangeListener(CastingPlayerChangeListener listener); + + /** + * Removes CastingPlayerChangeListener from the native layer. + * + * @param listener the specific instance of CastingPlayerChangeListener to be removed. + * @return a specific MatterError if the the operation failed or NO_ERROR if succeeded. + */ + @Override + public native MatterError removeCastingPlayerChangeListener(CastingPlayerChangeListener listener); +} 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 90a36bbfec92ca..be8a35a195403a 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 @@ -47,7 +47,7 @@ jobject extractJAppParameter(jobject jAppParameters, const char * methodName, co JNI_METHOD(jobject, finishInitialization)(JNIEnv *, jobject, jobject jAppParameters) { chip::DeviceLayer::StackLock lock; - ChipLogProgress(AppServer, "JNI_METHOD CastingAppJNI.finishInitialization called"); + ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::finishInitialization() called"); VerifyOrReturnValue(jAppParameters != nullptr, support::createJMatterError(CHIP_ERROR_INVALID_ARGUMENT)); CHIP_ERROR err = CHIP_NO_ERROR; @@ -81,7 +81,7 @@ JNI_METHOD(jobject, finishInitialization)(JNIEnv *, jobject, jobject jAppParamet JNI_METHOD(jobject, finishStartup)(JNIEnv *, jobject) { chip::DeviceLayer::StackLock lock; - ChipLogProgress(AppServer, "JNI_METHOD CastingAppJNI.finishStartup called"); + ChipLogProgress(AppServer, "JNI_METHOD CastingAppJNI::finishStartup() called"); CHIP_ERROR err = CHIP_NO_ERROR; auto & server = chip::Server::GetInstance(); @@ -107,7 +107,7 @@ JNI_METHOD(jobject, finishStartup)(JNIEnv *, jobject) jobject extractJAppParameter(jobject jAppParameters, const char * methodName, const char * methodSig) { - ChipLogProgress(AppServer, "JNI_METHOD extractJAppParameter called"); + ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::extractJAppParameter() called"); JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); jclass jAppParametersClass; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp new file mode 100644 index 00000000000000..255ba570082e16 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2023 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. + * + */ + +#include "CastingPlayerDiscovery-JNI.h" + +#include "../JNIDACProvider.h" +#include "../support/CastingPlayerConverter-JNI.h" +#include "../support/ErrorConverter-JNI.h" +#include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" +#include "core/CastingApp.h" // from tv-casting-common +#include "core/CastingPlayerDiscovery.h" // from tv-casting-common + +#include +#include +#include +#include +#include + +using namespace chip; + +#define JNI_METHOD(RETURN, METHOD_NAME) \ + extern "C" JNIEXPORT RETURN JNICALL Java_com_matter_casting_core_MatterCastingPlayerDiscovery_##METHOD_NAME + +namespace matter { +namespace casting { +namespace core { + +/** + * @brief React to CastingPlayer discovery results with this singleton + */ +class DiscoveryDelegateImpl : public DiscoveryDelegate +{ +private: + DiscoveryDelegateImpl() {} + static DiscoveryDelegateImpl * discoveryDelegateImplSingletonInstance; + DiscoveryDelegateImpl(DiscoveryDelegateImpl & other) = delete; + void operator=(const DiscoveryDelegateImpl &) = delete; + +public: + jobject castingPlayerChangeListenerJavaObject = nullptr; + jmethodID onAddedCallbackJavaMethodID = nullptr; + jmethodID onChangedCallbackJavaMethodID = nullptr; + // jmethodID onRemovedCallbackJavaMethodID = nullptr; + + static DiscoveryDelegateImpl * GetInstance() + { + if (DiscoveryDelegateImpl::discoveryDelegateImplSingletonInstance == nullptr) + { + DiscoveryDelegateImpl::discoveryDelegateImplSingletonInstance = new DiscoveryDelegateImpl(); + } + return DiscoveryDelegateImpl::discoveryDelegateImplSingletonInstance; + } + + void HandleOnAdded(matter::casting::memory::Strong player) override + { + ChipLogProgress(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() called with CastingPlayer, ID: %s", + player->GetId()); + + VerifyOrReturn(castingPlayerChangeListenerJavaObject != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Not set, " + "CastingPlayerChangeListener == nullptr")); + VerifyOrReturn(onAddedCallbackJavaMethodID != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Not set, " + "onAddedCallbackJavaMethodID == nullptr")); + + jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + VerifyOrReturn(matterCastingPlayerJavaObject != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Could not create " + "CastingPlayer jobject")); + + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + env->CallVoidMethod(castingPlayerChangeListenerJavaObject, onAddedCallbackJavaMethodID, matterCastingPlayerJavaObject); + } + + void HandleOnUpdated(matter::casting::memory::Strong player) override + { + ChipLogProgress(AppServer, + "CastingPlayerDiscovery-JNI DiscoveryDelegateImpl::HandleOnUpdated() called with CastingPlayer, ID: %s", + player->GetId()); + + VerifyOrReturn(castingPlayerChangeListenerJavaObject != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Not set, " + "CastingPlayerChangeListener == nullptr")); + VerifyOrReturn(onChangedCallbackJavaMethodID != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Not set, " + "onChangedCallbackJavaMethodID == nullptr")); + + jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + VerifyOrReturn(matterCastingPlayerJavaObject != nullptr, + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Could not " + "create CastingPlayer jobject")); + + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + env->CallVoidMethod(castingPlayerChangeListenerJavaObject, onChangedCallbackJavaMethodID, matterCastingPlayerJavaObject); + } + + // TODO: In following PRs. Implement HandleOnRemoved after implemented in tv-casting-commom CastingPlayerDiscovery.h/cpp + // void HandleOnRemoved(matter::casting::memory::Strong player) override + // { + // ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI DiscoveryDelegateImpl::HandleOnRemoved() called with + // CastingPlayer, ID: %s", player->GetId()); + // } +}; + +// Initialize the static instance to nullptr +DiscoveryDelegateImpl * DiscoveryDelegateImpl::discoveryDelegateImplSingletonInstance = nullptr; + +JNI_METHOD(jobject, startDiscovery)(JNIEnv * env, jobject, jobject targetDeviceTypeLong = nullptr) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::startDiscovery() called"); + CastingPlayerDiscovery::GetInstance()->SetDelegate(DiscoveryDelegateImpl::GetInstance()); + + // Start CastingPlayer discovery + CHIP_ERROR err = CHIP_NO_ERROR; + if (targetDeviceTypeLong == nullptr) + { + ChipLogProgress(AppServer, + "CastingPlayerDiscovery-JNI::startDiscovery() received null target device type. Using default type."); + err = CastingPlayerDiscovery::GetInstance()->StartDiscovery(); + } + else + { + // Get the long value from the Java Long object + jclass longClass = env->GetObjectClass(targetDeviceTypeLong); + jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J"); + jlong jTargetDeviceType = env->CallLongMethod(targetDeviceTypeLong, longValueMethod); + env->DeleteLocalRef(longClass); + + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::startDiscovery() discovery target device type: %u", + static_cast(jTargetDeviceType)); + err = CastingPlayerDiscovery::GetInstance()->StartDiscovery(static_cast(jTargetDeviceType)); + } + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "CastingPlayerDiscovery-JNI startDiscovery() err: %" CHIP_ERROR_FORMAT, err.Format()); + return support::createJMatterError(err); + } + + return support::createJMatterError(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, stopDiscovery)(JNIEnv * env, jobject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::stopDiscovery() called"); + + // Stop CastingPlayer discovery + CHIP_ERROR err = CastingPlayerDiscovery::GetInstance()->StopDiscovery(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::StopDiscovery() err: %" CHIP_ERROR_FORMAT, err.Format()); + return support::createJMatterError(err); + } + + return support::createJMatterError(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, addCastingPlayerChangeListener)(JNIEnv * env, jobject, jobject castingPlayerChangeListenerJavaObject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() called"); + VerifyOrReturnValue(castingPlayerChangeListenerJavaObject != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + + if (DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject != nullptr) + { + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() Warning: Call removeCastingPlayerChangeListener " + "before adding a new one"); + return support::createJMatterError(CHIP_ERROR_INCORRECT_STATE); + } + + // Get the class and method IDs for the CastingPlayerChangeListener Java class + jclass castingPlayerChangeListenerJavaClass = env->GetObjectClass(castingPlayerChangeListenerJavaObject); + VerifyOrReturnValue(castingPlayerChangeListenerJavaClass != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + + jmethodID onAddedJavaMethodID = + env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onAdded", "(Lcom/matter/casting/core/CastingPlayer;)V"); + VerifyOrReturnValue(onAddedJavaMethodID != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + jmethodID onChangedJavaMethodID = + env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onChanged", "(Lcom/matter/casting/core/CastingPlayer;)V"); + VerifyOrReturnValue(onChangedJavaMethodID != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + // jmethodID onRemovedJavaMethodID = env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onRemoved", + // "(Lcom/matter/casting/core/CastingPlayer;)V"); VerifyOrReturnValue(onRemovedJavaMethodID != nullptr, + // support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + + // Set Java callbacks in the DiscoveryDelegateImpl Singleton + DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject = + env->NewGlobalRef(castingPlayerChangeListenerJavaObject); + DiscoveryDelegateImpl::GetInstance()->onAddedCallbackJavaMethodID = onAddedJavaMethodID; + DiscoveryDelegateImpl::GetInstance()->onChangedCallbackJavaMethodID = onChangedJavaMethodID; + // DiscoveryDelegateImpl::GetInstance()->onRemovedCallbackJavaMethodID = onRemovedJavaMethodID; + + return support::createJMatterError(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, removeCastingPlayerChangeListener)(JNIEnv * env, jobject, jobject castingPlayerChangeListenerJavaObject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() called"); + + // Check if the passed object is the same as the one added in addCastingPlayerChangeListener() JNI method + jboolean isSameObject = env->IsSameObject(castingPlayerChangeListenerJavaObject, + DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject); + + if ((bool) isSameObject) + { + // Delete the global reference to the Java object + env->DeleteGlobalRef(DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject); + // Remove the Java callbacks in the DiscoveryDelegateImpl Singleton + DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject = nullptr; + // No explicit cleanup required + DiscoveryDelegateImpl::GetInstance()->onAddedCallbackJavaMethodID = nullptr; + DiscoveryDelegateImpl::GetInstance()->onChangedCallbackJavaMethodID = nullptr; + // DiscoveryDelegateImpl::GetInstance()->onRemovedCallbackJavaMethodID = nullptr; + + return support::createJMatterError(CHIP_NO_ERROR); + } + else + { + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() Warning: Cannot remove listener. Received a " + "different CastingPlayerChangeListener object"); + return support::createJMatterError(CHIP_ERROR_INCORRECT_STATE); + } +} + +JNI_METHOD(jobject, getCastingPlayers)(JNIEnv * env, jobject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::getCastingPlayers() called"); + + // Create a new ArrayList + jclass arrayListClass = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "", "()V"); + jobject arrayList = env->NewObject(arrayListClass, arrayListConstructor); + jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + + std::vector> castingPlayersList = CastingPlayerDiscovery::GetInstance()->GetCastingPlayers(); + + for (const auto & player : castingPlayersList) + { + jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + if (matterCastingPlayerJavaObject != nullptr) + { + jboolean added = env->CallBooleanMethod(arrayList, addMethod, matterCastingPlayerJavaObject); + if (!((bool) added)) + { + ChipLogError(AppServer, + "CastingPlayerDiscovery-JNI::getCastingPlayers() Warning: Unable to add CastingPlayer with ID: %s", + player->GetId()); + } + } + } + + return arrayList; +} + +}; // namespace core +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.h new file mode 100644 index 00000000000000..94fb568b813960 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.h @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2023 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. + */ + +#pragma once + +#include + +namespace matter { +namespace casting { +namespace core { + +class CastingPlayerDiscoveryJNI +{ +public: +private: + friend CastingPlayerDiscoveryJNI & CastingAppJNIMgr(); + static CastingPlayerDiscoveryJNI sInstance; +}; + +inline class CastingPlayerDiscoveryJNI & CastingAppJNIMgr() +{ + return CastingPlayerDiscoveryJNI::sInstance; +} +}; // namespace core +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp new file mode 100644 index 00000000000000..a993a501bd6aa0 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 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. + * + */ + +#include "CastingPlayerConverter-JNI.h" +#include + +namespace matter { +namespace casting { +namespace support { + +using namespace chip; + +jobject createJCastingPlayer(matter::casting::memory::Strong player) +{ + ChipLogProgress(AppServer, "CastingPlayerConverter-JNI.createJCastingPlayer() called"); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + + // Get a reference to the MatterCastingPlayer Java class + jclass matterCastingPlayerJavaClass = env->FindClass("com/matter/casting/core/MatterCastingPlayer"); + if (matterCastingPlayerJavaClass == nullptr) + { + ChipLogError(AppServer, + "CastingPlayerConverter-JNI.createJCastingPlayer() could not locate MatterCastingPlayer Java class"); + return nullptr; + } + + // Get the constructor for the com/matter/casting/core/MatterCastingPlayer Java class + jmethodID constructor = + env->GetMethodID(matterCastingPlayerJavaClass, "", + "(ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;IIIJ)V"); + if (constructor == nullptr) + { + ChipLogError( + AppServer, + "CastingPlayerConverter-JNI.createJCastingPlayer() could not locate MatterCastingPlayer Java class constructor"); + return nullptr; + } + + // Convert the CastingPlayer fields to MatterCastingPlayer Java types + jobject jIpAddressList = nullptr; + const chip::Inet::IPAddress * ipAddresses = player->GetIPAddresses(); + if (ipAddresses != nullptr) + { + chip::JniReferences::GetInstance().CreateArrayList(jIpAddressList); + for (size_t i = 0; i < player->GetNumIPs() && i < chip::Dnssd::CommonResolutionData::kMaxIPAddresses; i++) + { + char addrCString[chip::Inet::IPAddress::kMaxStringLength]; + ipAddresses[i].ToString(addrCString, chip::Inet::IPAddress::kMaxStringLength); + jstring jIPAddressStr = env->NewStringUTF(addrCString); + + jclass jIPAddressClass = env->FindClass("java/net/InetAddress"); + jmethodID jGetByNameMid = + env->GetStaticMethodID(jIPAddressClass, "getByName", "(Ljava/lang/String;)Ljava/net/InetAddress;"); + jobject jIPAddress = env->CallStaticObjectMethod(jIPAddressClass, jGetByNameMid, jIPAddressStr); + + chip::JniReferences::GetInstance().AddToList(jIpAddressList, jIPAddress); + } + } + + // Create a new instance of the MatterCastingPlayer Java class + jobject jMatterCastingPlayer = nullptr; + jMatterCastingPlayer = env->NewObject(matterCastingPlayerJavaClass, constructor, static_cast(player->IsConnected()), + env->NewStringUTF(player->GetId()), env->NewStringUTF(player->GetHostName()), + env->NewStringUTF(player->GetDeviceName()), env->NewStringUTF(player->GetInstanceName()), + jIpAddressList, (jint) (player->GetPort()), (jint) (player->GetProductId()), + (jint) (player->GetVendorId()), (jlong) (player->GetDeviceType())); + if (jMatterCastingPlayer == nullptr) + { + ChipLogError(AppServer, + "CastingPlayerConverter-JNI.createJCastingPlayer() Warning: Could not create MatterCastingPlayer Java object"); + } + return jMatterCastingPlayer; +} + +}; // namespace support +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h new file mode 100644 index 00000000000000..91b0ac6c79b92f --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2023-2024 Project CHIP Authors + * + * 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. + */ +#pragma once + +#include "core/CastingPlayer.h" + +#include + +#include + +namespace matter { +namespace casting { +namespace support { + +/** + * @brief Convertes a native CastingPlayer into a MatterCastingPlayer jobject + * + * @param CastingPlayer represents a Matter commissioner that is able to play media to a physical + * output or to a display screen which is part of the device. + * + * @return pointer to the CastingPlayer jobject if created successfully, nullptr otherwise. + */ +jobject createJCastingPlayer(matter::casting::memory::Strong player); + +}; // namespace support +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp index 171a44dc793f9f..9cec7897488103 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp @@ -35,7 +35,7 @@ jobject createJMatterError(CHIP_ERROR inErr) jmethodID jMatterErrorConstructor = env->GetMethodID(jMatterErrorClass, "", "(JLjava/lang/String;)V"); - return env->NewObject(jMatterErrorClass, jMatterErrorConstructor, err.AsInteger(), nullptr); + return env->NewObject(jMatterErrorClass, jMatterErrorConstructor, inErr.AsInteger(), nullptr); } }; // namespace support diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/activity_main.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/activity_main.xml index 1a8bfe110a0495..ff56873cb23c22 100644 --- a/examples/tv-casting-app/android/App/app/src/main/res/layout/activity_main.xml +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="MainActivity"> + tools:context=".chip.casting.app.MainActivity"> + tools:context=".chip.casting.app.CertTestFragment"> + tools:context=".chip.casting.app.CommissionerDiscoveryFragment"> + tools:context=".chip.casting.app.ContentLauncherFragment"> + + + + +