From 0af637bf5e46ac610968e1e46dfb352aac17fe43 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Sat, 13 Apr 2024 04:38:20 -0700 Subject: [PATCH] [DO NOT REVIEW][epskc] demo version impl for epskc --- android/build-commissioner-libs.sh | 2 +- .../openthread_commissioner/app/build.gradle | 10 +- .../commissioner/app/BorderAgentAdapter.java | 139 ++++++++++ .../app/BorderAgentDiscoverer.java | 105 +++++--- .../commissioner/app/BorderAgentInfo.java | 60 +++-- .../commissioner/app/CameraSourceView.java | 15 +- .../app/CommissioningFragment.java | 8 +- .../app/FetchCredentialDialogFragment.java | 146 ----------- .../commissioner/app/FragmentCallback.java | 14 + .../app/GetAdminPasscodeFragment.java | 190 ++++++++++++++ .../app/InputNetworkPasswordFragment.java | 29 +-- .../commissioner/app/JoinerDeviceInfo.java | 13 +- .../commissioner/app/MainActivity.java | 46 +++- .../commissioner/app/NetworkAdapter.java | 126 --------- .../app/RetrieveDatasetFragment.java | 233 +++++++++++++++++ .../commissioner/app/ScanQrCodeFragment.java | 19 +- ...t.java => SelectBorderRouterFragment.java} | 157 +++++++----- .../commissioner/app/SetDatasetFragment.java | 242 ++++++++++++++++++ .../commissioner/app/ThreadNetworkInfo.java | 88 ------- .../app/ThreadNetworkInfoHolder.java | 83 ------ .../app/src/main/res/layout/activity_main.xml | 2 +- .../res/layout/border_agent_list_item.xml | 49 ++++ .../layout/fragment_get_admin_passcode.xml | 74 ++++++ .../fragment_input_network_password.xml | 3 +- .../res/layout/fragment_retrieve_dataset.xml | 73 ++++++ ....xml => fragment_select_border_router.xml} | 93 ++++--- .../main/res/layout/fragment_set_dataset.xml | 83 ++++++ .../src/main/res/layout/network_list_item.xml | 24 -- .../app/src/main/res/values/strings.xml | 6 +- .../app/src/main/res/values/styles.xml | 7 +- include/commissioner/commissioner.hpp | 13 + src/library/commissioner_impl.cpp | 29 +++ src/library/commissioner_impl.hpp | 3 + src/library/commissioner_safe.cpp | 17 ++ src/library/commissioner_safe.hpp | 1 + 35 files changed, 1505 insertions(+), 697 deletions(-) create mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentAdapter.java delete mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FetchCredentialDialogFragment.java create mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/GetAdminPasscodeFragment.java delete mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/NetworkAdapter.java create mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/RetrieveDatasetFragment.java rename android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/{SelectNetworkFragment.java => SelectBorderRouterFragment.java} (60%) create mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/SetDatasetFragment.java delete mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfo.java delete mode 100644 android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfoHolder.java create mode 100644 android/openthread_commissioner/app/src/main/res/layout/border_agent_list_item.xml create mode 100644 android/openthread_commissioner/app/src/main/res/layout/fragment_get_admin_passcode.xml create mode 100644 android/openthread_commissioner/app/src/main/res/layout/fragment_retrieve_dataset.xml rename android/openthread_commissioner/app/src/main/res/layout/{fragment_select_network.xml => fragment_select_border_router.xml} (54%) create mode 100644 android/openthread_commissioner/app/src/main/res/layout/fragment_set_dataset.xml delete mode 100644 android/openthread_commissioner/app/src/main/res/layout/network_list_item.xml diff --git a/android/build-commissioner-libs.sh b/android/build-commissioner-libs.sh index ed9c40e95..183e25780 100755 --- a/android/build-commissioner-libs.sh +++ b/android/build-commissioner-libs.sh @@ -54,7 +54,7 @@ cmake -GNinja \ -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_CXX_STANDARD=11 \ -DCMAKE_CXX_STANDARD_REQUIRED=ON \ - -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_BUILD_TYPE=Debug \ -DOT_COMM_ANDROID=ON \ -DOT_COMM_JAVA_BINDING=ON \ -DOT_COMM_APP=OFF \ diff --git a/android/openthread_commissioner/app/build.gradle b/android/openthread_commissioner/app/build.gradle index 6aa612ab2..2302385fb 100644 --- a/android/openthread_commissioner/app/build.gradle +++ b/android/openthread_commissioner/app/build.gradle @@ -97,14 +97,18 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation 'com.google.guava:guava:31.1-jre' - implementation 'androidx.appcompat:appcompat:1.2.0' + // Fix Duplicate class + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + + implementation 'com.google.guava:guava:33.2.1-android' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.activity:activity:1.9.1' implementation "androidx.concurrent:concurrent-futures:1.1.0" implementation 'androidx.constraintlayout:constraintlayout:2.0.2' implementation 'androidx.navigation:navigation-fragment:2.3.0' implementation 'com.google.android.gms:play-services-vision:20.1.3+' implementation 'com.google.android.gms:play-services-threadnetwork:16.0.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.9.0' androidTestImplementation 'com.google.truth:truth:1.4.4' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentAdapter.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentAdapter.java new file mode 100644 index 000000000..75f3df919 --- /dev/null +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentAdapter.java @@ -0,0 +1,139 @@ +package io.openthread.commissioner.app; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.ArrayMap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import androidx.annotation.Nullable; +import io.openthread.commissioner.app.BorderAgentDiscoverer.BorderAgentListener; +import java.net.Inet6Address; +import java.util.Map; +import java.util.Vector; + +public class BorderAgentAdapter extends BaseAdapter implements BorderAgentListener { + + private final Vector borderAgentServices = new Vector<>(); + private final Map epskcServices = new ArrayMap<>(); + + private final LayoutInflater inflater; + + BorderAgentAdapter(Context context) { + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public void addBorderAgent(BorderAgentInfo serviceInfo) { + if (serviceInfo.isEpskcService) { + epskcServices.put(serviceInfo.instanceName, serviceInfo); + notifyDataSetChanged(); + return; + } + + boolean hasExistingBorderRouter = false; + for (int i = 0; i < borderAgentServices.size(); i++) { + if (borderAgentServices.get(i).instanceName.equals(serviceInfo.instanceName)) { + borderAgentServices.set(i, serviceInfo); + hasExistingBorderRouter = true; + } + } + + if (!hasExistingBorderRouter) { + borderAgentServices.add(serviceInfo); + } + + notifyDataSetChanged(); + } + + public void removeBorderAgent(boolean isEpskcService, String instanceName) { + if (isEpskcService) { + epskcServices.remove(instanceName); + } else { + borderAgentServices.removeIf(serviceInfo -> serviceInfo.instanceName.equals(instanceName)); + } + notifyDataSetChanged(); + } + + public void clear() { + borderAgentServices.clear(); + epskcServices.clear(); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return borderAgentServices.size(); + } + + @Override + public Object getItem(int position) { + return borderAgentServices.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup container) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.border_agent_list_item, container, false); + } + + BorderAgentInfo borderAgentInfo = borderAgentServices.get(position); + + TextView instanceNameText = convertView.findViewById(R.id.border_agent_instance_name); + instanceNameText.setText(borderAgentInfo.instanceName); + + TextView vendorNameText = convertView.findViewById(R.id.border_agent_vendor_name); + vendorNameText.setText(borderAgentInfo.vendorName); + + TextView modelNameText = convertView.findViewById(R.id.border_agent_model_name); + modelNameText.setText(borderAgentInfo.modelName); + + TextView adminModeText = convertView.findViewById(R.id.border_agent_admin_mode); + adminModeText.setText( + "In Administration Mode: " + (inAdministrationMode(borderAgentInfo) ? "YES" : "NO")); + + TextView borderAgentIpAddrText = convertView.findViewById(R.id.border_agent_ip_addr); + int port = + inAdministrationMode(borderAgentInfo) + ? getEpskcService(borderAgentInfo).port + : borderAgentInfo.port; + String socketAddress; + if (borderAgentInfo.host instanceof Inet6Address) { + socketAddress = "[" + borderAgentInfo.host.getHostAddress() + "]:" + port; + } else { + socketAddress = borderAgentInfo.host.getHostAddress() + ":" + port; + } + borderAgentIpAddrText.setText(socketAddress); + return convertView; + } + + private boolean inAdministrationMode(BorderAgentInfo borderAgentInfo) { + return epskcServices.containsKey(borderAgentInfo.instanceName); + } + + @Override + public void onBorderAgentFound(BorderAgentInfo borderAgentInfo) { + new Handler(Looper.getMainLooper()).post(() -> addBorderAgent(borderAgentInfo)); + } + + @Override + public void onBorderAgentLost(boolean isEpskcService, String instanceName) { + new Handler(Looper.getMainLooper()).post(() -> removeBorderAgent(isEpskcService, instanceName)); + } + + /** + * Returns the _meshcop-e._udp service which is associated with the given _meshcop._udp service, + * or {@code null} if such service doesn't exist. + */ + @Nullable + private BorderAgentInfo getEpskcService(BorderAgentInfo meshcopService) { + return epskcServices.get(meshcopService.instanceName); + } +} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentDiscoverer.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentDiscoverer.java index 38c1eca3c..ceb8da2ab 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentDiscoverer.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentDiscoverer.java @@ -30,12 +30,17 @@ import android.Manifest.permission; import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.net.wifi.WifiManager; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; +import com.google.common.net.InetAddresses; +import java.net.Inet6Address; +import java.net.InetAddress; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -47,18 +52,23 @@ public class BorderAgentDiscoverer implements NsdManager.DiscoveryListener { private static final String TAG = BorderAgentDiscoverer.class.getSimpleName(); - private static final String SERVICE_TYPE = "_meshcop._udp"; + public static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp"; + public static final String MESHCOP_E_SERVICE_TYPE = "_meshcop-e._udp"; private static final String KEY_ID = "id"; private static final String KEY_NETWORK_NAME = "nn"; private static final String KEY_EXTENDED_PAN_ID = "xp"; + private static final String KEY_VENDOR_NAME = "vn"; + private static final String KEY_MODEL_NAME = "mn"; - private WifiManager.MulticastLock wifiMulticastLock; - private NsdManager nsdManager; - private BorderAgentListener borderAgentListener; + private final WifiManager.MulticastLock wifiMulticastLock; + private final NsdManager nsdManager; + private final ConnectivityManager connManager; + private final String serviceType; + private final BorderAgentListener borderAgentListener; private ExecutorService executor = Executors.newSingleThreadExecutor(); - private BlockingQueue unresolvedServices = new ArrayBlockingQueue<>(256); - private AtomicBoolean isResolvingService = new AtomicBoolean(false); + private final BlockingQueue unresolvedServices = new ArrayBlockingQueue<>(256); + private final AtomicBoolean isResolvingService = new AtomicBoolean(false); private boolean isScanning = false; @@ -66,16 +76,19 @@ public interface BorderAgentListener { void onBorderAgentFound(BorderAgentInfo borderAgentInfo); - void onBorderAgentLost(byte[] id); + default void onBorderAgentLost(boolean isEpskcService, String instanceName) {} } @RequiresPermission(permission.INTERNET) - public BorderAgentDiscoverer(Context context, BorderAgentListener borderAgentListener) { + public BorderAgentDiscoverer( + Context context, String serviceType, BorderAgentListener borderAgentListener) { WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); wifiMulticastLock = wifi.createMulticastLock("multicastLock"); - nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + nsdManager = context.getSystemService(NsdManager.class); + connManager = context.getSystemService(ConnectivityManager.class); + this.serviceType = serviceType; this.borderAgentListener = borderAgentListener; } @@ -91,8 +104,8 @@ public void start() { wifiMulticastLock.acquire(); startResolver(); - nsdManager.discoverServices( - BorderAgentDiscoverer.SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, this); + + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, this); } private void startResolver() { @@ -112,7 +125,7 @@ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { public void onServiceResolved(NsdServiceInfo serviceInfo) { BorderAgentInfo borderAgent = getBorderAgentInfo(serviceInfo); if (borderAgent != null) { - Log.d(TAG, "successfully resolved service: " + serviceInfo.toString()); + Log.d(TAG, "successfully resolved service: " + serviceInfo); Log.d( TAG, "successfully resolved service: " + serviceInfo.getHost().getCanonicalHostName()); @@ -167,7 +180,7 @@ public void stop() { @Override public void onDiscoveryStarted(String serviceType) { - Log.d(TAG, "start discovering Border Agent"); + Log.d(TAG, "start discovering Border Agent: " + serviceType); } @Override @@ -184,11 +197,9 @@ public void onServiceFound(NsdServiceInfo nsdServiceInfo) { @Override public void onServiceLost(NsdServiceInfo nsdServiceInfo) { - byte[] id = getBorderAgentId(nsdServiceInfo); - if (id != null) { - Log.d(TAG, "a Border Agent service is gone: " + nsdServiceInfo.getServiceName()); - borderAgentListener.onBorderAgentLost(id); - } + Log.d(TAG, "a Border Agent service is gone: " + nsdServiceInfo.getServiceName()); + borderAgentListener.onBorderAgentLost( + serviceType.equals(MESHCOP_E_SERVICE_TYPE), nsdServiceInfo.getServiceName()); } @Override @@ -204,26 +215,54 @@ public void onStopDiscoveryFailed(String serviceType, int errorCode) { @Nullable private BorderAgentInfo getBorderAgentInfo(NsdServiceInfo serviceInfo) { Map attrs = serviceInfo.getAttributes(); - byte[] id = getBorderAgentId(serviceInfo); - - if (!attrs.containsKey(KEY_NETWORK_NAME) || !attrs.containsKey(KEY_EXTENDED_PAN_ID)) { - return null; - } - return new BorderAgentInfo( - id, - new String(attrs.get(KEY_NETWORK_NAME)), + serviceType.equals(MESHCOP_E_SERVICE_TYPE), + serviceInfo.getServiceName(), + handleNsdServiceAddress(serviceInfo.getHost()), + serviceInfo.getPort(), + attrs.get(KEY_ID), + getStringAttribute(attrs, KEY_NETWORK_NAME), attrs.get(KEY_EXTENDED_PAN_ID), - serviceInfo.getHost(), - serviceInfo.getPort()); + getStringAttribute(attrs, KEY_VENDOR_NAME), + getStringAttribute(attrs, KEY_MODEL_NAME)); } @Nullable - private byte[] getBorderAgentId(NsdServiceInfo serviceInfo) { - Map attrs = serviceInfo.getAttributes(); - if (attrs.containsKey(KEY_ID)) { - return attrs.get(KEY_ID).clone(); + private static String getStringAttribute(Map attributes, String key) { + byte[] value = attributes.get(key); + if (value == null) { + return null; + } + return new String(value); + } + + /** + * Properly handles the {@link InetAddress} within a discovered {@link NsdServiceInfo}. + * + *

For example, adds the scope ID to an IPv6 link-local address to make is usable. + */ + private InetAddress handleNsdServiceAddress(InetAddress address) { + if (!(address instanceof Inet6Address)) { + return address; } - return null; + + Inet6Address address6 = (Inet6Address) address; + + // Sets the scope ID for IPv6 link-local address if it's missing. This can happen before + // Android U + if (address6.isLinkLocalAddress() && address6.getScopeId() == 0) { + // Assume the mDNS service is discovered on the current active default network + Network network = connManager.getActiveNetwork(); + if (network == null) { + return address6; + } + String interfaceName = connManager.getLinkProperties(network).getInterfaceName(); + if (interfaceName == null) { + return address6; + } + return InetAddresses.forString(address6.getHostAddress() + "%" + interfaceName); + } + + return address6; } } diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentInfo.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentInfo.java index a916be670..025579101 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentInfo.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/BorderAgentInfo.java @@ -30,49 +30,65 @@ import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.net.InetAddresses; import java.net.InetAddress; -import java.net.UnknownHostException; public class BorderAgentInfo implements Parcelable { - - public byte[] id; - public String networkName; - public byte[] extendedPanId; - public InetAddress host; - public int port; + public final boolean isEpskcService; + public final String instanceName; + public final InetAddress host; + public final int port; + @Nullable public final byte[] id; + @Nullable public final String networkName; + @Nullable public final byte[] extendedPanId; + @Nullable public final String vendorName; + @Nullable public final String modelName; public BorderAgentInfo( - @NonNull byte[] id, - @NonNull String networkName, - @NonNull byte[] extendedPanId, - @NonNull InetAddress host, - @NonNull int port) { + boolean isEpskcService, + String instanceName, + InetAddress host, + int port, + @Nullable byte[] id, + @Nullable String networkName, + @Nullable byte[] extendedPanId, + @Nullable String vendorName, + @Nullable String modelName) { + this.isEpskcService = isEpskcService; + this.instanceName = instanceName; + this.host = host; + this.port = port; this.id = id == null ? null : id.clone(); this.networkName = networkName; this.extendedPanId = extendedPanId == null ? null : extendedPanId.clone(); - this.host = host; - this.port = port; + this.vendorName = vendorName; + this.modelName = modelName; } protected BorderAgentInfo(Parcel in) { + isEpskcService = in.readInt() != 0; + instanceName = in.readString(); + host = InetAddresses.forString(in.readString()); + port = in.readInt(); id = in.createByteArray(); networkName = in.readString(); extendedPanId = in.createByteArray(); - try { - host = InetAddress.getByAddress(in.createByteArray()); - } catch (UnknownHostException e) { - } - port = in.readInt(); + vendorName = in.readString(); + modelName = in.readString(); } @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(isEpskcService ? 1 : 0); + dest.writeString(instanceName); + dest.writeString(host.getHostAddress()); + dest.writeInt(port); dest.writeByteArray(id); dest.writeString(networkName); dest.writeByteArray(extendedPanId); - dest.writeByteArray(host.getAddress()); - dest.writeInt(port); + dest.writeString(vendorName); + dest.writeString(modelName); } @Override diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CameraSourceView.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CameraSourceView.java index 3b2b30502..d30d2dc36 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CameraSourceView.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CameraSourceView.java @@ -35,6 +35,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.RequiresPermission; import com.google.android.gms.vision.CameraSource; import com.google.android.gms.vision.Detector; @@ -46,13 +47,12 @@ public class CameraSourceView extends ViewGroup { private static final String TAG = CameraSourceView.class.getSimpleName(); - private SurfaceView surfaceView; + private final SurfaceView surfaceView; private boolean cameraStarted = false; private boolean surfaceAvailable; - Context context; + private final Context context; private CameraSource cameraSource; - private BarcodeDetector barcodeDetector; public CameraSourceView(Context context, AttributeSet attrs) { super(context, attrs); @@ -65,7 +65,7 @@ public CameraSourceView(Context context, AttributeSet attrs) { public void initializeBarcodeDetectorAndCamera(Detector.Processor barcodeProcessor) { - barcodeDetector = new BarcodeDetector.Builder(context).build(); + BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); barcodeDetector.setProcessor(barcodeProcessor); cameraSource = @@ -80,7 +80,6 @@ public void initializeBarcodeDetectorAndCamera(Detector.Processor barco /** * Attempts to start the camera source. * - * @throws IOException if the camera doesn't work * @throws SecurityException if app doesn't have permission to access camera */ @RequiresPermission(Manifest.permission.CAMERA) @@ -132,17 +131,17 @@ private class SurfaceCallback implements SurfaceHolder.Callback { @RequiresPermission(Manifest.permission.CAMERA) @Override - public void surfaceCreated(SurfaceHolder surface) { + public void surfaceCreated(@NonNull SurfaceHolder surface) { surfaceAvailable = true; startCamera(); } @Override - public void surfaceDestroyed(SurfaceHolder surface) { + public void surfaceDestroyed(@NonNull SurfaceHolder surface) { surfaceAvailable = false; } @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {} } } diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CommissioningFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CommissioningFragment.java index be02476e1..f0e09b74b 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CommissioningFragment.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/CommissioningFragment.java @@ -59,7 +59,7 @@ public class CommissioningFragment extends Fragment ImageView errorImage; private final FragmentCallback fragmentCallback; - private final ThreadNetworkInfoHolder networkInfoHolder; + private final BorderAgentInfo borderAgentInfo; private final byte[] pskc; private final JoinerDeviceInfo joinerDeviceInfo; @@ -68,11 +68,11 @@ public class CommissioningFragment extends Fragment public CommissioningFragment( FragmentCallback fragmentCallback, - ThreadNetworkInfoHolder networkInfoHolder, + BorderAgentInfo borderAgentInfo, byte[] pskc, JoinerDeviceInfo joinerDeviceInfo) { this.fragmentCallback = fragmentCallback; - this.networkInfoHolder = networkInfoHolder; + this.borderAgentInfo = borderAgentInfo; this.pskc = pskc; this.joinerDeviceInfo = joinerDeviceInfo; } @@ -124,8 +124,6 @@ public void onDestroy() { } private void startMeshcop() { - BorderAgentInfo borderAgentInfo = networkInfoHolder.getBorderAgents().get(0); - showInProgress("Petitioning..."); commissionFuture = diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FetchCredentialDialogFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FetchCredentialDialogFragment.java deleted file mode 100644 index 65b87f8ee..000000000 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FetchCredentialDialogFragment.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2020, The OpenThread Commissioner Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package io.openthread.commissioner.app; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; - -public class FetchCredentialDialogFragment extends DialogFragment - implements DialogInterface.OnClickListener { - - private CredentialListener credentialListener; - private TextView statusText; - - private AlertDialog dialog; - - BorderAgentInfo borderAgentInfo; - byte[] pskc; - private ThreadNetworkCredentials credentials; - - public interface CredentialListener { - - void onCancelClick(FetchCredentialDialogFragment fragment); - - void onConfirmClick( - FetchCredentialDialogFragment fragment, ThreadNetworkCredentials credentials); - } - - public FetchCredentialDialogFragment( - @NonNull BorderAgentInfo borderAgentInfo, - @NonNull byte[] pskc, - @NonNull CredentialListener credentialListener) { - this.borderAgentInfo = borderAgentInfo; - this.pskc = pskc; - this.credentialListener = credentialListener; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = requireActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.fragment_fetch_credential_dialog, null); - - statusText = view.findViewById(R.id.fetch_credential_status); - - builder.setTitle("Fetching Credential"); - builder.setView(view); - builder.setPositiveButton(R.string.fetch_credential_done, this); - builder.setNegativeButton(R.string.fetch_credential_cancel, this); - - dialog = builder.create(); - return dialog; - } - - @Override - public void onStart() { - super.onStart(); - - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - - // TODO(wgtdkp): - - startFetching(); - } - - @Override - public void onStop() { - super.onStop(); - - stopFetching(); - } - - @Override - public void onClick(DialogInterface dialogInterface, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - credentialListener.onConfirmClick(this, credentials); - } else { - stopFetching(); - credentialListener.onCancelClick(this); - } - } - - private void startFetching() { - Thread thread = - new Thread( - () -> { - new Handler(Looper.getMainLooper()) - .post( - () -> { - statusText.setText("petitioning..."); - }); - - String status; - - // TODO(wgtdkp): - final String statusCopy = ""; - - new Handler(Looper.getMainLooper()) - .post( - () -> { - statusText.setText(statusCopy); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - }); - }); - thread.start(); - } - - private void stopFetching() { - // TODO(wgtdkp): - } -} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FragmentCallback.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FragmentCallback.java index 24ea2f9e1..e74c6b76f 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FragmentCallback.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/FragmentCallback.java @@ -29,6 +29,20 @@ package io.openthread.commissioner.app; public interface FragmentCallback { + /** + * Called when the Operational Dataset is retrieved successfully ir failed. + * + * @param result can be one of RESULT_* of {@link android.app.Activity} + */ + void onRetrieveDatasetResult(int result); + + /** + * Called when the Operational Dataset is set successfully ir failed. + * + * @param result can be one of RESULT_* of {@link android.app.Activity} + */ + void onSetDatasetResult(int result); + /** * Called when adding a new Thread device has finished either successfully or failed. * diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/GetAdminPasscodeFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/GetAdminPasscodeFragment.java new file mode 100644 index 000000000..65d8dc40c --- /dev/null +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/GetAdminPasscodeFragment.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2024, The OpenThread Commissioner Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package io.openthread.commissioner.app; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import io.openthread.commissioner.app.BorderAgentDiscoverer.BorderAgentListener; + +public class GetAdminPasscodeFragment extends Fragment implements BorderAgentListener { + + // Indicates the flow of retrieving the dataset after getting the admin passcode + public static int FLOW_RETRIEVE_DATASET = 0; + + // Indicates the flow of setting the dataset after getting the admin passcode + public static int FLOW_SET_DATASET = 1; + + private static final String TAG = GetAdminPasscodeFragment.class.getSimpleName(); + + private final FragmentCallback fragmentCallback; + private final BorderAgentInfo borderAgentInfo; + private final int flow; + + private BorderAgentDiscoverer meshcopEpskcDiscoverer; + + private TextView waitForMeshcopETextView; + private TextView inputAdminPasscodeTextView; + private LinearLayout adminPasscodeEditLayout; + private EditText adminPasscodeEditText; + private Button retrieveDatasetButton; + + @Nullable private Integer epskcPort; + + public GetAdminPasscodeFragment( + FragmentCallback fragmentCallback, BorderAgentInfo borderAgentInfo, int flow) { + this.fragmentCallback = fragmentCallback; + this.borderAgentInfo = borderAgentInfo; + this.flow = flow; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_get_admin_passcode, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + meshcopEpskcDiscoverer = + new BorderAgentDiscoverer( + requireContext(), BorderAgentDiscoverer.MESHCOP_E_SERVICE_TYPE, this); + meshcopEpskcDiscoverer.start(); + + waitForMeshcopETextView = view.findViewById(R.id.wait_for_device_text); + inputAdminPasscodeTextView = view.findViewById(R.id.input_admin_passcode_text); + inputAdminPasscodeTextView.setVisibility(View.INVISIBLE); + + adminPasscodeEditLayout = view.findViewById(R.id.admin_passcode_layout); + adminPasscodeEditLayout.setVisibility(View.INVISIBLE); + adminPasscodeEditText = view.findViewById(R.id.admin_passcode_edit); + adminPasscodeEditText.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (s.length() >= 9) { + retrieveDatasetButton.setVisibility(View.VISIBLE); + } else { + retrieveDatasetButton.setVisibility(View.INVISIBLE); + } + } + }); + + retrieveDatasetButton = view.findViewById(R.id.retrieve_dataset_button); + retrieveDatasetButton.setText( + flow == FLOW_RETRIEVE_DATASET ? "Retrieve Dataset" : "Set Dataset"); + retrieveDatasetButton.setVisibility(View.INVISIBLE); + retrieveDatasetButton.setOnClickListener(this::onRetrieveDatasetClicked); + } + + private void onRetrieveDatasetClicked(View v) { + String passcode = adminPasscodeEditText.getText().toString().trim(); + if (passcode.length() != 9) { + throw new AssertionError("Admin passcode should always be 9 digits"); + } + + if (flow == GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET) { + FragmentUtils.moveToNextFragment( + this, + new RetrieveDatasetFragment(fragmentCallback, borderAgentInfo, passcode, epskcPort)); + } else if (flow == GetAdminPasscodeFragment.FLOW_SET_DATASET) { + FragmentUtils.moveToNextFragment( + this, new SetDatasetFragment(fragmentCallback, borderAgentInfo, passcode, epskcPort)); + } else { + throw new AssertionError("Unknown Admin Passcode flow: " + flow); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + Log.d(TAG, "::onDestroy"); + + meshcopEpskcDiscoverer.stop(); + } + + @Override + public void onResume() { + super.onResume(); + + Log.d(TAG, "::onResume"); + + meshcopEpskcDiscoverer.start(); + } + + @Override + public void onPause() { + super.onPause(); + + Log.d(TAG, "::onPause"); + + meshcopEpskcDiscoverer.stop(); + } + + @Override + public void onBorderAgentFound(BorderAgentInfo meshcopEInfo) { + if (meshcopEInfo.instanceName.equals(borderAgentInfo.instanceName)) { + Log.i(TAG, "Found the MeshCoP-ePSKc service for " + borderAgentInfo.instanceName); + + epskcPort = meshcopEInfo.port; + + requireActivity() + .runOnUiThread( + () -> { + waitForMeshcopETextView.setText( + "Device " + borderAgentInfo.instanceName + " is ready \u2713"); + inputAdminPasscodeTextView.setVisibility(View.VISIBLE); + adminPasscodeEditLayout.setVisibility(View.VISIBLE); + }); + } else { + Log.i(TAG, "Found new MeshCoP-ePSKc service " + meshcopEInfo.instanceName); + } + } +} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/InputNetworkPasswordFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/InputNetworkPasswordFragment.java index aa73387ec..5fd8b70c8 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/InputNetworkPasswordFragment.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/InputNetworkPasswordFragment.java @@ -44,17 +44,16 @@ import io.openthread.commissioner.ErrorCode; public class InputNetworkPasswordFragment extends Fragment { - private static final String TAG = InputNetworkPasswordFragment.class.getSimpleName(); private final FragmentCallback fragmentCallback; - private final ThreadNetworkInfoHolder selectedNetwork; + private final BorderAgentInfo selectedBorderAgent; private EditText passwordText; public InputNetworkPasswordFragment( - FragmentCallback fragmentCallback, ThreadNetworkInfoHolder selectedNetwork) { + FragmentCallback fragmentCallback, BorderAgentInfo selectedBorderAgent) { this.fragmentCallback = fragmentCallback; - this.selectedNetwork = selectedNetwork; + this.selectedBorderAgent = selectedBorderAgent; } @Override @@ -85,7 +84,11 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { } private void onPasswordInputted() { - byte[] pskc = computePskc(selectedNetwork.getNetworkInfo(), passwordText.getText().toString()); + byte[] pskc = + computePskc( + selectedBorderAgent.networkName, + selectedBorderAgent.extendedPanId, + passwordText.getText().toString()); if (pskc == null) { FragmentUtils.showAlertAndExit( @@ -94,23 +97,19 @@ private void onPasswordInputted() { } FragmentUtils.moveToNextFragment( - this, new ScanQrCodeFragment(fragmentCallback, selectedNetwork, pskc)); + this, new ScanQrCodeFragment(fragmentCallback, selectedBorderAgent, pskc)); } - private byte[] computePskc(ThreadNetworkInfo threadNetworkInfo, String password) { - ByteArray extendedPanId = new ByteArray(threadNetworkInfo.getExtendedPanId()); + private byte[] computePskc(String networkName, byte[] extendedPanId, String password) { ByteArray pskc = new ByteArray(); Error error = - Commissioner.generatePSKc( - pskc, password, threadNetworkInfo.getNetworkName(), extendedPanId); + Commissioner.generatePSKc(pskc, password, networkName, new ByteArray(extendedPanId)); if (error.getCode() != ErrorCode.kNone) { Log.e( TAG, String.format( "failed to generate PSKc: %s; network-name=%s, extended-pan-id=%s", - error, - threadNetworkInfo.getNetworkName(), - CommissionerUtils.getHexString(threadNetworkInfo.getExtendedPanId()))); + error, networkName, CommissionerUtils.getHexString(extendedPanId))); return null; } @@ -119,8 +118,8 @@ private byte[] computePskc(ThreadNetworkInfo threadNetworkInfo, String password) String.format( "generated pskc=%s, network-name=%s, extended-pan-id=%s", CommissionerUtils.getHexString(pskc), - threadNetworkInfo.getNetworkName(), - CommissionerUtils.getHexString(threadNetworkInfo.getExtendedPanId()))); + networkName, + CommissionerUtils.getHexString(extendedPanId))); return CommissionerUtils.getByteArray(pskc); } } diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/JoinerDeviceInfo.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/JoinerDeviceInfo.java index bea6807f5..93597de5f 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/JoinerDeviceInfo.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/JoinerDeviceInfo.java @@ -30,21 +30,18 @@ import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; public class JoinerDeviceInfo implements Parcelable { + private final byte[] eui64; + private final String pskd; - @NonNull private byte[] eui64; - - @NonNull private String pskd; - - public JoinerDeviceInfo(@NonNull byte[] eui64, @NonNull String pskd) { - this.eui64 = eui64; + public JoinerDeviceInfo(byte[] eui64, String pskd) { + this.eui64 = eui64.clone(); this.pskd = pskd; } public byte[] getEui64() { - return eui64; + return eui64.clone(); } public String getPskd() { diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/MainActivity.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/MainActivity.java index 3039fe150..391e3f94b 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/MainActivity.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/MainActivity.java @@ -33,7 +33,6 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; -import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -43,12 +42,6 @@ public class MainActivity extends AppCompatActivity implements FragmentCallback private static final String TAG = MainActivity.class.getSimpleName(); - @Nullable private ThreadNetworkInfoHolder selectedNetwork; - - @Nullable private byte[] pskc; - - @Nullable private JoinerDeviceInfo joinerDeviceInfo; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -61,7 +54,7 @@ protected void onCreate(Bundle savedInstanceState) { gitHash.setText(BuildConfig.GIT_HASH); if (savedInstanceState == null) { - showFragment(new SelectNetworkFragment(this, null), false); + showFragment(new SelectBorderRouterFragment(this), false); } } @@ -113,4 +106,41 @@ private void finishCommissioning(int result) { public void onAddDeviceResult(int result) { finishCommissioning(result); } + + @Override + public void onSetDatasetResult(int result) { + finishCommissioning(result); + } + + @Override + public void onRetrieveDatasetResult(int result) { + finishCommissioning(result); + } + + /* + public void onGetAdminPasscodeStarted(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow) { + showFragment( + new GetAdminPasscodeFragment(this, borderAgentInfo, adminPasscodeFlow), + true); + } + + public void onAdminPasscodeReceived( + BorderAgentInfo borderAgentInfo, int adminPasscodeFlow, String passcode, int epskcPort) { + if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET) { + showFragment( + new RetrieveDatasetFragment(this, borderAgentInfo, passcode, epskcPort), + true); + } else if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_SET_DATASET) { + showFragment( + new SetDatasetFragment(this, borderAgentInfo, passcode, epskcPort), + true); + } else { + throw new AssertionError("Unknown Admin Passcode flow: " + adminPasscodeFlow); + } + } + + public void onCredentialsRetrieved() { + clearFragmentsInBackStack(); + } + */ } diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/NetworkAdapter.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/NetworkAdapter.java deleted file mode 100644 index 24ef05598..000000000 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/NetworkAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2020, The OpenThread Commissioner Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package io.openthread.commissioner.app; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; -import io.openthread.commissioner.app.BorderAgentDiscoverer.BorderAgentListener; -import java.util.Arrays; -import java.util.Vector; - -public class NetworkAdapter extends BaseAdapter implements BorderAgentListener { - - private Vector networks; - - private LayoutInflater inflater; - - NetworkAdapter(Context context) { - inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - networks = new Vector<>(); - } - - public void addBorderAgent(BorderAgentInfo borderAgent) { - boolean hasExistingNetwork = false; - for (ThreadNetworkInfoHolder networkInfoHolder : networks) { - if (networkInfoHolder.getNetworkInfo().getNetworkName().equals(borderAgent.networkName) - && Arrays.equals( - networkInfoHolder.getNetworkInfo().getExtendedPanId(), borderAgent.extendedPanId)) { - networkInfoHolder.getBorderAgents().add(borderAgent); - hasExistingNetwork = true; - } - } - - if (!hasExistingNetwork) { - networks.add(new ThreadNetworkInfoHolder(borderAgent)); - } - - notifyDataSetChanged(); - } - - public void removeBorderAgent(byte[] lostBorderAgentId) { - for (ThreadNetworkInfoHolder networkInfoHolder : networks) { - for (BorderAgentInfo borderAgent : networkInfoHolder.getBorderAgents()) { - if (Arrays.equals(borderAgent.id, lostBorderAgentId)) { - networkInfoHolder.getBorderAgents().remove(borderAgent); - if (networkInfoHolder.getBorderAgents().isEmpty()) { - networks.remove(networkInfoHolder); - } - - notifyDataSetChanged(); - return; - } - } - } - } - - @Override - public int getCount() { - return networks.size(); - } - - @Override - public Object getItem(int position) { - return networks.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup container) { - if (convertView == null) { - convertView = inflater.inflate(R.layout.network_list_item, container, false); - } - TextView networkNameText = convertView.findViewById(R.id.network_name); - networkNameText.setText(networks.get(position).getNetworkInfo().getNetworkName()); - - TextView borderAgentIpAddrText = convertView.findViewById(R.id.border_agent_ip_addr); - borderAgentIpAddrText.setText( - networks.get(position).getBorderAgents().get(0).host.getHostAddress()); - return convertView; - } - - @Override - public void onBorderAgentFound(BorderAgentInfo borderAgentInfo) { - new Handler(Looper.getMainLooper()).post(() -> addBorderAgent(borderAgentInfo)); - } - - @Override - public void onBorderAgentLost(byte[] borderAgentId) { - new Handler(Looper.getMainLooper()).post(() -> removeBorderAgent(borderAgentId)); - } -} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/RetrieveDatasetFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/RetrieveDatasetFragment.java new file mode 100644 index 000000000..9e2f8c50b --- /dev/null +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/RetrieveDatasetFragment.java @@ -0,0 +1,233 @@ +package io.openthread.commissioner.app; + +import static com.google.common.io.BaseEncoding.base16; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.gms.threadnetwork.ThreadBorderAgent; +import com.google.android.gms.threadnetwork.ThreadNetwork; +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.primitives.Bytes; +import io.openthread.commissioner.ByteArray; +import io.openthread.commissioner.Commissioner; +import io.openthread.commissioner.CommissionerHandler; +import io.openthread.commissioner.Config; +import io.openthread.commissioner.Error; +import io.openthread.commissioner.ErrorCode; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Retrieves Thread Active Operational Dataset from a Border Router device and save it into Google + * Play Services. + */ +public class RetrieveDatasetFragment extends Fragment { + private static final String TAG = GetAdminPasscodeFragment.class.getSimpleName(); + + private final FragmentCallback fragmentCallback; + private final BorderAgentInfo borderAgentInfo; + private final String passcode; + private final int epskcPort; + + private TextView connectStatusText; + private TextView retrieveDatasetStatusText; + private TextView saveDatasetStatusText; + + private Button showDatasetButton; + + @Nullable private ThreadNetworkCredentials retrievedDataset; + + private ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); + + private CommissionerHandler commissionerHandler = + new CommissionerHandler() { + // FIXME(wgtdkp): we need to provide an override to the JNI callback, otherwise, it will + // crash + @Override + public void onKeepAliveResponse(Error error) { + Log.d(TAG, "received keep-alive response: " + error.toString()); + } + }; + + public RetrieveDatasetFragment( + FragmentCallback fragmentCallback, + BorderAgentInfo borderAgentInfo, + String passcode, + int epskcPort) { + this.fragmentCallback = fragmentCallback; + this.borderAgentInfo = borderAgentInfo; + this.passcode = passcode; + this.epskcPort = epskcPort; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_retrieve_dataset, container, false); + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + connectStatusText = view.findViewById(R.id.connected_to_br_text); + connectStatusText.setText("Connecting to " + borderAgentInfo.instanceName + "..."); + + retrieveDatasetStatusText = view.findViewById(R.id.dataset_retrieved_text); + retrieveDatasetStatusText.setVisibility(View.INVISIBLE); + + saveDatasetStatusText = view.findViewById(R.id.dataset_saved_text); + saveDatasetStatusText.setVisibility(View.INVISIBLE); + + showDatasetButton = view.findViewById(R.id.show_dataset_button); + showDatasetButton.setVisibility(View.INVISIBLE); + showDatasetButton.setOnClickListener(v -> showDataset()); + + view.findViewById(R.id.done_button) + .setOnClickListener(v -> fragmentCallback.onRetrieveDatasetResult(Activity.RESULT_OK)); + + backgroundExecutor.execute(this::retrieveDataset); + } + + private void retrieveDataset() { + // Create a commissioner candidate + + Commissioner commissioner = Commissioner.create(commissionerHandler); + Config config = new Config(); + config.setId("ePSKc demo"); + + if (passcode.getBytes(StandardCharsets.UTF_8).length != 9) { + throw new IllegalStateException(); + } + + config.setPSKc(new ByteArray(passcode.getBytes())); + config.setLogger(new NativeCommissionerLogger()); + config.setEnableCcm(false); + config.setEnableDtlsDebugLogging(true); + + Error error = commissioner.init(config); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to create commissioner: " + error.toString()); + return; + } + + // Connect to the Border Router + + String[] existingCommissionerId = new String[1]; + error = + commissioner.petition( + existingCommissionerId, borderAgentInfo.host.getHostAddress(), epskcPort); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to connect to Border Router: " + error.toString()); + return; + } + + getActivity().runOnUiThread(() -> connectStatusText.setText("Border Router connected \u2713")); + + // Retrieve Active Dataset + + ByteArray datasetTlvs = new ByteArray(); + error = commissioner.getRawActiveDataset(datasetTlvs, 0xffff); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to retrieve Thread credentials: " + error.toString()); + return; + } + + retrievedDataset = + ThreadNetworkCredentials.fromActiveOperationalDataset(Bytes.toArray(datasetTlvs)); + + getActivity() + .runOnUiThread( + () -> { + retrieveDatasetStatusText.setVisibility(View.VISIBLE); + showDatasetButton.setVisibility(View.VISIBLE); + }); + + // Save dataset to Google Play services + + Task task = + ThreadNetwork.getClient(getActivity()) + .addCredentials( + ThreadBorderAgent.newBuilder(borderAgentInfo.id).build(), retrievedDataset); + + try { + Tasks.await(task); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + commissioner.resign(); + } + + getActivity() + .runOnUiThread( + () -> { + saveDatasetStatusText.setVisibility(View.VISIBLE); + }); + } + + private void showDataset() { + if (retrievedDataset == null) { + return; + } + + String message = + "Network Name: " + + retrievedDataset.getNetworkName() + + "\n" + + "Extended PAN ID: " + + base16().encode(retrievedDataset.getExtendedPanId()) + + "\n" + + "PAN ID: " + + retrievedDataset.getPanId() + + "\n" + + "Channel: " + + retrievedDataset.getChannel() + + "\n" + + "Network Key: " + + base16().encode(retrievedDataset.getNetworkKey()) + + "\n" + + "PSKc: " + + base16().encode(retrievedDataset.getPskc()) + + "\n" + + "Mesh-local Prefix: " + + base16().encode(retrievedDataset.getMeshLocalPrefix()) + + "\n" + + "Raw TLV: " + + base16().encode(retrievedDataset.getActiveOperationalDataset()); + + new MaterialAlertDialogBuilder(getActivity()) + .setTitle("Thread Dataset") + .setMessage(message) + .setPositiveButton("OK", (dialog, which) -> {}) + .show(); + } + + private void fail(String errorMessage) { + getActivity().runOnUiThread(() -> showAlertDialog(errorMessage)); + } + + private void showAlertDialog(String message) { + new MaterialAlertDialogBuilder(getActivity(), R.style.ThreadNetworkAlertTheme) + .setMessage(message) + .setPositiveButton( + "OK", + (dialog, which) -> fragmentCallback.onRetrieveDatasetResult(Activity.RESULT_CANCELED)) + .show(); + } +} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ScanQrCodeFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ScanQrCodeFragment.java index f6701c47b..84f4c8154 100644 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ScanQrCodeFragment.java +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ScanQrCodeFragment.java @@ -62,21 +62,21 @@ public class ScanQrCodeFragment extends Fragment implements Detector.Processor onAddDeviceButtonClicked()); - - if (joinerDeviceInfo != null) { - String deviceInfoString = - String.format( - "eui64: %s\npskd: %s", - CommissionerUtils.getHexString(joinerDeviceInfo.getEui64()), - joinerDeviceInfo.getPskd()); - TextView deviceInfoView = view.findViewById(R.id.device_info); - deviceInfoView.setText(deviceInfoString); - } else { - view.findViewById(R.id.your_device).setVisibility(View.INVISIBLE); - view.findViewById(R.id.device_info).setVisibility(View.INVISIBLE); - } + addDeviceButton.setVisibility(View.INVISIBLE); - final ListView networkListView = view.findViewById(R.id.networks); - networkListView.setAdapter(networksAdapter); + final ListView borderAgentListView = view.findViewById(R.id.networks); + borderAgentListView.setAdapter(borderAgentAdapter); - networkListView.setOnItemClickListener( + borderAgentListView.setOnItemClickListener( (AdapterView adapterView, View v, int position, long id) -> { - selectedNetwork = (ThreadNetworkInfoHolder) adapterView.getItemAtPosition(position); + selectedBorderAgent = (BorderAgentInfo) adapterView.getItemAtPosition(position); + retrieveDatasetButton.setVisibility(View.VISIBLE); + setDatasetButton.setVisibility(View.VISIBLE); addDeviceButton.setVisibility(View.VISIBLE); }); - } - private void onAddDeviceButtonClicked() { - BorderAgentInfo selectedBorderAgent = selectedNetwork.getBorderAgents().get(0); + retrieveDatasetButton.setOnClickListener( + v -> { + if (validateSelectedBorderAgent()) { + FragmentUtils.moveToNextFragment( + this, + new GetAdminPasscodeFragment( + fragmentCallback, + selectedBorderAgent, + GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET)); + } + }); + setDatasetButton.setOnClickListener( + v -> { + if (validateSelectedBorderAgent()) { + FragmentUtils.moveToNextFragment( + this, + new GetAdminPasscodeFragment( + fragmentCallback, + selectedBorderAgent, + GetAdminPasscodeFragment.FLOW_SET_DATASET)); + } + }); - if (selectedBorderAgent.id == null) { - FragmentUtils.showAlertAndExit( - getActivity(), - fragmentCallback, - "Invalid Border Router", - "No \"id\" TXT entry found in Border Router mDNS service"); - return; - } + addDeviceButton.setOnClickListener(v -> onAddDeviceButtonClicked()); + } - if (selectedBorderAgent.extendedPanId == null) { - FragmentUtils.showAlertAndExit( - getActivity(), - fragmentCallback, - "Invalid Border Router", - "No \"xp\" TXT entry found in Border Router mDNS service"); + private void onAddDeviceButtonClicked() { + if (!validateSelectedBorderAgent()) { return; } - ThreadCommissionerServiceImpl commissionerService = + ThreadCommissionerService commissionerService = ThreadCommissionerServiceImpl.newInstance( getActivity(), /* intermediateStateCallback= */ null); @@ -175,13 +184,13 @@ private void onAddDeviceButtonClicked() { public void onSuccess(ThreadNetworkCredentials credentials) { if (credentials != null) { FragmentUtils.moveToNextFragment( - SelectNetworkFragment.this, + SelectBorderRouterFragment.this, new ScanQrCodeFragment( - fragmentCallback, selectedNetwork, credentials.getPskc())); + fragmentCallback, selectedBorderAgent, credentials.getPskc())); } else { FragmentUtils.moveToNextFragment( - SelectNetworkFragment.this, - new InputNetworkPasswordFragment(fragmentCallback, selectedNetwork)); + SelectBorderRouterFragment.this, + new InputNetworkPasswordFragment(fragmentCallback, selectedBorderAgent)); } } @@ -197,4 +206,26 @@ public void onFailure(Throwable t) { }, ContextCompat.getMainExecutor(getActivity())); } + + private boolean validateSelectedBorderAgent() { + if (selectedBorderAgent.id == null) { + FragmentUtils.showAlertAndExit( + getActivity(), + fragmentCallback, + "Invalid Border Router", + "No \"id\" TXT entry found in Border Router mDNS service"); + return false; + } + + if (selectedBorderAgent.extendedPanId == null) { + FragmentUtils.showAlertAndExit( + getActivity(), + fragmentCallback, + "Invalid Border Router", + "No \"xp\" TXT entry found in Border Router mDNS service"); + return false; + } + + return true; + } } diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/SetDatasetFragment.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/SetDatasetFragment.java new file mode 100644 index 000000000..b6a73a826 --- /dev/null +++ b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/SetDatasetFragment.java @@ -0,0 +1,242 @@ +package io.openthread.commissioner.app; + +import android.app.Activity; +import android.content.IntentSender; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.IntentSenderRequest; +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import com.google.android.gms.threadnetwork.ThreadNetwork; +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Bytes; +import io.openthread.commissioner.ByteArray; +import io.openthread.commissioner.Commissioner; +import io.openthread.commissioner.CommissionerHandler; +import io.openthread.commissioner.Config; +import io.openthread.commissioner.Error; +import io.openthread.commissioner.ErrorCode; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Retrieves Thread Active Operational Dataset from a Border Router device and save it into Google + * Play Services. + */ +public class SetDatasetFragment extends Fragment { + + private static final String TAG = GetAdminPasscodeFragment.class.getSimpleName(); + + private final FragmentCallback fragmentCallback; + private final BorderAgentInfo borderAgentInfo; + private final String passcode; + private final int epskcPort; + + private Button usePreferredButton; + + private TextView getPreferredStatusText; + private TextView connectStatusText; + private TextView setDatasetStatusText; + private TextView migrationNoteText; + private Button doneButton; + + private ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); + + private CommissionerHandler commissionerHandler = + new CommissionerHandler() { + // FIXME(wgtdkp): we need to provide an override to the JNI callback, otherwise, it will + // crash + @Override + public void onKeepAliveResponse(Error error) { + Log.d(TAG, "received keep-alive response: " + error.toString()); + } + }; + + private final ActivityResultLauncher preferredCredentialsLauncher = + registerForActivityResult( + new StartIntentSenderForResult(), + result -> { + int resultCode = result.getResultCode(); + if (resultCode == Activity.RESULT_OK) { + ThreadNetworkCredentials credentials = + ThreadNetworkCredentials.fromIntentSenderResultData( + Preconditions.checkNotNull(result.getData())); + getPreferredStatusText.setText("Preferred credentials loaded \u2713"); + backgroundExecutor.execute(() -> setDataset(credentials)); + } else { + getPreferredStatusText.setText("Denied to share!"); + doneButton.setVisibility(View.VISIBLE); + } + }); + + public SetDatasetFragment( + FragmentCallback fragmentCallback, + BorderAgentInfo borderAgentInfo, + String passcode, + int epskcPort) { + this.fragmentCallback = fragmentCallback; + this.borderAgentInfo = borderAgentInfo; + this.passcode = passcode; + this.epskcPort = epskcPort; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_set_dataset, container, false); + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + usePreferredButton = view.findViewById(R.id.use_preferred_button); + usePreferredButton.setOnClickListener(this::getPreferredCredentials); + + getPreferredStatusText = view.findViewById(R.id.get_preferred_text); + getPreferredStatusText.setVisibility(View.INVISIBLE); + + connectStatusText = view.findViewById(R.id.connected_to_br_text); + connectStatusText.setText("Connecting to " + borderAgentInfo.instanceName + "..."); + connectStatusText.setVisibility(View.INVISIBLE); + + setDatasetStatusText = view.findViewById(R.id.set_dataset_text); + setDatasetStatusText.setVisibility(View.INVISIBLE); + + migrationNoteText = view.findViewById(R.id.network_migration_text); + migrationNoteText.setVisibility(View.INVISIBLE); + + doneButton = view.findViewById(R.id.done_button); + doneButton.setVisibility(View.INVISIBLE); + doneButton.setOnClickListener(v -> fragmentCallback.onSetDatasetResult(Activity.RESULT_OK)); + } + + private void getPreferredCredentials(View v) { + getPreferredStatusText.setVisibility(View.VISIBLE); + + ThreadNetwork.getClient(getActivity()) + .getPreferredCredentials() + .addOnSuccessListener( + intentSenderResult -> { + IntentSender intentSender = intentSenderResult.getIntentSender(); + if (intentSender != null) { + preferredCredentialsLauncher.launch( + new IntentSenderRequest.Builder(intentSender).build()); + } else { + getPreferredStatusText.setText("No preferred credentials found!"); + } + }) + .addOnFailureListener(e -> getPreferredStatusText.setText(e.getMessage())); + } + + private void setDataset(ThreadNetworkCredentials credentials) { + Preconditions.checkNotNull(credentials); + + Log.i(TAG, "Start setting dataset: " + credentials); + + getActivity() + .runOnUiThread( + () -> { + connectStatusText.setVisibility(View.VISIBLE); + }); + + // Create a commissioner candidate + + Commissioner commissioner = Commissioner.create(commissionerHandler); + Config config = new Config(); + config.setId("Set dataset"); + + if (passcode.getBytes(StandardCharsets.UTF_8).length != 9) { + fail("Invalid passcode: " + passcode); + return; + } + + config.setPSKc(new ByteArray(passcode.getBytes())); + config.setLogger(new NativeCommissionerLogger()); + config.setEnableCcm(false); + config.setEnableDtlsDebugLogging(true); + + Error error = commissioner.init(config); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to create commissioner: " + error.toString()); + return; + } + + // Connect to the Border Router + + String[] existingCommissionerId = new String[1]; + error = + commissioner.petition( + existingCommissionerId, borderAgentInfo.host.getHostAddress(), epskcPort); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to connect to Border Router: " + error.toString()); + return; + } + + getActivity() + .runOnUiThread( + () -> { + connectStatusText.setText("Border Router connected \u2713"); + }); + + // Set Pending Dataset + + error = commissioner.setRawPendingDataset(makePendingDataset(credentials)); + if (error.getCode() != ErrorCode.kNone) { + fail("Failed to set Pending Dataset: " + error.toString()); + commissioner.resign(); + return; + } + + getActivity() + .runOnUiThread( + () -> { + setDatasetStatusText.setVisibility(View.VISIBLE); + migrationNoteText.setVisibility(View.VISIBLE); + doneButton.setVisibility(View.VISIBLE); + }); + + commissioner.resign(); + } + + /** + * Creates a Pending Dataset for the given Active Dataset with the Pending Timestamp set to the + * current date time. + */ + private static ByteArray makePendingDataset(ThreadNetworkCredentials activeDataset) { + byte[] datasetTlvs = activeDataset.getActiveOperationalDataset(); + + byte[] delayTimer = new byte[] {52, 4, 0, 0, 0x75, 0x30}; // 30 seconds + + long now = Instant.now().getEpochSecond(); + byte[] pendingTimestamp = new byte[10]; + ByteBuffer buffer = ByteBuffer.wrap(pendingTimestamp); + buffer.put(new byte[] {51, 8}).putLong(now << 16); + + return new ByteArray(Bytes.concat(delayTimer, pendingTimestamp, datasetTlvs)); + } + + private void fail(String errorMessage) { + getActivity().runOnUiThread(() -> showAlertDialog(errorMessage)); + } + + private void showAlertDialog(String message) { + new MaterialAlertDialogBuilder(getActivity(), R.style.ThreadNetworkAlertTheme) + .setMessage(message) + .setPositiveButton( + "OK", (dialog, which) -> fragmentCallback.onRetrieveDatasetResult(Activity.RESULT_OK)) + .show(); + } +} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfo.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfo.java deleted file mode 100644 index 6afed98df..000000000 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfo.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020, The OpenThread Commissioner Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package io.openthread.commissioner.app; - -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; - -public class ThreadNetworkInfo implements Parcelable { - - @NonNull private final String networkName; - - @NonNull private final byte[] extendedPanId; - - public ThreadNetworkInfo(@NonNull String networkName, @NonNull byte[] extendedPanId) { - this.networkName = networkName; - this.extendedPanId = extendedPanId; - } - - protected ThreadNetworkInfo(Parcel in) { - networkName = in.readString(); - extendedPanId = in.createByteArray(); - } - - public String getNetworkName() { - return networkName; - } - - public byte[] getExtendedPanId() { - return extendedPanId; - } - - public static final Creator CREATOR = - new Creator() { - @Override - public ThreadNetworkInfo createFromParcel(Parcel in) { - return new ThreadNetworkInfo(in); - } - - @Override - public ThreadNetworkInfo[] newArray(int size) { - return new ThreadNetworkInfo[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeString(networkName); - parcel.writeByteArray(extendedPanId); - } - - @Override - public String toString() { - return String.format( - "{name=%s, extendedPanId=%s}", networkName, CommissionerUtils.getHexString(extendedPanId)); - } -} diff --git a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfoHolder.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfoHolder.java deleted file mode 100644 index 59b27fc32..000000000 --- a/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/ThreadNetworkInfoHolder.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2020, The OpenThread Commissioner Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package io.openthread.commissioner.app; - -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import java.util.ArrayList; - -public class ThreadNetworkInfoHolder implements Parcelable { - @NonNull private final ThreadNetworkInfo networkInfo; - - @NonNull private final ArrayList borderAgents; - - public ThreadNetworkInfoHolder(BorderAgentInfo borderAgent) { - networkInfo = new ThreadNetworkInfo(borderAgent.networkName, borderAgent.extendedPanId); - borderAgents = new ArrayList<>(); - borderAgents.add(borderAgent); - } - - protected ThreadNetworkInfoHolder(Parcel in) { - networkInfo = in.readParcelable(ThreadNetworkInfo.class.getClassLoader()); - borderAgents = in.createTypedArrayList(BorderAgentInfo.CREATOR); - } - - public static final Creator CREATOR = - new Creator() { - @Override - public ThreadNetworkInfoHolder createFromParcel(Parcel in) { - return new ThreadNetworkInfoHolder(in); - } - - @Override - public ThreadNetworkInfoHolder[] newArray(int size) { - return new ThreadNetworkInfoHolder[size]; - } - }; - - public ThreadNetworkInfo getNetworkInfo() { - return networkInfo; - } - - public ArrayList getBorderAgents() { - return borderAgents; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(networkInfo, flags); - dest.writeParcelableArray((BorderAgentInfo[]) borderAgents.toArray(), flags); - } -} diff --git a/android/openthread_commissioner/app/src/main/res/layout/activity_main.xml b/android/openthread_commissioner/app/src/main/res/layout/activity_main.xml index 620e730f0..658edffd8 100644 --- a/android/openthread_commissioner/app/src/main/res/layout/activity_main.xml +++ b/android/openthread_commissioner/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,7 @@ android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".app.MainActivity"> + tools:context=".MainActivity"> + + + + + + + + + + + + + diff --git a/android/openthread_commissioner/app/src/main/res/layout/fragment_get_admin_passcode.xml b/android/openthread_commissioner/app/src/main/res/layout/fragment_get_admin_passcode.xml new file mode 100644 index 000000000..63be64971 --- /dev/null +++ b/android/openthread_commissioner/app/src/main/res/layout/fragment_get_admin_passcode.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + +