From f3d23980fb751c410215bbf74ab350f2382bd523 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 +- .../commissioner/app/MainActivity.java | 44 ++- .../service/build.gradle | 6 +- .../service/BorderAgentAdapter.java | 138 ++++++++ .../service/BorderAgentDiscoverer.java | 313 ++++++++++-------- .../commissioner/service/BorderAgentInfo.java | 43 ++- .../service/CameraSourceView.java | 18 +- .../service/CommissionerActivity.java | 33 +- .../service/FragmentCallback.java | 8 + .../service/GetAdminPasscodeFragment.java | 182 ++++++++++ .../service/JoinerDeviceInfo.java | 13 +- .../commissioner/service/NetworkAdapter.java | 126 ------- .../service/RetrieveDatasetFragment.java | 169 ++++++++++ .../service/ScanQrCodeFragment.java | 6 +- .../service/SelectBorderRouterFragment.java | 152 +++++++++ .../service/SelectNetworkFragment.java | 258 --------------- .../service/SetDatasetFragment.java | 234 +++++++++++++ .../res/layout/border_agent_list_item.xml | 49 +++ .../layout/fragment_get_admin_passcode.xml | 74 +++++ .../res/layout/fragment_retrieve_dataset.xml | 63 ++++ ....xml => fragment_select_border_router.xml} | 82 +++-- .../main/res/layout/fragment_set_dataset.xml | 84 +++++ .../src/main/res/layout/network_list_item.xml | 24 -- .../service/src/main/res/values/strings.xml | 4 + .../service/src/main/res/values/styles.xml | 2 +- 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 + 30 files changed, 1545 insertions(+), 645 deletions(-) create mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentAdapter.java create mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/GetAdminPasscodeFragment.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkAdapter.java create mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/RetrieveDatasetFragment.java create mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectNetworkFragment.java create mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SetDatasetFragment.java create mode 100644 android/openthread_commissioner/service/src/main/res/layout/border_agent_list_item.xml create mode 100644 android/openthread_commissioner/service/src/main/res/layout/fragment_get_admin_passcode.xml create mode 100644 android/openthread_commissioner/service/src/main/res/layout/fragment_retrieve_dataset.xml rename android/openthread_commissioner/service/src/main/res/layout/{fragment_select_network.xml => fragment_select_border_router.xml} (64%) create mode 100644 android/openthread_commissioner/service/src/main/res/layout/fragment_set_dataset.xml delete mode 100644 android/openthread_commissioner/service/src/main/res/layout/network_list_item.xml diff --git a/android/build-commissioner-libs.sh b/android/build-commissioner-libs.sh index 1054904d8..b5bd64058 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/src/main/java/io/openthread/commissioner/app/MainActivity.java b/android/openthread_commissioner/app/src/main/java/io/openthread/commissioner/app/MainActivity.java index 22df9feca..4726be75f 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 @@ -40,22 +40,29 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import io.openthread.commissioner.service.BorderAgentInfo; import io.openthread.commissioner.service.FragmentCallback; +import io.openthread.commissioner.service.GetAdminPasscodeFragment; import io.openthread.commissioner.service.JoinerDeviceInfo; import io.openthread.commissioner.service.MeshcopFragment; +import io.openthread.commissioner.service.RetrieveDatasetFragment; +import io.openthread.commissioner.service.SetDatasetFragment; import io.openthread.commissioner.service.ScanQrCodeFragment; -import io.openthread.commissioner.service.SelectNetworkFragment; +import io.openthread.commissioner.service.SelectBorderRouterFragment; import io.openthread.commissioner.service.ThreadNetworkInfoHolder; public class MainActivity extends AppCompatActivity implements FragmentCallback { private static final String TAG = MainActivity.class.getSimpleName(); - @Nullable private ThreadNetworkInfoHolder selectedNetwork; + @Nullable + private ThreadNetworkInfoHolder selectedNetwork; - @Nullable private byte[] pskc; + @Nullable + private byte[] pskc; - @Nullable private JoinerDeviceInfo joinerDeviceInfo; + @Nullable + private JoinerDeviceInfo joinerDeviceInfo; @Override protected void onCreate(Bundle savedInstanceState) { @@ -69,7 +76,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); } } @@ -154,4 +161,31 @@ public void onJoinerInfoReceived(@Nullable JoinerDeviceInfo joinerDeviceInfo) { public void onMeshcopResult(int result) { finishCommissioning(result); } + + @Override + public void onGetAdminPasscodeStarted(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow) { + showFragment(new GetAdminPasscodeFragment(this, borderAgentInfo, adminPasscodeFlow), /* addToBackStack= */ true); + } + + @Override + public void onAdminPasscodeReceived(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow, String passcode, + int epskcPort) { + if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET) { + showFragment(new RetrieveDatasetFragment(this, borderAgentInfo, passcode, + epskcPort), /* addToBackStack= */ true); + } else if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_SET_DATASET) { + showFragment(new SetDatasetFragment(this, borderAgentInfo, passcode, + epskcPort), /* addToBackStack= */ true); + } else { + throw new AssertionError("Unknown Admin Passcode flow: " + adminPasscodeFlow); + } + } + + @Override + public void onCredentialsRetrieved() { + selectedNetwork = null; + pskc = null; + joinerDeviceInfo = null; + clearFragmentsInBackStack(); + } } diff --git a/android/openthread_commissioner/service/build.gradle b/android/openthread_commissioner/service/build.gradle index 4f9dfeb36..38f3b5b33 100644 --- a/android/openthread_commissioner/service/build.gradle +++ b/android/openthread_commissioner/service/build.gradle @@ -73,8 +73,12 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) + // Fix Duplicate class + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + implementation 'com.google.guava:guava:31.1-jre' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.activity:activity:1.8.2' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation "androidx.concurrent:concurrent-futures:1.1.0" implementation 'androidx.constraintlayout:constraintlayout:2.0.2' implementation 'androidx.navigation:navigation-fragment:2.3.0' diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentAdapter.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentAdapter.java new file mode 100644 index 000000000..9f8c4be36 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentAdapter.java @@ -0,0 +1,138 @@ +package io.openthread.commissioner.service; + +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.service.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/service/src/main/java/io/openthread/commissioner/service/BorderAgentDiscoverer.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDiscoverer.java index 6e4214d12..a99b9756d 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDiscoverer.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDiscoverer.java @@ -34,8 +34,10 @@ import android.net.nsd.NsdServiceInfo; import android.net.wifi.WifiManager; import android.util.Log; + import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; + import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -45,185 +47,202 @@ public class BorderAgentDiscoverer implements NsdManager.DiscoveryListener { - private static final String TAG = BorderAgentDiscoverer.class.getSimpleName(); + private static final String TAG = BorderAgentDiscoverer.class.getSimpleName(); - private static final String SERVICE_TYPE = "_meshcop._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"; + 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 ExecutorService executor = Executors.newSingleThreadExecutor(); - private BlockingQueue unresolvedServices = new ArrayBlockingQueue<>(256); - private AtomicBoolean isResolvingService = new AtomicBoolean(false); + private final String serviceType; + private final BorderAgentListener borderAgentListener; - private boolean isScanning = false; + private ExecutorService executor = Executors.newSingleThreadExecutor(); + private BlockingQueue unresolvedServices = new ArrayBlockingQueue<>(256); + private AtomicBoolean isResolvingService = new AtomicBoolean(false); - public interface BorderAgentListener { + private boolean isScanning = false; - void onBorderAgentFound(BorderAgentInfo borderAgentInfo); + public interface BorderAgentListener { - void onBorderAgentLost(byte[] id); - } + void onBorderAgentFound(BorderAgentInfo borderAgentInfo); - @RequiresPermission(permission.INTERNET) - public BorderAgentDiscoverer(Context context, BorderAgentListener borderAgentListener) { - WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - wifiMulticastLock = wifi.createMulticastLock("multicastLock"); + //void onBorderAgentLost(byte[] id); - nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + default void onBorderAgentLost(boolean isEpskcService, String instanceName) { + } + } - this.borderAgentListener = borderAgentListener; - } + @RequiresPermission(permission.INTERNET) + public BorderAgentDiscoverer(Context context, String serviceType, BorderAgentListener borderAgentListener) { + WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + wifiMulticastLock = wifi.createMulticastLock("multicastLock"); - public void start() { - if (isScanning) { - Log.w(TAG, "the Border Agent discoverer is already running!"); - return; + nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + + this.serviceType = serviceType; + this.borderAgentListener = borderAgentListener; } - isScanning = true; - - wifiMulticastLock.setReferenceCounted(true); - wifiMulticastLock.acquire(); - - startResolver(); - nsdManager.discoverServices( - BorderAgentDiscoverer.SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, this); - } - - private void startResolver() { - NsdManager.ResolveListener listener = - new NsdManager.ResolveListener() { - @Override - public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { - Log.e( - TAG, - String.format( - "failed to resolve service %s, error: %d, this=%s", - serviceInfo.toString(), errorCode, this)); - isResolvingService.set(false); - } - - @Override - 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.getHost().getCanonicalHostName()); - borderAgentListener.onBorderAgentFound(borderAgent); - } - isResolvingService.set(false); - } - }; - - Log.d(TAG, "mDNS resolve listener is " + listener); - - if (executor.isTerminated()) { - executor = Executors.newSingleThreadExecutor(); + public void start() { + if (isScanning) { + Log.w(TAG, "the Border Agent discoverer is already running!"); + return; + } + + isScanning = true; + + wifiMulticastLock.setReferenceCounted(true); + wifiMulticastLock.acquire(); + + startResolver(); + + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, this); } - executor.submit( - () -> { - while (true) { - if (!isResolvingService.get()) { - NsdServiceInfo serviceInfo = unresolvedServices.take(); - - isResolvingService.set(true); - nsdManager.resolveService(serviceInfo, listener); - } - } - }); - } - - private void stopResolver() { - if (!executor.isTerminated()) { - executor.shutdownNow(); + private void startResolver() { + NsdManager.ResolveListener listener = + new NsdManager.ResolveListener() { + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + Log.e( + TAG, + String.format( + "failed to resolve service %s, error: %d, this=%s", + serviceInfo.toString(), errorCode, this)); + isResolvingService.set(false); + } + + @Override + 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.getHost().getCanonicalHostName()); + borderAgentListener.onBorderAgentFound(borderAgent); + } + isResolvingService.set(false); + } + }; + + Log.d(TAG, "mDNS resolve listener is " + listener); + + if (executor.isTerminated()) { + executor = Executors.newSingleThreadExecutor(); + } + + executor.submit( + () -> { + while (true) { + if (!isResolvingService.get()) { + NsdServiceInfo serviceInfo = unresolvedServices.take(); + + isResolvingService.set(true); + nsdManager.resolveService(serviceInfo, listener); + } + } + }); } - isResolvingService.set(false); - unresolvedServices.clear(); - } - - public void stop() { - if (!isScanning) { - Log.w(TAG, "the Border Agent discoverer has already been stopped!"); - return; + + private void stopResolver() { + if (!executor.isTerminated()) { + executor.shutdownNow(); + } + isResolvingService.set(false); + unresolvedServices.clear(); } - nsdManager.stopServiceDiscovery(this); - stopResolver(); + public void stop() { + if (!isScanning) { + Log.w(TAG, "the Border Agent discoverer has already been stopped!"); + return; + } - if (wifiMulticastLock != null) { - wifiMulticastLock.release(); + nsdManager.stopServiceDiscovery(this); + stopResolver(); + + if (wifiMulticastLock != null) { + wifiMulticastLock.release(); + } + + isScanning = false; } - isScanning = false; - } + @Override + public void onDiscoveryStarted(String serviceType) { + Log.d(TAG, "start discovering Border Agent: " + serviceType); + } - @Override - public void onDiscoveryStarted(String serviceType) { - Log.d(TAG, "start discovering Border Agent"); - } + @Override + public void onDiscoveryStopped(String serviceType) { + Log.d(TAG, "stop discovering Border Agent"); + } - @Override - public void onDiscoveryStopped(String serviceType) { - Log.d(TAG, "stop discovering Border Agent"); - } + @Override + public void onServiceFound(NsdServiceInfo nsdServiceInfo) { + Log.d(TAG, "a Border Agent service found"); - @Override - public void onServiceFound(NsdServiceInfo nsdServiceInfo) { - Log.d(TAG, "a Border Agent service found"); + unresolvedServices.offer(nsdServiceInfo); + } - unresolvedServices.offer(nsdServiceInfo); - } + @Override + public void onServiceLost(NsdServiceInfo nsdServiceInfo) { + Log.d(TAG, "a Border Agent service is gone: " + nsdServiceInfo.getServiceName()); - @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); + borderAgentListener.onBorderAgentLost( + serviceType.equals(MESHCOP_E_SERVICE_TYPE), nsdServiceInfo.getServiceName()); } - } - @Override - public void onStartDiscoveryFailed(String serviceType, int errorCode) { - Log.d(TAG, "start discovering Border Agent failed: " + errorCode); - } + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + Log.d(TAG, "start discovering Border Agent failed: " + errorCode); + } - @Override - public void onStopDiscoveryFailed(String serviceType, int errorCode) { - Log.d(TAG, "stop discovering Border Agent failed: " + errorCode); - } + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + Log.d(TAG, "stop discovering Border Agent failed: " + errorCode); + } - @Nullable - private BorderAgentInfo getBorderAgentInfo(NsdServiceInfo serviceInfo) { - Map attrs = serviceInfo.getAttributes(); - byte[] id = getBorderAgentId(serviceInfo); + @Nullable + private BorderAgentInfo getBorderAgentInfo(NsdServiceInfo serviceInfo) { + Map attrs = serviceInfo.getAttributes(); + byte[] id = getBorderAgentId(serviceInfo); + + return new BorderAgentInfo( + serviceType.equals(MESHCOP_E_SERVICE_TYPE), + serviceInfo.getServiceName(), + 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)); + } - if (!attrs.containsKey(KEY_NETWORK_NAME) || !attrs.containsKey(KEY_EXTENDED_PAN_ID)) { - return null; + @Nullable + private static String getStringAttribute(Map attributes, String key) { + byte[] value = attributes.get(key); + if (value == null) { + return null; + } + return new String(value); } - return new BorderAgentInfo( - id, - new String(attrs.get(KEY_NETWORK_NAME)), - attrs.get(KEY_EXTENDED_PAN_ID), - serviceInfo.getHost(), - serviceInfo.getPort()); - } - - @Nullable - private byte[] getBorderAgentId(NsdServiceInfo serviceInfo) { - Map attrs = serviceInfo.getAttributes(); - if (attrs.containsKey(KEY_ID)) { - return attrs.get(KEY_ID).clone(); + @Nullable + private byte[] getBorderAgentId(NsdServiceInfo serviceInfo) { + Map attrs = serviceInfo.getAttributes(); + if (attrs.containsKey(KEY_ID)) { + return attrs.get(KEY_ID).clone(); + } + return null; } - return null; - } } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentInfo.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentInfo.java index 0a9babede..1174f94f6 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentInfo.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentInfo.java @@ -35,27 +35,42 @@ import java.net.UnknownHostException; public class BorderAgentInfo implements Parcelable { - - public byte[] id; - public String networkName; - public byte[] extendedPanId; + public final boolean isEpskcService; + public final String instanceName; + public final byte[] id; + public final String networkName; + public final byte[] extendedPanId; public InetAddress host; public int port; + public final String vendorName; + + public final String modelName; + public BorderAgentInfo( - @NonNull byte[] id, - @NonNull String networkName, - @NonNull byte[] extendedPanId, - @NonNull InetAddress host, - @NonNull int port) { - this.id = id.clone(); + boolean isEpskcService, + String instanceName, + byte[] id, + String networkName, + byte[] extendedPanId, + InetAddress host, + int port, + String vendorName, + String modelName) { + this.isEpskcService = isEpskcService; + this.instanceName = instanceName; + this.id = id == null ? null : id.clone(); this.networkName = networkName; - this.extendedPanId = extendedPanId.clone(); + 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(); id = in.createByteArray(); networkName = in.readString(); extendedPanId = in.createByteArray(); @@ -64,15 +79,21 @@ protected BorderAgentInfo(Parcel in) { } 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.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/service/src/main/java/io/openthread/commissioner/service/CameraSourceView.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CameraSourceView.java index 7ca189bf8..8d06f828d 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CameraSourceView.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CameraSourceView.java @@ -35,6 +35,8 @@ 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,15 +48,14 @@ 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) { + public CameraSourceView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; @@ -65,7 +66,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 +81,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 +132,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/service/src/main/java/io/openthread/commissioner/service/CommissionerActivity.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerActivity.java index 06c7ce62f..274bd3b91 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerActivity.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerActivity.java @@ -38,6 +38,7 @@ import androidx.fragment.app.Fragment; public class CommissionerActivity extends AppCompatActivity implements FragmentCallback { + private static final String TAG = CommissionerActivity.class.getSimpleName(); public static final String ACTION_COMMISSION_DEVICE = @@ -45,8 +46,6 @@ public class CommissionerActivity extends AppCompatActivity implements FragmentC public static final String ACTION_FETCH_CREDENTIAL = "io.openthread.commissioner.action.FETCH_CREDENTIAL"; - public static final String KEY_JOINER_DISCERNER = "JOINER_DISCERNER"; - public static final String KEY_JOINER_DISCERNER_BIT_LENGTH = "JOINER_DISCERNER_BIT_LENGTH"; public static final String KEY_JOINER_EUI64 = "JOINER_EUI64"; public static final String KEY_JOINER_PSKD = "JOINER_PSKD"; @@ -65,7 +64,7 @@ protected void onCreate(Bundle savedInstanceState) { // TODO(wgtdkp): scan QR code of the joiner device. showFragment(new ScanQrCodeFragment(this)); } else { - showFragment(new SelectNetworkFragment(this, joinerDeviceInfo)); + showFragment(new SelectBorderRouterFragment(this, joinerDeviceInfo)); } } else if (ACTION_FETCH_CREDENTIAL.equals(intent.getAction())) { // TODO(wgtdkp): @@ -110,7 +109,8 @@ public void showFragment(Fragment fragment) { @Override public void onJoinerInfoReceived(@Nullable JoinerDeviceInfo joinerDeviceInfo) { - showFragment(new SelectNetworkFragment(this, joinerDeviceInfo)); + //showFragment(new SelectNetworkFragment(this, joinerDeviceInfo)); + // TODO(wgtdkp): goto commissioning fragment. } @Override @@ -123,4 +123,29 @@ public void onNetworkSelected( public void onMeshcopResult(int result) { // TODO(wgtdkp): } + + @Override + public void onGetAdminPasscodeStarted(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow) { + showFragment(new GetAdminPasscodeFragment(this, borderAgentInfo, adminPasscodeFlow)); + } + + @Override + public void onAdminPasscodeReceived(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow, + String passcode, + int epskcPort) { + if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET) { + showFragment( + new RetrieveDatasetFragment(this, borderAgentInfo, passcode, epskcPort)); + } else if (adminPasscodeFlow == GetAdminPasscodeFragment.FLOW_SET_DATASET) { + showFragment( + new SetDatasetFragment(this, borderAgentInfo, passcode, epskcPort)); + } else { + throw new AssertionError("Unknown Admin Passcode flow: " + adminPasscodeFlow); + } + } + + @Override + public void onCredentialsRetrieved() { + // TODO: + } } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FragmentCallback.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FragmentCallback.java index 98aeeb2f4..9e67ca378 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FragmentCallback.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FragmentCallback.java @@ -37,4 +37,12 @@ void onNetworkSelected( @Nullable ThreadNetworkInfoHolder networkInfoHolder, @Nullable byte[] pskc); void onMeshcopResult(int result); + + /** Called when GetAdminPasscode is started. */ + void onGetAdminPasscodeStarted(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow); + + /** Called when the user inputted the passcode for a given Border Router device. */ + void onAdminPasscodeReceived(BorderAgentInfo borderAgentInfo, int adminPasscodeFlow, String passcode, int epskcPort); + + void onCredentialsRetrieved(); } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/GetAdminPasscodeFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/GetAdminPasscodeFragment.java new file mode 100644 index 000000000..86167a709 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/GetAdminPasscodeFragment.java @@ -0,0 +1,182 @@ +/* + * 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.service; + +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.service.BorderAgentDiscoverer.BorderAgentListener; +import java.util.Objects; + +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); + } + + 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"); + } + + fragmentCallback.onAdminPasscodeReceived(borderAgentInfo, flow, passcode, + Objects.requireNonNull(epskcPort)); + } + + @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/service/src/main/java/io/openthread/commissioner/service/JoinerDeviceInfo.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/JoinerDeviceInfo.java index aacdc2a61..455fc2b77 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/JoinerDeviceInfo.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/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/service/src/main/java/io/openthread/commissioner/service/NetworkAdapter.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkAdapter.java deleted file mode 100644 index ebb259621..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/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.service; - -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.service.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)); - } - - new Handler(Looper.getMainLooper()).post(() -> 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); - } - - new Handler(Looper.getMainLooper()).post(() -> 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) { - addBorderAgent(borderAgentInfo); - } - - @Override - public void onBorderAgentLost(byte[] borderAgentId) { - removeBorderAgent(borderAgentId); - } -} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/RetrieveDatasetFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/RetrieveDatasetFragment.java new file mode 100644 index 000000000..0398fd2f6 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/RetrieveDatasetFragment.java @@ -0,0 +1,169 @@ +package io.openthread.commissioner.service; + +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.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 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); + + ((Button)view.findViewById(R.id.done_button)).setOnClickListener(v -> { + fragmentCallback.onCredentialsRetrieved(); + }); + + 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; + } + + getActivity().runOnUiThread(() -> { + retrieveDatasetStatusText.setVisibility(View.VISIBLE); + }); + + // Save dataset to Google Play services + + Task task = + ThreadNetwork.getClient(getActivity()) + .addCredentials( + ThreadBorderAgent.newBuilder(borderAgentInfo.id).build(), + ThreadNetworkCredentials.fromActiveOperationalDataset(Bytes.toArray(datasetTlvs))); + + 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 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.onCredentialsRetrieved(); + })).show(); + } +} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ScanQrCodeFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ScanQrCodeFragment.java index 229a6003d..1b987a5ff 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ScanQrCodeFragment.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ScanQrCodeFragment.java @@ -153,11 +153,7 @@ public void onRequestPermissionsResult( + (grantResults.length > 0 ? grantResults[0] : "(empty)")); DialogInterface.OnClickListener listener = - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - joinerInfoCallback.onJoinerInfoReceived(null); - } - }; + (dialog, id) -> joinerInfoCallback.onJoinerInfoReceived(null); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java new file mode 100644 index 000000000..ed24fdd94 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java @@ -0,0 +1,152 @@ +/* + * 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.service; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.ListView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +public class SelectBorderRouterFragment extends Fragment { + + private static final String TAG = SelectBorderRouterFragment.class.getSimpleName(); + + private final FragmentCallback networkInfoCallback; + + @Nullable + private final JoinerDeviceInfo joinerDeviceInfo; + + private BorderAgentAdapter borderAgentAdapter; + + private Button retrieveDatasetButton; + private Button setDatasetButton; + + private BorderAgentDiscoverer meshcopDiscoverer; + private BorderAgentDiscoverer meshcopEpskcDiscoverer; + + private BorderAgentInfo selectedBorderAgent; + + public SelectBorderRouterFragment(FragmentCallback networkInfoCallback) { + this(networkInfoCallback, /* joinerDeviceInfo= */ null); + } + + public SelectBorderRouterFragment(FragmentCallback networkInfoCallback, @Nullable JoinerDeviceInfo joinerDeviceInfo) { + this.networkInfoCallback = networkInfoCallback; + this.joinerDeviceInfo = joinerDeviceInfo; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Log.d(TAG, "::onCreate"); + + borderAgentAdapter = new BorderAgentAdapter(requireContext()); + meshcopDiscoverer = new BorderAgentDiscoverer(requireContext(), + BorderAgentDiscoverer.MESHCOP_SERVICE_TYPE, borderAgentAdapter); + meshcopDiscoverer.start(); + meshcopEpskcDiscoverer = new BorderAgentDiscoverer(requireContext(), + BorderAgentDiscoverer.MESHCOP_E_SERVICE_TYPE, borderAgentAdapter); + meshcopEpskcDiscoverer.start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + Log.d(TAG, "::onDestroy"); + + meshcopDiscoverer.stop(); + meshcopEpskcDiscoverer.stop(); + borderAgentAdapter.clear(); + } + + @Override + public void onResume() { + super.onResume(); + + Log.d(TAG, "::onResume"); + + meshcopDiscoverer.start(); + meshcopEpskcDiscoverer.start(); + } + + @Override + public void onPause() { + super.onPause(); + + Log.d(TAG, "::onPause"); + + meshcopDiscoverer.stop(); + meshcopEpskcDiscoverer.stop(); + borderAgentAdapter.clear(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_select_border_router, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Hide the button + retrieveDatasetButton = view.findViewById(R.id.retrieve_dataset_button); + retrieveDatasetButton.setVisibility(View.GONE); + setDatasetButton = view.findViewById(R.id.set_dataset_button); + setDatasetButton.setVisibility(View.GONE); + + final ListView borderAgentListView = view.findViewById(R.id.networks); + borderAgentListView.setAdapter(borderAgentAdapter); + + borderAgentListView.setOnItemClickListener( + (AdapterView adapterView, View v, int position, long id) -> { + selectedBorderAgent = (BorderAgentInfo) adapterView.getItemAtPosition(position); + retrieveDatasetButton.setVisibility(View.VISIBLE); + setDatasetButton.setVisibility(View.VISIBLE); + }); + + retrieveDatasetButton.setOnClickListener( + v -> networkInfoCallback.onGetAdminPasscodeStarted(selectedBorderAgent, + GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET)); + setDatasetButton.setOnClickListener( + v -> networkInfoCallback.onGetAdminPasscodeStarted(selectedBorderAgent, + GetAdminPasscodeFragment.FLOW_SET_DATASET)); + } +} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectNetworkFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectNetworkFragment.java deleted file mode 100644 index c34ede3a4..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectNetworkFragment.java +++ /dev/null @@ -1,258 +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.service; - -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.AdapterView; -import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.FutureCallback; -import io.openthread.commissioner.ByteArray; -import io.openthread.commissioner.Commissioner; -import io.openthread.commissioner.Error; -import io.openthread.commissioner.ErrorCode; - -public class SelectNetworkFragment extends Fragment - implements InputNetworkPasswordDialogFragment.PasswordDialogListener, - FetchCredentialDialogFragment.CredentialListener, - View.OnClickListener { - - private static final String TAG = SelectNetworkFragment.class.getSimpleName(); - - private FragmentCallback networkInfoCallback; - - @Nullable private JoinerDeviceInfo joinerDeviceInfo; - - private NetworkAdapter networksAdapter; - - private ThreadNetworkInfoHolder selectedNetwork; - private byte[] userInputPskc; - private Button addDeviceButton; - - private BorderAgentDiscoverer borderAgentDiscoverer; - - public SelectNetworkFragment() {} - - public SelectNetworkFragment( - @NonNull FragmentCallback networkInfoCallback, @Nullable JoinerDeviceInfo joinerDeviceInfo) { - this.networkInfoCallback = networkInfoCallback; - this.joinerDeviceInfo = joinerDeviceInfo; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Log.d(TAG, "::onCreate"); - - networksAdapter = new NetworkAdapter(getContext()); - borderAgentDiscoverer = new BorderAgentDiscoverer(getContext(), networksAdapter); - borderAgentDiscoverer.start(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - Log.d(TAG, "::onDestroy"); - - borderAgentDiscoverer.stop(); - } - - @Override - public void onResume() { - super.onResume(); - - Log.d(TAG, "::onResume"); - - borderAgentDiscoverer.start(); - } - - @Override - public void onPause() { - super.onPause(); - - Log.d(TAG, "::onPause"); - - borderAgentDiscoverer.stop(); - } - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_select_network, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Hide the button - addDeviceButton = view.findViewById(R.id.add_device_button); - addDeviceButton.setVisibility(View.GONE); - - 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); - } - - final ListView networkListView = view.findViewById(R.id.networks); - networkListView.setAdapter(networksAdapter); - - networkListView.setOnItemClickListener( - (AdapterView adapterView, View v, int position, long id) -> { - selectedNetwork = (ThreadNetworkInfoHolder) adapterView.getItemAtPosition(position); - addDeviceButton.setVisibility(View.VISIBLE); - }); - - view.findViewById(R.id.add_device_button).setOnClickListener(this); - } - - // Click listeners for network password dialog. - - @Override - public void onPositiveClick(InputNetworkPasswordDialogFragment fragment, String password) { - BorderAgentInfo selectedBorderAgent = selectedNetwork.getBorderAgents().get(0); - userInputPskc = computePskc(selectedNetwork.getNetworkInfo(), password); - - networkInfoCallback.onNetworkSelected(selectedNetwork, userInputPskc); - } - - @Override - public void onNegativeClick(InputNetworkPasswordDialogFragment fragment) { - networkInfoCallback.onNetworkSelected(selectedNetwork, null); - } - - private byte[] computePskc(ThreadNetworkInfo threadNetworkInfo, String password) { - ByteArray extendedPanId = new ByteArray(threadNetworkInfo.getExtendedPanId()); - ByteArray pskc = new ByteArray(); - Error error = - Commissioner.generatePSKc( - pskc, password, threadNetworkInfo.getNetworkName(), extendedPanId); - if (error.getCode() != ErrorCode.kNone) { - Log.e( - TAG, - String.format( - "failed to generate PSKc: %s; network-name=%s, extended-pan-id=%s", - error.toString(), - threadNetworkInfo.getNetworkName(), - CommissionerUtils.getHexString(threadNetworkInfo.getExtendedPanId()))); - } else { - Log.d( - TAG, - String.format( - "generated pskc=%s, network-name=%s, extended-pan-id=%s", - CommissionerUtils.getHexString(pskc), - threadNetworkInfo.getNetworkName(), - CommissionerUtils.getHexString(threadNetworkInfo.getExtendedPanId()))); - } - - return CommissionerUtils.getByteArray(pskc); - } - - @Override - public void onClick(View view) { - BorderAgentInfo selectedBorderAgent = selectedNetwork.getBorderAgents().get(0); - if (selectedBorderAgent.id == null) { - showAlertDialog("Invalid Border Router: no \"id\" associated"); - return; - } - - ThreadCommissionerServiceImpl commissionerService = - ThreadCommissionerServiceImpl.newInstance(null); - - FluentFuture.from(commissionerService.getThreadNetworkCredentials(selectedBorderAgent)) - .addCallback( - new FutureCallback() { - @Override - public void onSuccess(ThreadNetworkCredentials credentials) { - if (credentials != null) { - networkInfoCallback.onNetworkSelected(selectedNetwork, credentials.getPskc()); - } else { - // Ask the user to input Commissioner password. - new InputNetworkPasswordDialogFragment(SelectNetworkFragment.this) - .show( - getParentFragmentManager(), - InputNetworkPasswordDialogFragment.class.getSimpleName()); - } - } - - @Override - public void onFailure(Throwable t) { - Log.e(TAG, "Failed to retrieve Thread network credentials from GMS", t); - showAlertDialog(t.getMessage()); - } - }, - ContextCompat.getMainExecutor(getActivity())); - } - - private void showAlertDialog(String message) { - new MaterialAlertDialogBuilder(getActivity(), R.style.ThreadNetworkAlertTheme) - .setMessage(message) - .setPositiveButton( - "OK", - ((dialog, which) -> { - networkInfoCallback.onMeshcopResult(Activity.RESULT_CANCELED); - })) - .show(); - } - - @Override - public void onCancelClick(FetchCredentialDialogFragment fragment) { - // TODO: - } - - @Override - public void onConfirmClick( - FetchCredentialDialogFragment fragment, ThreadNetworkCredentials credentials) { - // TODO: - } -} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SetDatasetFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SetDatasetFragment.java new file mode 100644 index 000000000..aad55a7c5 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SetDatasetFragment.java @@ -0,0 +1,234 @@ +package io.openthread.commissioner.service; + +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.ActivityResultCaller; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.IntentSenderRequest; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult; +import androidx.annotation.NonNull; +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.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 io.openthread.commissioner.PendingOperationalDataset; +import io.openthread.commissioner.Timestamp; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Locale; +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 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.onCredentialsRetrieved()); + } + + 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.onCredentialsRetrieved(); + })).show(); + } +} diff --git a/android/openthread_commissioner/service/src/main/res/layout/border_agent_list_item.xml b/android/openthread_commissioner/service/src/main/res/layout/border_agent_list_item.xml new file mode 100644 index 000000000..8c034e569 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/res/layout/border_agent_list_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/android/openthread_commissioner/service/src/main/res/layout/fragment_get_admin_passcode.xml b/android/openthread_commissioner/service/src/main/res/layout/fragment_get_admin_passcode.xml new file mode 100644 index 000000000..63be64971 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/res/layout/fragment_get_admin_passcode.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + +