From 295a0bba7dbee81a6900725fefd5f758aafb74e7 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 | 2 +- .../commissioner/app/MainActivity.java | 43 +++- .../service/build.gradle | 10 +- .../service/BorderAgentAdapter.java | 139 +++++++++++ .../service/BorderAgentDiscoverer.java | 48 +++- .../commissioner/service/BorderAgentInfo.java | 45 +++- .../service/CommissionerActivity.java | 26 ++ .../service/FragmentCallback.java | 8 + .../service/GetAdminPasscodeFragment.java | 187 ++++++++++++++ .../service/RetrieveDatasetFragment.java | 169 +++++++++++++ .../service/SelectBorderRouterFragment.java | 146 +++++++++++ .../service/SelectNetworkFragment.java | 66 ++++- .../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 +++++ .../layout/fragment_select_border_router.xml | 77 ++++++ .../res/layout/fragment_select_network.xml | 44 ++-- .../main/res/layout/fragment_set_dataset.xml | 84 +++++++ .../service/src/main/res/values/strings.xml | 4 + .../service/src/main/res/values/styles.xml | 17 +- 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 + 27 files changed, 1541 insertions(+), 59 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 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 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 create mode 100644 android/openthread_commissioner/service/src/main/res/layout/fragment_select_border_router.xml create mode 100644 android/openthread_commissioner/service/src/main/res/layout/fragment_set_dataset.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/build.gradle b/android/openthread_commissioner/app/build.gradle index f6f239a01..45b4ad1f8 100644 --- a/android/openthread_commissioner/app/build.gradle +++ b/android/openthread_commissioner/app/build.gradle @@ -51,7 +51,7 @@ android { defaultConfig { applicationId "io.openthread.commissioner.app" - minSdkVersion 24 + minSdkVersion 26 targetSdkVersion 34 versionCode 1 versionName "0.0.1" 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..20ab3c698 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,10 +40,15 @@ 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.SelectBorderRouterFragment; import io.openthread.commissioner.service.SelectNetworkFragment; import io.openthread.commissioner.service.ThreadNetworkInfoHolder; @@ -51,11 +56,14 @@ 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 +77,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 +162,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 6e0894c72..168d4c008 100644 --- a/android/openthread_commissioner/service/build.gradle +++ b/android/openthread_commissioner/service/build.gradle @@ -34,7 +34,7 @@ android { compileSdkVersion 34 defaultConfig { - minSdkVersion 24 + minSdkVersion 26 targetSdkVersion 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -73,10 +73,16 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) - 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:31.1-jre' + implementation 'androidx.activity:activity:1.8.2' + implementation 'androidx.appcompat:appcompat:1.6.1' 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' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.5' 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..92116147b --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentAdapter.java @@ -0,0 +1,139 @@ +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.net.SocketAddress; +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 f63bd587f..32481c270 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,6 +34,7 @@ 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; @@ -46,14 +47,20 @@ 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_DISCRIMINATOR = "discriminator"; 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 String serviceType; + private final BorderAgentListener borderAgentListener; private ExecutorService executor = Executors.newSingleThreadExecutor(); private BlockingQueue unresolvedServices = new ArrayBlockingQueue<>(256); @@ -64,16 +71,19 @@ public class BorderAgentDiscoverer implements NsdManager.DiscoveryListener { public interface BorderAgentListener { void onBorderAgentFound(BorderAgentInfo borderAgentInfo); - void onBorderAgentLost(String discriminator); + default void onBorderAgentLost(String discriminator) {} + + 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); + this.serviceType = serviceType; this.borderAgentListener = borderAgentListener; } @@ -89,8 +99,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() { @@ -165,7 +175,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 @@ -187,6 +197,8 @@ public void onServiceLost(NsdServiceInfo nsdServiceInfo) { Log.d(TAG, "a Border Agent service is gone"); borderAgentListener.onBorderAgentLost(discriminator); } + + borderAgentListener.onBorderAgentLost(serviceType.equals(MESHCOP_E_SERVICE_TYPE), nsdServiceInfo.getServiceName()); } @Override @@ -209,16 +221,26 @@ private BorderAgentInfo getBorderAgentInfo(NsdServiceInfo serviceInfo) { discriminator = new String(attrs.get(KEY_DISCRIMINATOR)); } - if (!attrs.containsKey(KEY_NETWORK_NAME) || !attrs.containsKey(KEY_EXTENDED_PAN_ID)) { - return null; - } - return new BorderAgentInfo( + serviceType.equals(MESHCOP_E_SERVICE_TYPE), + serviceInfo.getServiceName(), + attrs.get(KEY_ID), discriminator, - new String(attrs.get(KEY_NETWORK_NAME)), + getStringAttribute(attrs, KEY_NETWORK_NAME), attrs.get(KEY_EXTENDED_PAN_ID), serviceInfo.getHost(), - serviceInfo.getPort()); + serviceInfo.getPort(), + getStringAttribute(attrs, KEY_VENDOR_NAME), + getStringAttribute(attrs, KEY_MODEL_NAME)); + } + + @Nullable + private static String getStringAttribute(Map attributes, String key) { + byte[] value = attributes.get(key); + if (value == null) { + return null; + } + return new String(value); } private String getBorderAgentDiscriminator(NsdServiceInfo serviceInfo) { 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 c60bc9a7b..d1d127b16 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,26 +35,46 @@ import java.net.UnknownHostException; public class BorderAgentInfo implements Parcelable { - public String discriminator; - public String networkName; - public byte[] extendedPanId; + public final boolean isEpskcService; + public final String instanceName; + public final byte[] id; + public final String discriminator; + 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 String discriminator, - @NonNull String networkName, - @NonNull byte[] extendedPanId, - @NonNull InetAddress host, - @NonNull int port) { + boolean isEpskcService, + String instanceName, + byte[] id, + String discriminator, + 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.discriminator = discriminator; this.networkName = networkName; - this.extendedPanId = extendedPanId; + 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(); discriminator = in.readString(); networkName = in.readString(); extendedPanId = in.createByteArray(); @@ -63,15 +83,22 @@ 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(discriminator); 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/CommissionerActivity.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerActivity.java index 06c7ce62f..366267b3c 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 = @@ -123,4 +124,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..d4b76d78e --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/GetAdminPasscodeFragment.java @@ -0,0 +1,187 @@ +/* + * 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(getContext(), + 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; + + getActivity().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); + } + } + + @Override + public void onBorderAgentLost(String discriminator) { + Log.w(TAG, "Lost MeshCoP-ePSKc service " + discriminator); + } +} 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/SelectBorderRouterFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java new file mode 100644 index 000000000..f69b73578 --- /dev/null +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/SelectBorderRouterFragment.java @@ -0,0 +1,146 @@ +/* + * 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.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.fragment.app.Fragment; + +public class SelectBorderRouterFragment extends Fragment { + + private static final String TAG = SelectBorderRouterFragment.class.getSimpleName(); + + private FragmentCallback networkInfoCallback; + + private BorderAgentAdapter borderAgentAdapter; + + private Button retrieveDatasetButton; + private Button setDatasetButton; + + private BorderAgentDiscoverer meshcopDiscoverer; + private BorderAgentDiscoverer meshcopEpskcDiscoverer; + + private BorderAgentInfo selectedBorderAgent; + + public SelectBorderRouterFragment() { + } + + public SelectBorderRouterFragment(@NonNull FragmentCallback networkInfoCallback) { + this.networkInfoCallback = networkInfoCallback; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Log.d(TAG, "::onCreate"); + + borderAgentAdapter = new BorderAgentAdapter(getContext()); + meshcopDiscoverer = new BorderAgentDiscoverer(getContext(), + BorderAgentDiscoverer.MESHCOP_SERVICE_TYPE, borderAgentAdapter); + meshcopDiscoverer.start(); + meshcopEpskcDiscoverer = new BorderAgentDiscoverer(getContext(), + 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 index 2287c24a7..f2edcd872 100644 --- 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 @@ -48,23 +48,28 @@ public class SelectNetworkFragment extends Fragment implements InputNetworkPasswordDialogFragment.PasswordDialogListener, - FetchCredentialDialogFragment.CredentialListener, - View.OnClickListener { + FetchCredentialDialogFragment.CredentialListener, + View.OnClickListener { + private static final String TAG = SelectNetworkFragment.class.getSimpleName(); private FragmentCallback networkInfoCallback; - @Nullable private JoinerDeviceInfo joinerDeviceInfo; + @Nullable + private JoinerDeviceInfo joinerDeviceInfo; private NetworkAdapter networksAdapter; private ThreadNetworkInfoHolder selectedNetwork; private byte[] userInputPskc; private Button addDeviceButton; + private Button retrieveDatasetButton; - private BorderAgentDiscoverer borderAgentDiscoverer; + private BorderAgentDiscoverer meshcopDiscoverer; + private BorderAgentDiscoverer meshcopEpskcDiscoverer; - public SelectNetworkFragment() {} + public SelectNetworkFragment() { + } public SelectNetworkFragment( @NonNull FragmentCallback networkInfoCallback, @Nullable JoinerDeviceInfo joinerDeviceInfo) { @@ -79,8 +84,12 @@ public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "::onCreate"); networksAdapter = new NetworkAdapter(getContext()); - borderAgentDiscoverer = new BorderAgentDiscoverer(getContext(), networksAdapter); - borderAgentDiscoverer.start(); + meshcopDiscoverer = new BorderAgentDiscoverer(getContext(), + BorderAgentDiscoverer.MESHCOP_SERVICE_TYPE, networksAdapter); + meshcopDiscoverer.start(); + meshcopEpskcDiscoverer = new BorderAgentDiscoverer(getContext(), + BorderAgentDiscoverer.MESHCOP_E_SERVICE_TYPE, networksAdapter); + meshcopEpskcDiscoverer.start(); } @Override @@ -89,7 +98,8 @@ public void onDestroy() { Log.d(TAG, "::onDestroy"); - borderAgentDiscoverer.stop(); + meshcopDiscoverer.stop(); + meshcopEpskcDiscoverer.stop(); } @Override @@ -98,7 +108,8 @@ public void onResume() { Log.d(TAG, "::onResume"); - borderAgentDiscoverer.start(); + meshcopDiscoverer.start(); + meshcopEpskcDiscoverer.stop(); } @Override @@ -107,7 +118,8 @@ public void onPause() { Log.d(TAG, "::onPause"); - borderAgentDiscoverer.stop(); + meshcopDiscoverer.stop(); + meshcopEpskcDiscoverer.stop(); } @Override @@ -124,6 +136,8 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { // Hide the button addDeviceButton = view.findViewById(R.id.add_device_button); addDeviceButton.setVisibility(View.GONE); + retrieveDatasetButton = view.findViewById(R.id.retrieve_dataset_button); + retrieveDatasetButton.setVisibility(View.GONE); if (joinerDeviceInfo != null) { String deviceInfoString = @@ -145,9 +159,11 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { (AdapterView adapterView, View v, int position, long id) -> { selectedNetwork = (ThreadNetworkInfoHolder) adapterView.getItemAtPosition(position); addDeviceButton.setVisibility(View.VISIBLE); + retrieveDatasetButton.setVisibility(View.VISIBLE); }); - view.findViewById(R.id.add_device_button).setOnClickListener(this); + addDeviceButton.setOnClickListener(v -> onAddDeviceClicked()); + retrieveDatasetButton.setOnClickListener(v -> onRetrieveDatasetClicked()); } // Click listeners for network password dialog. @@ -223,6 +239,34 @@ public void onClick(View view) { } } + private void onAddDeviceClicked() { + try { + BorderAgentInfo selectedBorderAgent = selectedNetwork.getBorderAgents().get(0); + ThreadCommissionerServiceImpl commissionerService = new ThreadCommissionerServiceImpl(null); + + // TODO(wgtdkp): we could be blocked here. + BorderAgentRecord borderAgentRecord = + commissionerService.getBorderAgentRecord(selectedBorderAgent).get(); + + if (borderAgentRecord != null && borderAgentRecord.getPskc() != null) { + networkInfoCallback.onNetworkSelected(selectedNetwork, borderAgentRecord.getPskc()); + } else { + // Ask the user to input Commissioner password. + new InputNetworkPasswordDialogFragment(SelectNetworkFragment.this) + .show( + getParentFragmentManager(), + InputNetworkPasswordDialogFragment.class.getSimpleName()); + } + } catch (ExecutionException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private void onRetrieveDatasetClicked() { + networkInfoCallback.onGetAdminPasscodeStarted(selectedNetwork.getBorderAgents().get(0), + GetAdminPasscodeFragment.FLOW_RETRIEVE_DATASET); + } + @Override public void onCancelClick(FetchCredentialDialogFragment fragment) { // 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 @@ + + + + + + + + + + + + + +