Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Respect updated DNS SD broadcast names
Browse files Browse the repository at this point in the history
When a device is advertising over dns-sd and the TX bits change (device
name) a message is sent out to indicate that the old adverisment should
be expired and the new one should be used.

In the current implementation of the casting app, we stop listening to
updates after the `discoveryDurationSeconds` timeout. This causes us to
miss messages and Android seems to fall back to the cached value of the
DNS advertisment.

We need to change the usage pattern here so that clients stop the
discovery manually after it is started and keep it active for the
lifetime they need it for.

I also updated the test apps to behave a bit nicer with discovered
commissioners.
cliffamzn committed Feb 27, 2023
1 parent 265d55c commit 6643b86
Showing 9 changed files with 256 additions and 150 deletions.
1 change: 0 additions & 1 deletion examples/tv-casting-app/android/App/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -4,147 +4,194 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
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.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import com.chip.casting.DiscoveredNodeData;
import com.chip.casting.FailureCallback;
import com.chip.casting.MatterError;
import com.chip.casting.SuccessCallback;
import com.chip.casting.TvCastingApp;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/** A {@link Fragment} to discover commissioners on the network */
public class CommissionerDiscoveryFragment extends Fragment {
private static final String TAG = CommissionerDiscoveryFragment.class.getSimpleName();
private static final long DISCOVERY_DURATION_SECS = 10;
private final TvCastingApp tvCastingApp;

public CommissionerDiscoveryFragment(TvCastingApp tvCastingApp) {
this.tvCastingApp = tvCastingApp;
}

/**
* Use this factory method to create a new instance of this fragment using the provided
* parameters.
*
* @return A new instance of fragment CommissionerDiscoveryFragment.
*/
public static CommissionerDiscoveryFragment newInstance(TvCastingApp tvCastingApp) {
return new CommissionerDiscoveryFragment(tvCastingApp);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_commissioner_discovery, container, false);
}

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

Button manualCommissioningButton = getView().findViewById(R.id.manualCommissioningButton);
Callback callback = (Callback) this.getActivity();
View.OnClickListener manualCommissioningButtonOnClickListener =
new View.OnClickListener() {
@Override
public void onClick(View v) {
callback.handleCommissioningButtonClicked(null);
}
};
manualCommissioningButton.setOnClickListener(manualCommissioningButtonOnClickListener);

Context context = this.getContext();
SuccessCallback<DiscoveredNodeData> successCallback =
new SuccessCallback<DiscoveredNodeData>() {
@Override
public void handle(DiscoveredNodeData discoveredNodeData) {
Log.d(TAG, "Discovered a Video Player Commissioner: " + discoveredNodeData);
String buttonText = getCommissionerButtonText(discoveredNodeData);

if (!buttonText.isEmpty()) {
Button commissionerButton = new Button(context);
commissionerButton.setText(buttonText);
CommissionerDiscoveryFragment.Callback callback =
(CommissionerDiscoveryFragment.Callback) getActivity();
commissionerButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(
TAG,
"CommissionerResolveListener.onServiceResolved.OnClickListener.onClick called for "
+ discoveredNodeData);
callback.handleCommissioningButtonClicked(discoveredNodeData);
}
});
new Handler(Looper.getMainLooper())
.post(
() ->
((LinearLayout) getActivity().findViewById(R.id.castingCommissioners))
.addView(commissionerButton));
private static final String TAG = CommissionerDiscoveryFragment.class.getSimpleName();
private static final long DISCOVERY_POLL_INTERVAL_MS = 15000;
private static final List<DiscoveredNodeData> commissionerVideoPlayerList = new ArrayList<>();
private FailureCallback failureCallback;
private SuccessCallback<DiscoveredNodeData> successCallback;
private ScheduledFuture poller;
private final TvCastingApp tvCastingApp;
private ScheduledExecutorService executor;

public CommissionerDiscoveryFragment(TvCastingApp tvCastingApp) {
this.tvCastingApp = tvCastingApp;
this.executor = Executors.newSingleThreadScheduledExecutor();
}

/**
* Use this factory method to create a new instance of this fragment using the provided
* parameters.
*
* @return A new instance of fragment CommissionerDiscoveryFragment.
*/
public static CommissionerDiscoveryFragment newInstance(TvCastingApp tvCastingApp) {
return new CommissionerDiscoveryFragment(tvCastingApp);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_commissioner_discovery, container, false);
}
}
};

FailureCallback failureCallback =
new FailureCallback() {
@Override
public void handle(MatterError matterError) {
Log.e(TAG, "Error occurred during video player commissioner discovery: " + matterError);
}
};

Button discoverButton = getView().findViewById(R.id.discoverButton);
discoverButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "Discovering on button click");
tvCastingApp.discoverVideoPlayerCommissioners(
DISCOVERY_DURATION_SECS, successCallback, failureCallback);
}

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

Button manualCommissioningButton = getView().findViewById(R.id.manualCommissioningButton);
// In the ideal case we wouldn't rely on the host activity to maintain this object since
// the lifecycle of the context isn't tied to this callback. Since this is an example app
// this should work fine.
Callback callback = (Callback) this.getActivity();
View.OnClickListener manualCommissioningButtonOnClickListener =
v -> callback.handleCommissioningButtonClicked(null);
manualCommissioningButton.setOnClickListener(manualCommissioningButtonOnClickListener);
ArrayAdapter<DiscoveredNodeData> arrayAdapter = new VideoPlayerCommissionerAdapter(getActivity(), commissionerVideoPlayerList);
final ListView list = getActivity().findViewById(R.id.commissionerList);
list.setAdapter(arrayAdapter);
list.setOnItemClickListener((parent, view1, position, id) -> {
DiscoveredNodeData discoveredNodeData = (DiscoveredNodeData)parent.getItemAtPosition(position);
Log.d(
TAG,
"OnItemClickListener.onClick called for "
+ discoveredNodeData);
Callback callback1 =
(Callback) getActivity();
callback1.handleCommissioningButtonClicked(discoveredNodeData);
});

Log.d(TAG, "Auto discovering");
tvCastingApp.discoverVideoPlayerCommissioners(
DISCOVERY_DURATION_SECS, successCallback, failureCallback);
}

@VisibleForTesting
public String getCommissionerButtonText(DiscoveredNodeData commissioner) {
String main = commissioner.getDeviceName() != null ? commissioner.getDeviceName() : "";
String aux =
"" + (commissioner.getProductId() > 0 ? "Product ID: " + commissioner.getProductId() : "");
aux +=
commissioner.getDeviceType() > 0
this.successCallback =
new SuccessCallback<DiscoveredNodeData>() {
@Override
public void handle(DiscoveredNodeData discoveredNodeData) {
Log.d(TAG, "Discovered a Video Player Commissioner: " + discoveredNodeData);
new Handler(Looper.getMainLooper())
.post(() -> {
if (commissionerVideoPlayerList.contains(discoveredNodeData)) {
Log.d(TAG, "Replacing existing entry in players list");
arrayAdapter.remove(discoveredNodeData);
}
arrayAdapter.add(discoveredNodeData);
});
}
};

this.failureCallback =
new FailureCallback() {
@Override
public void handle(MatterError matterError) {
Log.e(TAG, "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);
}
}
};

Button discoverButton = getView().findViewById(R.id.discoverButton);
discoverButton.setOnClickListener(
v -> {
Log.d(TAG, "Discovering on button click");
tvCastingApp.discoverVideoPlayerCommissioners(successCallback, failureCallback);
});
}

@Override
public void onResume()
{
super.onResume();
Log.d(TAG, "Auto discovering");

poller = executor
.scheduleAtFixedRate(
() -> {
tvCastingApp.discoverVideoPlayerCommissioners(successCallback, failureCallback);
},
0,
DISCOVERY_POLL_INTERVAL_MS,
TimeUnit.MILLISECONDS);
}

@Override
public void onPause() {
super.onPause();
tvCastingApp.stopVideoPlayerDiscovery();
poller.cancel(true);
}

/** Interface for notifying the host. */
public interface Callback {
/** Notifies listener of Commissioning Button click. */
void handleCommissioningButtonClicked(DiscoveredNodeData selectedCommissioner);
}
}

class VideoPlayerCommissionerAdapter extends ArrayAdapter<DiscoveredNodeData> {
private final List<DiscoveredNodeData> playerList;
private LayoutInflater inflater;

public VideoPlayerCommissionerAdapter(Context applicationContext, List<DiscoveredNodeData> playerList) {
super(applicationContext, 0, playerList);
this.playerList = playerList;
inflater = (LayoutInflater.from(applicationContext));
}

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
view = inflater.inflate(R.layout.commissionable_player_list_item, null);
String buttonText = getCommissionerButtonText(playerList.get(i));
TextView playerDescription = view.findViewById(R.id.commissionable_player_description);
playerDescription.setText(buttonText);
return view;
}

private String getCommissionerButtonText(DiscoveredNodeData commissioner) {
String main = commissioner.getDeviceName() != null ? commissioner.getDeviceName() : "";
String aux =
"" + (commissioner.getProductId() > 0 ? "Product ID: " + commissioner.getProductId() : "");
aux +=
commissioner.getDeviceType() > 0
? (aux.isEmpty() ? "" : " ") + "Device Type: " + commissioner.getDeviceType()
: "";
aux +=
commissioner.getVendorId() > 0
aux +=
commissioner.getVendorId() > 0
? (aux.isEmpty() ? "" : " from ") + "Vendor ID: " + commissioner.getVendorId()
: "";
aux = aux.isEmpty() ? aux : "\n[" + aux + "]";

String preCommissioned = commissioner.isPreCommissioned() ? " (Pre-commissioned)" : "";
return main + aux + preCommissioned;
}
aux = aux.isEmpty() ? aux : "\n[" + aux + "]";

/** Interface for notifying the host. */
public interface Callback {
/** Notifies listener of Commissioning Button click. */
void handleCommissioningButtonClicked(DiscoveredNodeData selectedCommissioner);
}
String preCommissioned = commissioner.isPreCommissioned() ? " (Pre-commissioned)" : "";
return main + aux + preCommissioned;
}
}
Original file line number Diff line number Diff line change
@@ -18,11 +18,13 @@
package com.chip.casting;

import android.net.nsd.NsdServiceInfo;

import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class DiscoveredNodeData {
private static final int MAX_IP_ADDRESSES = 5;
@@ -182,4 +184,21 @@ public String toString() {
+ ipAddresses
+ '}';
}

// autogenerated
@Override
public int hashCode() {
int result = Objects.hash(vendorId, productId, commissioningMode, deviceType);
result = 31 * result + Arrays.hashCode(rotatingId);
return result;
}

// autogenerated
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DiscoveredNodeData that = (DiscoveredNodeData) o;
return vendorId == that.vendorId && productId == that.productId && commissioningMode == that.commissioningMode && deviceType == that.deviceType && Objects.equals(hostName, that.hostName);
}
}
Original file line number Diff line number Diff line change
@@ -23,6 +23,9 @@ public class MatterError {
private int errorCode;
private String errorMessage;

public static final MatterError DISCOVERY_SERVICE_LOST =
new MatterError(4, "Discovery service was lost.");

public static final MatterError NO_ERROR = new MatterError(0, null);

public MatterError(int errorCode, String errorMessage) {
Original file line number Diff line number Diff line change
@@ -38,13 +38,13 @@ public class NsdDiscoveryListener implements NsdManager.DiscoveryListener {
private final ExecutorService resolutionExecutor;

public NsdDiscoveryListener(
NsdManager nsdManager,
String targetServiceType,
List<Long> deviceTypeFilter,
List<VideoPlayer> preCommissionedVideoPlayers,
SuccessCallback<DiscoveredNodeData> successCallback,
FailureCallback failureCallback,
NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState) {
NsdManager nsdManager,
String targetServiceType,
List<Long> deviceTypeFilter,
List<VideoPlayer> preCommissionedVideoPlayers,
SuccessCallback<DiscoveredNodeData> successCallback,
FailureCallback failureCallback,
NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState) {
this.nsdManager = nsdManager;
this.targetServiceType = targetServiceType;
this.deviceTypeFilter = deviceTypeFilter;
@@ -95,6 +95,7 @@ public void onServiceLost(NsdServiceInfo service) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "Service lost: " + service);
failureCallback.handle(MatterError.DISCOVERY_SERVICE_LOST);
}

@Override
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class TvCastingApp {
@@ -44,6 +45,11 @@ public class TvCastingApp {
private Context applicationContext;
private ChipAppServer chipAppServer;
private NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState;
private boolean discoveryStarted = false;

private WifiManager.MulticastLock multicastLock;
private NsdManager nsdManager;
private NsdDiscoveryListener nsdDiscoveryListener;

public boolean initApp(Context applicationContext, AppParameters appParameters) {
if (applicationContext == null || appParameters == null) {
@@ -103,21 +109,26 @@ public boolean initApp(Context applicationContext, AppParameters appParameters)
private native boolean initJni(AppParameters appParameters);

public void discoverVideoPlayerCommissioners(
long discoveryDurationSeconds,
SuccessCallback<DiscoveredNodeData> discoverySuccessCallback,
FailureCallback discoveryFailureCallback) {
Log.d(TAG, "TvCastingApp.discoverVideoPlayerCommissioners called");

if (this.discoveryStarted)
{
Log.d(TAG, "Discovery already started, stopping before starting again");
stopVideoPlayerDiscovery();
}

List<VideoPlayer> preCommissionedVideoPlayers = readCachedVideoPlayers();

WifiManager wifiManager =
(WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
WifiManager.MulticastLock multicastLock = wifiManager.createMulticastLock("multicastLock");
multicastLock = wifiManager.createMulticastLock("multicastLock");
multicastLock.setReferenceCounted(true);
multicastLock.acquire();

NsdManager nsdManager = (NsdManager) applicationContext.getSystemService(Context.NSD_SERVICE);
NsdDiscoveryListener nsdDiscoveryListener =
nsdManager = (NsdManager) applicationContext.getSystemService(Context.NSD_SERVICE);
nsdDiscoveryListener =
new NsdDiscoveryListener(
nsdManager,
DISCOVERY_TARGET_SERVICE_TYPE,
@@ -128,21 +139,22 @@ public void discoverVideoPlayerCommissioners(
nsdManagerResolverAvailState);

nsdManager.discoverServices(
DISCOVERY_TARGET_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, nsdDiscoveryListener);

Executors.newSingleThreadScheduledExecutor()
.schedule(
new Runnable() {
@Override
public void run() {
Log.d(TAG, "TvCastingApp stopping Video Player commissioner discovery");
nsdManager.stopServiceDiscovery(nsdDiscoveryListener);
multicastLock.release();
}
},
discoveryDurationSeconds,
TimeUnit.SECONDS);
Log.d(TAG, "TvCastingApp.discoverVideoPlayerCommissioners ended");
DISCOVERY_TARGET_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, nsdDiscoveryListener);
Log.d(TAG, "TvCastingApp.discoverVideoPlayerCommissioners started");
this.discoveryStarted = true;
}

public void stopVideoPlayerDiscovery()
{
Log.d(TAG, "TvCastingApp trying to stop video player discovery");
if (this.discoveryStarted && nsdManager != null && multicastLock != null && nsdDiscoveryListener != null) {
Log.d(TAG, "TvCastingApp stopping Video Player commissioner discovery");
nsdManager.stopServiceDiscovery(nsdDiscoveryListener);
if (multicastLock.isHeld()) {
multicastLock.release();
}
this.discoveryStarted = false;
}
}

public native boolean openBasicCommissioningWindow(
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/commissionable_player_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="blah"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="20dp" />

</LinearLayout>
Original file line number Diff line number Diff line change
@@ -31,6 +31,11 @@
android:layout_height="wrap_content"
android:text="@string/discovery_message_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />

<ListView
android:id = "@+id/commissionerList"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"/>
</LinearLayout>


Original file line number Diff line number Diff line change
@@ -37,7 +37,9 @@ class CommissionerDiscoveryViewModel: ObservableObject {
self.Log.info("discoveredCommissionerHandler called with \(commissioner)")
if(self.commissioners.contains(commissioner))
{
self.Log.info("Skipping previously discovered commissioner \(commissioner.description)")
var index = self.commissioners.firstIndex(of: commissioner)
self.commissioners[index!] = commissioner
self.Log.info("Updating previously discovered commissioner \(commissioner.description)")
}
else if(commissioner.numIPs == 0)
{
@@ -69,7 +71,9 @@ class CommissionerDiscoveryViewModel: ObservableObject {
if(commissioner != nil){
if(self.commissioners.contains(commissioner!))
{
self.Log.info("Skipping previously discovered commissioner \(commissioner!.description)")
var index = self.commissioners.firstIndex(of: commissioner!)
self.commissioners[index!] = commissioner!
self.Log.info("Updating previously discovered commissioner \(commissioner!.description)")
}
else
{

0 comments on commit 6643b86

Please sign in to comment.