diff --git a/examples/tv-casting-app/android/App/app/build.gradle b/examples/tv-casting-app/android/App/app/build.gradle index 99aabfdba51594..03d8fed015e6ca 100644 --- a/examples/tv-casting-app/android/App/app/build.gradle +++ b/examples/tv-casting-app/android/App/app/build.gradle @@ -37,6 +37,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + testOptions { + unitTests.returnDefaultValues = true + } + sourceSets { main { jniLibs.srcDirs = ['libs/jniLibs'] @@ -59,6 +63,7 @@ dependencies { implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' testImplementation 'junit:junit:4.+' + testImplementation 'org.mockito:mockito-core:3.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation 'com.google.zxing:core:3.3.0' 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 e379d2a7675095..55c3e48357d58b 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.chip.casting.app"> @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/MainActivity.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/MainActivity.java deleted file mode 100644 index 74928ce65079a6..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/MainActivity.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.chip.casting; - -import android.os.Bundle; -import android.widget.TextView; -import androidx.appcompat.app.AppCompatActivity; - -public class MainActivity extends AppCompatActivity { - - private TextView mHelloCastingTxt; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mHelloCastingTxt = findViewById(R.id.helloCastingTxt); - mHelloCastingTxt.setText("Hello, TV Casting app!"); - } -} 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 new file mode 100644 index 00000000000000..d13e04ce40ad30 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/CastingContext.java @@ -0,0 +1,27 @@ +package com.chip.casting.app; + +import android.content.Context; +import android.net.nsd.NsdManager; +import android.widget.LinearLayout; +import androidx.fragment.app.FragmentActivity; + +public class CastingContext { + private FragmentActivity fragmentActivity; + + public CastingContext(FragmentActivity fragmentActivity) { + this.fragmentActivity = fragmentActivity; + } + + public Context getApplicationContext() { + return fragmentActivity.getApplicationContext(); + } + + public NsdManager getNsdManager() { + return (NsdManager) + fragmentActivity.getApplicationContext().getSystemService(Context.NSD_SERVICE); + } + + public LinearLayout getCommissionersLayout() { + return (LinearLayout) fragmentActivity.findViewById(R.id.castingCommissioners); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/ChipTvCastingApplication.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ChipTvCastingApplication.java similarity index 84% rename from examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/ChipTvCastingApplication.java rename to examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ChipTvCastingApplication.java index 4a3873d84618f1..4986cc28e49c01 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/ChipTvCastingApplication.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/ChipTvCastingApplication.java @@ -1,4 +1,4 @@ -package com.chip.casting; +package com.chip.casting.app; import android.app.Application; 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 new file mode 100644 index 00000000000000..bbfabb68b93ed4 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java @@ -0,0 +1,51 @@ +package com.chip.casting.app; + +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import com.chip.casting.dnssd.CommissionerDiscoveryListener; +import com.chip.casting.util.GlobalCastingConstants; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + startCommissionerDiscovery(); + } + + private void startCommissionerDiscovery() { + WifiManager wifi = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + WifiManager.MulticastLock multicastLock = wifi.createMulticastLock("multicastLock"); + multicastLock.setReferenceCounted(true); + multicastLock.acquire(); + + CastingContext castingContext = new CastingContext(this); + NsdManager.DiscoveryListener commissionerDiscoveryListener = + new CommissionerDiscoveryListener(castingContext); + + NsdManager nsdManager = castingContext.getNsdManager(); + nsdManager.discoverServices( + GlobalCastingConstants.CommissionerServiceType, + NsdManager.PROTOCOL_DNS_SD, + commissionerDiscoveryListener); + + // Stop discovery after specified timeout + Executors.newSingleThreadScheduledExecutor() + .schedule( + new Runnable() { + @Override + public void run() { + nsdManager.stopServiceDiscovery(commissionerDiscoveryListener); + multicastLock.release(); + } + }, + 10, + TimeUnit.SECONDS); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerDiscoveryListener.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerDiscoveryListener.java new file mode 100644 index 00000000000000..3c563cbfc1c6a9 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerDiscoveryListener.java @@ -0,0 +1,62 @@ +package com.chip.casting.dnssd; + +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.util.Log; +import com.chip.casting.app.CastingContext; +import com.chip.casting.util.GlobalCastingConstants; +import java.util.ArrayList; +import java.util.List; + +public class CommissionerDiscoveryListener implements NsdManager.DiscoveryListener { + + private static final String TAG = CommissionerDiscoveryListener.class.getSimpleName(); + + private final CastingContext castingContext; + private final List commissioners = new ArrayList<>(); + + public CommissionerDiscoveryListener(CastingContext castingContext) { + this.castingContext = castingContext; + } + + @Override + public void onDiscoveryStarted(String regType) { + Log.d(TAG, "Service discovery started. regType: " + regType); + } + + @Override + public void onServiceFound(NsdServiceInfo service) { + Log.d(TAG, "Service discovery success. " + service); + if (service.getServiceType().equals(GlobalCastingConstants.CommissionerServiceType)) { + castingContext + .getNsdManager() + .resolveService(service, new CommissionerResolveListener(castingContext, commissioners)); + } else { + Log.d(TAG, "Ignoring discovered service: " + service.toString()); + } + } + + @Override + public void onServiceLost(NsdServiceInfo service) { + // When the network service is no longer available. + // Internal bookkeeping code goes here. + Log.e(TAG, "Service lost: " + service); + } + + @Override + public void onDiscoveryStopped(String serviceType) { + Log.i(TAG, "Discovery stopped: " + serviceType); + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Discovery failed to start: Error code:" + errorCode); + castingContext.getNsdManager().stopServiceDiscovery(this); + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Discovery failed to stop: Error code:" + errorCode); + castingContext.getNsdManager().stopServiceDiscovery(this); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerResolveListener.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerResolveListener.java new file mode 100644 index 00000000000000..ea8fe3dc402e71 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/CommissionerResolveListener.java @@ -0,0 +1,75 @@ +package com.chip.casting.dnssd; + +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Button; +import androidx.annotation.VisibleForTesting; +import com.chip.casting.app.CastingContext; +import java.util.List; + +public class CommissionerResolveListener implements NsdManager.ResolveListener { + + private static final String TAG = CommissionerResolveListener.class.getSimpleName(); + private final CastingContext castingContext; + private final List commissioners; + + public CommissionerResolveListener( + CastingContext castingContext, List commissioners) { + this.castingContext = castingContext; + this.commissioners = commissioners; + } + + @Override + public void onServiceResolved(NsdServiceInfo serviceInfo) { + DiscoveredNodeData commissioner = new DiscoveredNodeData(serviceInfo); + commissioners.add(commissioner); + Log.d(TAG, "Commissioner resolved: " + commissioner); + + String buttonText = getCommissionerButtonText(commissioner); + if (!buttonText.isEmpty()) { + Button commissionerButton = new Button(castingContext.getApplicationContext()); + commissionerButton.setText(buttonText); + new Handler(Looper.getMainLooper()) + .post(() -> castingContext.getCommissionersLayout().addView(commissionerButton)); + } else Log.e(TAG, "Skipped displaying " + commissioner); + } + + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + switch (errorCode) { + case NsdManager.FAILURE_ALREADY_ACTIVE: + Log.e(TAG, "FAILURE_ALREADY_ACTIVE - Service: " + serviceInfo); + castingContext + .getNsdManager() + .resolveService( + serviceInfo, new CommissionerResolveListener(castingContext, commissioners)); + break; + case NsdManager.FAILURE_INTERNAL_ERROR: + Log.e(TAG, "FAILURE_INTERNAL_ERROR - Service: " + serviceInfo); + break; + case NsdManager.FAILURE_MAX_LIMIT: + Log.e(TAG, "FAILURE_MAX_LIMIT - Service: " + serviceInfo); + break; + } + } + + @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 + ? (aux.isEmpty() ? "" : " ") + "Device Type: " + commissioner.getDeviceType() + : ""; + aux += + commissioner.getVendorId() > 0 + ? (aux.isEmpty() ? "" : " from ") + "Vendor ID: " + commissioner.getVendorId() + : ""; + aux = aux.isEmpty() ? aux : "\n[" + aux + "]"; + return main + aux; + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/DiscoveredNodeData.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/DiscoveredNodeData.java new file mode 100644 index 00000000000000..0022a42c1252af --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/dnssd/DiscoveredNodeData.java @@ -0,0 +1,210 @@ +package com.chip.casting.dnssd; + +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; + +public class DiscoveredNodeData { + private static final int MAX_IP_ADDRESSES = 5; + private static final int MAX_ROTATING_ID_LEN = 50; + private static final String KEY_DEVICE_NAME = "DN"; + private static final String KEY_DEVICE_TYPE = "DT"; + private static final String KEY_VENDOR_PRODUCT = "VP"; + + private String hostName; + private String instanceName; + private long longDiscriminator; + private long vendorId; + private long productId; + private byte commissioningMode; + private long deviceType; + private String deviceName; + private byte rotatingId[] = new byte[MAX_ROTATING_ID_LEN]; + private int rotatingIdLen; + private short pairingHint; + private String pairingInstruction; + private short port; + private int numIPs; + private List ipAddresses; + + public DiscoveredNodeData(NsdServiceInfo serviceInfo) { + Map attributes = serviceInfo.getAttributes(); + this.deviceName = new String(attributes.get(KEY_DEVICE_NAME), StandardCharsets.UTF_8); + this.deviceType = + Long.parseLong(new String(attributes.get(KEY_DEVICE_TYPE), StandardCharsets.UTF_8)); + + String vp = new String(attributes.get(KEY_VENDOR_PRODUCT), StandardCharsets.UTF_8); + if (vp != null) { + String[] vpArray = vp.split("\\+"); + if (vpArray.length > 0) { + this.vendorId = Long.parseLong(vpArray[0]); + if (vpArray.length == 2) { + this.productId = Long.parseLong(vpArray[1]); + } + } + } + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + public long getLongDiscriminator() { + return longDiscriminator; + } + + public void setLongDiscriminator(long longDiscriminator) { + this.longDiscriminator = longDiscriminator; + } + + public long getVendorId() { + return vendorId; + } + + public void setVendorId(long vendorId) { + this.vendorId = vendorId; + } + + public long getProductId() { + return productId; + } + + public void setProductId(long productId) { + this.productId = productId; + } + + public byte getCommissioningMode() { + return commissioningMode; + } + + public void setCommissioningMode(byte commissioningMode) { + this.commissioningMode = commissioningMode; + } + + public long getDeviceType() { + return deviceType; + } + + public void setDeviceType(long deviceType) { + this.deviceType = deviceType; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public byte[] getRotatingId() { + return rotatingId; + } + + public void setRotatingId(byte[] rotatingId) { + this.rotatingId = rotatingId; + } + + public int getRotatingIdLen() { + return rotatingIdLen; + } + + public void setRotatingIdLen(int rotatingIdLen) { + this.rotatingIdLen = rotatingIdLen; + } + + public short getPairingHint() { + return pairingHint; + } + + public void setPairingHint(short pairingHint) { + this.pairingHint = pairingHint; + } + + public String getPairingInstruction() { + return pairingInstruction; + } + + public void setPairingInstruction(String pairingInstruction) { + this.pairingInstruction = pairingInstruction; + } + + public short getPort() { + return port; + } + + public void setPort(short port) { + this.port = port; + } + + public int getNumIPs() { + return numIPs; + } + + public void setNumIPs(int numIPs) { + this.numIPs = numIPs; + } + + public List getIpAddresses() { + return ipAddresses; + } + + public void setIpAddresses(List ipAddresses) { + this.ipAddresses = ipAddresses; + } + + @Override + public String toString() { + return "DiscoveredNodeData{" + + "hostName='" + + hostName + + '\'' + + ", instanceName='" + + instanceName + + '\'' + + ", longDiscriminator=" + + longDiscriminator + + ", vendorId=" + + vendorId + + ", productId=" + + productId + + ", commissioningMode=" + + commissioningMode + + ", deviceType=" + + deviceType + + ", deviceName='" + + deviceName + + '\'' + + ", rotatingId=" + + Arrays.toString(rotatingId) + + ", rotatingIdLen=" + + rotatingIdLen + + ", pairingHint=" + + pairingHint + + ", pairingInstruction='" + + pairingInstruction + + '\'' + + ", port=" + + port + + ", numIPs=" + + numIPs + + ", ipAddresses=" + + ipAddresses + + '}'; + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java new file mode 100644 index 00000000000000..f6e60ec63a5b67 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/GlobalCastingConstants.java @@ -0,0 +1,5 @@ +package com.chip.casting.util; + +public class GlobalCastingConstants { + public static final String CommissionerServiceType = "_matterd._udp."; +} 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 99d71254d7b2e9..62a1aa33d57f63 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,14 +4,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context="MainActivity"> - - - \ No newline at end of file + + diff --git a/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/ExampleUnitTest.java b/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/ExampleUnitTest.java deleted file mode 100644 index 9c9e4486d25319..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.chip.casting; - -import static org.junit.Assert.*; - -import org.junit.Test; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} diff --git a/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/dnssd/CommissionerDiscoveryListenerTest.java b/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/dnssd/CommissionerDiscoveryListenerTest.java new file mode 100644 index 00000000000000..7c29fdfa9db03e --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/test/java/com/chip/casting/dnssd/CommissionerDiscoveryListenerTest.java @@ -0,0 +1,68 @@ +package com.chip.casting.dnssd; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import com.chip.casting.app.CastingContext; +import com.chip.casting.util.GlobalCastingConstants; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CommissionerDiscoveryListenerTest { + @Mock private CastingContext castingContext; + + @Mock private NsdServiceInfo nsdService; + + @Mock private NsdManager nsdManager; + + @Test + public void onServiceFound_callsResolve_whenCommissionerDiscovered() { + when(nsdService.getServiceType()).thenReturn(GlobalCastingConstants.CommissionerServiceType); + when(castingContext.getNsdManager()).thenReturn(nsdManager); + doNothing().when(nsdManager).resolveService(any(), any()); + + CommissionerDiscoveryListener listener = new CommissionerDiscoveryListener(castingContext); + listener.onServiceFound(nsdService); + + verify(nsdManager, times(1)).resolveService(any(), any()); + } + + @Test + public void onServiceFound_noCallsToResolve_whenNonCommissionerDiscovered() { + when(nsdService.getServiceType()).thenReturn("_type._udp"); + CommissionerDiscoveryListener listener = new CommissionerDiscoveryListener(castingContext); + listener.onServiceFound(nsdService); + + verify(nsdManager, times(0)).resolveService(any(), any()); + } + + @Test + public void onStartDiscoveryFailed_callsStopServiceDiscovery() { + when(castingContext.getNsdManager()).thenReturn(nsdManager); + doNothing().when(nsdManager).stopServiceDiscovery(any()); + + CommissionerDiscoveryListener listener = new CommissionerDiscoveryListener(castingContext); + listener.onStartDiscoveryFailed("_test_type._udp", 1); + + verify(nsdManager, times(1)).stopServiceDiscovery(any()); + } + + @Test + public void onStopDiscoveryFailed_callsStopServiceDiscovery() { + when(castingContext.getNsdManager()).thenReturn(nsdManager); + doNothing().when(nsdManager).stopServiceDiscovery(any()); + + CommissionerDiscoveryListener listener = new CommissionerDiscoveryListener(castingContext); + listener.onStopDiscoveryFailed("_test_type._udp", 1); + + verify(nsdManager, times(1)).stopServiceDiscovery(any()); + } +}