From f41975bb45e8f2284f771adb345121a726989185 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Sat, 11 May 2024 23:42:02 +0800 Subject: [PATCH] [android] cleanup Android sample app --- .../openthread_commissioner/app/build.gradle | 2 +- .../examples/qrcode/README.md | 3 + .../v=1&&eui=0000b57fffe15d68&&cc=J01NU5.png | Bin 0 -> 652 bytes .../service/build.gradle | 5 +- .../service/BorderAgentDatabaseTest.java | 67 --------- .../commissioner/service/BorderAgentDao.java | 61 -------- .../service/BorderAgentDatabase.java | 89 ------------ .../service/BorderAgentDiscoverer.java | 39 ++--- .../commissioner/service/BorderAgentInfo.java | 17 +-- .../service/BorderAgentRecord.java | 98 ------------- .../service/CommissionerUtils.java | 26 ++++ .../FetchCredentialDialogFragment.java | 9 +- .../commissioner/service/MeshcopFragment.java | 54 ++++--- .../commissioner/service/NetworkAdapter.java | 9 +- .../service/NetworkCredential.java | 33 ----- .../service/ScanQrCodeFragment.java | 3 - .../service/SelectNetworkFragment.java | 78 ++++++---- .../service/ThreadCommissionerService.java | 27 +--- .../ThreadCommissionerServiceFactory.java | 35 ----- .../ThreadCommissionerServiceImpl.java | 137 ++++++++---------- .../service/ThreadNetworkCredential.java | 78 ---------- .../service/src/main/res/values/styles.xml | 15 ++ 22 files changed, 222 insertions(+), 663 deletions(-) create mode 100644 android/openthread_commissioner/examples/qrcode/README.md create mode 100644 android/openthread_commissioner/examples/qrcode/v=1&&eui=0000b57fffe15d68&&cc=J01NU5.png delete mode 100644 android/openthread_commissioner/service/src/androidTest/java/io/openthread/commissioner/service/BorderAgentDatabaseTest.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDao.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDatabase.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentRecord.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkCredential.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceFactory.java delete mode 100644 android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadNetworkCredential.java 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/examples/qrcode/README.md b/android/openthread_commissioner/examples/qrcode/README.md new file mode 100644 index 000000000..4b3115d97 --- /dev/null +++ b/android/openthread_commissioner/examples/qrcode/README.md @@ -0,0 +1,3 @@ +# QR code + +- file `v=1&&eui=0000b57fffe15d68&&cc=J01NU5.png` is the Thread joiner device QR code which encodes the joiner EUI and PSKd as a text string (the content is `v=1&&eui=0000b57fffe15d68&&cc=J01NU5`) diff --git a/android/openthread_commissioner/examples/qrcode/v=1&&eui=0000b57fffe15d68&&cc=J01NU5.png b/android/openthread_commissioner/examples/qrcode/v=1&&eui=0000b57fffe15d68&&cc=J01NU5.png new file mode 100644 index 0000000000000000000000000000000000000000..9c0b71bf6b7791608526267ecad829c0ddb2a41e GIT binary patch literal 652 zcmV;70(1R|P)gII0{Fgq+rlu`N=lf&16#8+2X0#AKM?52Fr>g!eG>nyQVnKf5$BjPM99{^G9By z3kgXRl4g(KHyKn{8N+-S?ao!YVM#|q(u#CpdJ9Bs=@Dlm2@#*8VI>Jkb8=5qFzA^~ zLh?0oMz8ZN&GxGM+vF!9sX-)$W%6_qt5J81zAqI>NGgyNPHv$b-Q}Snt&QX$A!$MW zi1GzHcdu5Fkklid$=KS@@JzN|(k#g6G)O{HpGdj2+bJmzo?*|4zpc@RYe=TJKwt)E)60QlDg!c z6BSp@i$U})Q*D_ZOhQtbO!&(_-5IxZOU(<;B9lo-nvu&FbMh^R~icGd@%^8dj%!l7SB@&XF1ZnG;Jm6wm9JF_8pKiO6khCEwJd<-z m$=|mAm0KYpX+ju`e-}UNhrK&4d3Z7a0000> getAll(); - - @Query("SELECT * FROM border_agent_table WHERE discriminator=:discriminator LIMIT 1") - BorderAgentRecord getBorderAgent(@NonNull String discriminator); - - @Query( - "SELECT * FROM border_agent_table WHERE network_name=:networkName AND extended_pan_id=:extendedPanId") - List getBorderAgents( - @NonNull String networkName, @NonNull byte[] extendedPanId); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - void insert(BorderAgentRecord borderAgentRecord); - - @Query("DELETE FROM border_agent_table WHERE discriminator=:discriminator") - void delete(@NonNull String discriminator); - - @Query("DELETE FROM border_agent_table") - void deleteAll(); -} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDatabase.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDatabase.java deleted file mode 100644 index c8b7d87fe..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentDatabase.java +++ /dev/null @@ -1,89 +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 androidx.annotation.NonNull; -import androidx.room.Database; -import androidx.room.Room; -import androidx.room.RoomDatabase; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@Database( - entities = {BorderAgentRecord.class}, - version = 1, - exportSchema = false) -abstract class BorderAgentDatabase extends RoomDatabase { - - abstract BorderAgentDao borderAgentDao(); - - private static volatile BorderAgentDatabase INSTANCE; - private static final int NUMBER_OF_THREADS = 2; - - static final ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS); - - public static synchronized BorderAgentDatabase getDatabase() { - if (INSTANCE == null) { - INSTANCE = - Room.databaseBuilder( - CommissionerServiceApp.getContext(), - BorderAgentDatabase.class, - "network_credential_database") - .build(); - } - return INSTANCE; - } - - public CompletableFuture getBorderAgent(@NonNull String discriminator) { - CompletableFuture future = - CompletableFuture.supplyAsync(() -> borderAgentDao().getBorderAgent(discriminator)); - return future; - } - - public CompletableFuture insertBorderAgent(@NonNull BorderAgentRecord borderAgentRecord) { - CompletableFuture future = - CompletableFuture.runAsync( - () -> { - borderAgentDao().insert(borderAgentRecord); - }, - executor); - return future; - } - - public CompletableFuture deleteBorderAgent(@NonNull String discriminator) { - CompletableFuture future = - CompletableFuture.runAsync( - () -> { - borderAgentDao().delete(discriminator); - }, - executor); - return future; - } -} 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..6e4214d12 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; @@ -47,7 +48,7 @@ public class BorderAgentDiscoverer implements NsdManager.DiscoveryListener { private static final String TAG = BorderAgentDiscoverer.class.getSimpleName(); private static final String SERVICE_TYPE = "_meshcop._udp"; - private static final String KEY_DISCRIMINATOR = "discriminator"; + private static final String KEY_ID = "id"; private static final String KEY_NETWORK_NAME = "nn"; private static final String KEY_EXTENDED_PAN_ID = "xp"; @@ -62,9 +63,10 @@ public class BorderAgentDiscoverer implements NsdManager.DiscoveryListener { private boolean isScanning = false; public interface BorderAgentListener { + void onBorderAgentFound(BorderAgentInfo borderAgentInfo); - void onBorderAgentLost(String discriminator); + void onBorderAgentLost(byte[] id); } @RequiresPermission(permission.INTERNET) @@ -182,10 +184,10 @@ public void onServiceFound(NsdServiceInfo nsdServiceInfo) { @Override public void onServiceLost(NsdServiceInfo nsdServiceInfo) { - String discriminator = getBorderAgentDiscriminator(nsdServiceInfo); - if (discriminator != null) { - Log.d(TAG, "a Border Agent service is gone"); - borderAgentListener.onBorderAgentLost(discriminator); + byte[] id = getBorderAgentId(nsdServiceInfo); + if (id != null) { + Log.d(TAG, "a Border Agent service is gone: " + nsdServiceInfo.getServiceName()); + borderAgentListener.onBorderAgentLost(id); } } @@ -199,40 +201,29 @@ 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(); - - // Use the host address as default discriminator. - String discriminator = serviceInfo.getHost().getHostAddress(); - - if (attrs.containsKey(KEY_DISCRIMINATOR)) { - discriminator = new String(attrs.get(KEY_DISCRIMINATOR)); - } + byte[] id = getBorderAgentId(serviceInfo); if (!attrs.containsKey(KEY_NETWORK_NAME) || !attrs.containsKey(KEY_EXTENDED_PAN_ID)) { return null; } return new BorderAgentInfo( - discriminator, + id, new String(attrs.get(KEY_NETWORK_NAME)), attrs.get(KEY_EXTENDED_PAN_ID), serviceInfo.getHost(), serviceInfo.getPort()); } - private String getBorderAgentDiscriminator(NsdServiceInfo serviceInfo) { + @Nullable + private byte[] getBorderAgentId(NsdServiceInfo serviceInfo) { Map attrs = serviceInfo.getAttributes(); - - if (attrs.containsKey(KEY_DISCRIMINATOR)) { - return new String(attrs.get(KEY_DISCRIMINATOR)); - } - - if (serviceInfo.getHost() != null) { - // Use the host address as default discriminator. - return serviceInfo.getHost().getHostAddress(); + if (attrs.containsKey(KEY_ID)) { + return attrs.get(KEY_ID).clone(); } - 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 c60bc9a7b..0a9babede 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,28 @@ import java.net.UnknownHostException; public class BorderAgentInfo implements Parcelable { - public String discriminator; + + public byte[] id; public String networkName; public byte[] extendedPanId; public InetAddress host; public int port; public BorderAgentInfo( - @NonNull String discriminator, + @NonNull byte[] id, @NonNull String networkName, @NonNull byte[] extendedPanId, @NonNull InetAddress host, @NonNull int port) { - this.discriminator = discriminator; + this.id = id.clone(); this.networkName = networkName; - this.extendedPanId = extendedPanId; + this.extendedPanId = extendedPanId.clone(); this.host = host; this.port = port; } protected BorderAgentInfo(Parcel in) { - discriminator = in.readString(); + id = in.createByteArray(); networkName = in.readString(); extendedPanId = in.createByteArray(); try { @@ -67,7 +68,7 @@ protected BorderAgentInfo(Parcel in) { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(discriminator); + dest.writeByteArray(id); dest.writeString(networkName); dest.writeByteArray(extendedPanId); dest.writeByteArray(host.getAddress()); @@ -79,10 +80,6 @@ public int describeContents() { return 0; } - public boolean equals(BorderAgentInfo other) { - return this.discriminator.equals(other.discriminator); - } - public static final Creator CREATOR = new Creator() { @Override diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentRecord.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentRecord.java deleted file mode 100644 index 107a8ea90..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/BorderAgentRecord.java +++ /dev/null @@ -1,98 +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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.PrimaryKey; - -@Entity(tableName = "border_agent_table") -public class BorderAgentRecord { - - @PrimaryKey - @NonNull - @ColumnInfo(name = "discriminator") - private String discriminator; - - @NonNull - @ColumnInfo(name = "network_name") - private String networkName; - - @NonNull - @ColumnInfo(name = "extended_pan_id", typeAffinity = ColumnInfo.BLOB) - private byte[] extendedPanId; - - @NonNull - @ColumnInfo(name = "pskc", typeAffinity = ColumnInfo.BLOB) - private byte[] pskc; - - @Nullable - @ColumnInfo(name = "active_operational_dataset", typeAffinity = ColumnInfo.BLOB) - private byte[] activeOperationalDataset; - - public BorderAgentRecord( - @NonNull String discriminator, - @NonNull String networkName, - @NonNull byte[] extendedPanId, - @NonNull byte[] pskc, - @Nullable byte[] activeOperationalDataset) { - this.discriminator = discriminator; - this.networkName = networkName; - this.extendedPanId = extendedPanId; - this.pskc = pskc; - this.activeOperationalDataset = activeOperationalDataset; - } - - @NonNull - public String getDiscriminator() { - return discriminator; - } - - @NonNull - public String getNetworkName() { - return networkName; - } - - @NonNull - public byte[] getExtendedPanId() { - return extendedPanId; - } - - @NonNull - public byte[] getPskc() { - return pskc; - } - - @Nullable - public byte[] getActiveOperationalDataset() { - return activeOperationalDataset; - } -} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerUtils.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerUtils.java index a51f40970..b31748886 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerUtils.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/CommissionerUtils.java @@ -29,6 +29,9 @@ package io.openthread.commissioner.service; import androidx.annotation.Nullable; +import androidx.concurrent.futures.CallbackToFutureAdapter; +import com.google.android.gms.tasks.Task; +import com.google.common.util.concurrent.ListenableFuture; import io.openthread.commissioner.ByteArray; public class CommissionerUtils { @@ -76,4 +79,27 @@ public static String getHexString(@Nullable byte[] byteArray) { public static String getHexString(ByteArray byteArray) { return getHexString(getByteArray(byteArray)); } + + /** Converts a {@link Task} to a {@link ListenableFuture}. */ + public static ListenableFuture toListenableFuture(Task task) { + return CallbackToFutureAdapter.getFuture( + completer -> { + task.addOnCompleteListener( + completedTask -> { + if (completedTask.isCanceled()) { + completer.setCancelled(); + } else if (completedTask.isSuccessful()) { + completer.set(completedTask.getResult()); + } else { + Exception e = completedTask.getException(); + if (e != null) { + completer.setException(e); + } else { + throw new IllegalStateException(); + } + } + }); + return "toListenableFuture"; + }); + } } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FetchCredentialDialogFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FetchCredentialDialogFragment.java index 58b6c6f90..7331e61b3 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FetchCredentialDialogFragment.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/FetchCredentialDialogFragment.java @@ -39,6 +39,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; public class FetchCredentialDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { @@ -50,12 +51,14 @@ public class FetchCredentialDialogFragment extends DialogFragment BorderAgentInfo borderAgentInfo; byte[] pskc; - private ThreadNetworkCredential credential; + private ThreadNetworkCredentials credentials; public interface CredentialListener { + void onCancelClick(FetchCredentialDialogFragment fragment); - void onConfirmClick(FetchCredentialDialogFragment fragment, ThreadNetworkCredential credential); + void onConfirmClick( + FetchCredentialDialogFragment fragment, ThreadNetworkCredentials credentials); } public FetchCredentialDialogFragment( @@ -105,7 +108,7 @@ public void onStop() { @Override public void onClick(DialogInterface dialogInterface, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { - credentialListener.onConfirmClick(this, credential); + credentialListener.onConfirmClick(this, credentials); } else { stopFetching(); credentialListener.onCancelClick(this); diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/MeshcopFragment.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/MeshcopFragment.java index f36d3700c..2ba0eb6ab 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/MeshcopFragment.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/MeshcopFragment.java @@ -40,8 +40,11 @@ import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import java.util.concurrent.CompletableFuture; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; public class MeshcopFragment extends Fragment implements ThreadCommissionerServiceImpl.IntermediateStateCallback { @@ -55,18 +58,17 @@ public class MeshcopFragment extends Fragment ImageView doneImage; ImageView errorImage; - @NonNull private FragmentCallback meshcopCallback; + @NonNull private final FragmentCallback meshcopCallback; - @NonNull private ThreadNetworkInfoHolder networkInfoHolder; + @NonNull private final ThreadNetworkInfoHolder networkInfoHolder; - @NonNull private byte[] pskc; + @NonNull private final byte[] pskc; - @NonNull private JoinerDeviceInfo joinerDeviceInfo; + @NonNull private final JoinerDeviceInfo joinerDeviceInfo; - private ThreadCommissionerServiceImpl commissionerService = - new ThreadCommissionerServiceImpl(this); - - private CompletableFuture commissionFuture; + private final ThreadCommissionerServiceImpl commissionerService = + ThreadCommissionerServiceImpl.newInstance(this); + private ListenableFuture commissionFuture; public MeshcopFragment( @NonNull FragmentCallback meshcopCallback, @@ -127,25 +129,21 @@ private void startMeshcop() { showInProgress("Petitioning..."); commissionFuture = - commissionerService - .commissionJoinerDevice(borderAgentInfo, pskc, joinerDeviceInfo) - .thenRun( - () -> { - new Handler(Looper.getMainLooper()) - .post( - () -> { - showCommissionDone(true, "Commission Succeed"); - }); - }) - .exceptionally( - ex -> { - new Handler(Looper.getMainLooper()) - .post( - () -> { - showCommissionDone(false, ex.getMessage()); - }); - return null; - }); + commissionerService.commissionJoinerDevice(borderAgentInfo, pskc, joinerDeviceInfo); + FluentFuture.from(commissionFuture) + .addCallback( + new FutureCallback() { + @Override + public void onSuccess(Void result) { + showCommissionDone(true, "Commission Succeed"); + } + + @Override + public void onFailure(Throwable t) { + showCommissionDone(false, t.getMessage()); + } + }, + ContextCompat.getMainExecutor(getActivity())); } private void stopMeshcop() { 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 index 96a220a62..ebb259621 100644 --- 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 @@ -41,6 +41,7 @@ import java.util.Vector; public class NetworkAdapter extends BaseAdapter implements BorderAgentListener { + private Vector networks; private LayoutInflater inflater; @@ -68,10 +69,10 @@ public void addBorderAgent(BorderAgentInfo borderAgent) { new Handler(Looper.getMainLooper()).post(() -> notifyDataSetChanged()); } - public void removeBorderAgent(String lostBorderAgentDisciminator) { + public void removeBorderAgent(byte[] lostBorderAgentId) { for (ThreadNetworkInfoHolder networkInfoHolder : networks) { for (BorderAgentInfo borderAgent : networkInfoHolder.getBorderAgents()) { - if (borderAgent.discriminator.equals(lostBorderAgentDisciminator)) { + if (Arrays.equals(borderAgent.id, lostBorderAgentId)) { networkInfoHolder.getBorderAgents().remove(borderAgent); if (networkInfoHolder.getBorderAgents().isEmpty()) { networks.remove(networkInfoHolder); @@ -119,7 +120,7 @@ public void onBorderAgentFound(BorderAgentInfo borderAgentInfo) { } @Override - public void onBorderAgentLost(String discriminator) { - removeBorderAgent(discriminator); + public void onBorderAgentLost(byte[] borderAgentId) { + removeBorderAgent(borderAgentId); } } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkCredential.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkCredential.java deleted file mode 100644 index ea37c923f..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/NetworkCredential.java +++ /dev/null @@ -1,33 +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; - -public interface NetworkCredential { - byte[] getEncoded(); -} 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 e2020cd74..229a6003d 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 @@ -56,9 +56,6 @@ public class ScanQrCodeFragment extends Fragment implements Detector.Processor() { + @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 @@ -230,7 +252,7 @@ public void onCancelClick(FetchCredentialDialogFragment fragment) { @Override public void onConfirmClick( - FetchCredentialDialogFragment fragment, ThreadNetworkCredential credential) { + FetchCredentialDialogFragment fragment, ThreadNetworkCredentials credentials) { // TODO: } } diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerService.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerService.java index bdfc35fdb..87add5fca 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerService.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerService.java @@ -29,32 +29,17 @@ package io.openthread.commissioner.service; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.util.concurrent.CompletableFuture; +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; +import com.google.common.util.concurrent.ListenableFuture; public interface ThreadCommissionerService { - // This method returns Credential of a given Thread Network. - // If the Commissioner Service doesn't possess the Network - // Credentials already, it will fetch the Credentials from - // a Border Agent (assisting device) using the Thread - // Commissioning protocol. - // - // The pskc is used to securely connect to the Border Agent device. - // If no pskc is given, the user will be asked to input it. - // The Network Credentials will be saved in the Commissioner - // Service before returning. - CompletableFuture fetchThreadNetworkCredential( - @NonNull BorderAgentInfo borderAgentInfo, @Nullable byte[] pskc); - - // This method returns Credential of a given Thread Network stored in the database on the phone. - CompletableFuture getThreadNetworkCredential( + /** Returns Credential of a given Thread Network stored in the database on the phone. */ + ListenableFuture getThreadNetworkCredentials( @NonNull BorderAgentInfo borderAgentInfo); - // This method deletes Credential of a given Thread Network stored in the database on the phone. - CompletableFuture deleteThreadNetworkCredential(@NonNull BorderAgentInfo borderAgentInfo); - - CompletableFuture commissionJoinerDevice( + /** Securely adds a new Thread joiner device into the Thread network via MeshCoP. */ + ListenableFuture commissionJoinerDevice( @NonNull BorderAgentInfo borderAgentInfo, @NonNull byte[] pskc, @NonNull JoinerDeviceInfo joinerDeviceInfo); diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceFactory.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceFactory.java deleted file mode 100644 index d42550a13..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceFactory.java +++ /dev/null @@ -1,35 +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; - -class ThreadCommissionerServiceFactory { - public static ThreadCommissionerService getCommissionerService() { - return new ThreadCommissionerServiceImpl(null); - } -} diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceImpl.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceImpl.java index da9f34bda..e2490118e 100644 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceImpl.java +++ b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadCommissionerServiceImpl.java @@ -32,6 +32,14 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.gms.threadnetwork.ThreadBorderAgent; +import com.google.android.gms.threadnetwork.ThreadNetwork; +import com.google.android.gms.threadnetwork.ThreadNetworkClient; +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import io.openthread.commissioner.ByteArray; import io.openthread.commissioner.ChannelMask; import io.openthread.commissioner.Commissioner; @@ -43,14 +51,15 @@ import java.math.BigInteger; import java.net.Inet6Address; import java.net.InetAddress; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; public class ThreadCommissionerServiceImpl extends CommissionerHandler implements ThreadCommissionerService { public interface IntermediateStateCallback { + void onPetitioned(); void onJoinerRequest(@NonNull byte[] joinerId); @@ -60,93 +69,64 @@ public interface IntermediateStateCallback { private static final int SECONDS_WAIT_FOR_JOINER = 60; + private final ThreadNetworkClient threadNetworkClient; + private final Executor executor; @Nullable private IntermediateStateCallback intermediateStateCallback; - @NonNull private BorderAgentDatabase borderAgentDatabase = BorderAgentDatabase.getDatabase(); - @Nullable private JoinerDeviceInfo curJoinerInfo; private ConditionVariable curJoinerCommissioned = new ConditionVariable(); - public ThreadCommissionerServiceImpl( + public static ThreadCommissionerServiceImpl newInstance( + @Nullable IntermediateStateCallback intermediateStateCallback) { + return new ThreadCommissionerServiceImpl( + ThreadNetwork.getClient(CommissionerServiceApp.getContext()), + Executors.newSingleThreadExecutor(), + intermediateStateCallback); + } + + private ThreadCommissionerServiceImpl( + ThreadNetworkClient threadNetworkClient, + Executor executor, @Nullable IntermediateStateCallback intermediateStateCallback) { + this.threadNetworkClient = threadNetworkClient; + this.executor = executor; this.intermediateStateCallback = intermediateStateCallback; } @Override - public CompletableFuture commissionJoinerDevice( + public ListenableFuture commissionJoinerDevice( @NonNull BorderAgentInfo borderAgentInfo, @NonNull byte[] pskc, @NonNull JoinerDeviceInfo joinerDeviceInfo) { - CompletableFuture future = - CompletableFuture.runAsync( - () -> { - try { - doCommissionJoinerDevice( - borderAgentInfo, pskc, joinerDeviceInfo, SECONDS_WAIT_FOR_JOINER); - } catch (ThreadCommissionerException e) { - throw new CompletionException(e); - } - }); - return future; - } - - @Override - public CompletableFuture fetchThreadNetworkCredential( - @NonNull BorderAgentInfo borderAgentInfo, @Nullable byte[] pskc) { - return getThreadNetworkCredential(borderAgentInfo) - .thenApply( - credential -> { - if (credential != null) { - return credential; - } - - try { - byte[] activeDataset = doFetchActiveDataset(borderAgentInfo, pskc); - return new ThreadNetworkCredential(activeDataset); - } catch (ThreadCommissionerException e) { - throw new CompletionException(e); - } - }); - } - - @Override - public CompletableFuture getThreadNetworkCredential( - @NonNull BorderAgentInfo borderAgentInfo) { - return getBorderAgentRecord(borderAgentInfo) - .thenApply( - borderAgentRecord -> { - if (borderAgentRecord == null - || borderAgentRecord.getActiveOperationalDataset() == null) { - return null; - } - return new ThreadNetworkCredential(borderAgentRecord.getActiveOperationalDataset()); - }); - } - - CompletableFuture getBorderAgentRecord( - @NonNull BorderAgentInfo borderAgentInfo) { - return borderAgentDatabase.getBorderAgent(borderAgentInfo.discriminator); + return Futures.submitAsync( + () -> { + try { + doCommissionJoinerDevice( + borderAgentInfo, pskc, joinerDeviceInfo, SECONDS_WAIT_FOR_JOINER); + return Futures.immediateVoidFuture(); + } catch (ThreadCommissionerException ex) { + return Futures.immediateFailedFuture(ex); + } + }, + executor); } @Override - public CompletableFuture deleteThreadNetworkCredential( + public ListenableFuture getThreadNetworkCredentials( @NonNull BorderAgentInfo borderAgentInfo) { - return borderAgentDatabase.deleteBorderAgent(borderAgentInfo.discriminator); + return FluentFuture.from( + CommissionerUtils.toListenableFuture( + threadNetworkClient.getCredentialsByBorderAgent( + ThreadBorderAgent.newBuilder(borderAgentInfo.id).build()))) + .transform(result -> result.getCredentials(), MoreExecutors.directExecutor()); } - // This method adds given Thread Network Credential into database on the phone. - private CompletableFuture addThreadNetworkCredential( - @NonNull BorderAgentInfo borderAgentInfo, - @NonNull byte[] pskc, - @Nullable ThreadNetworkCredential networkCredential) { - BorderAgentRecord borderAgentRecord = - new BorderAgentRecord( - borderAgentInfo.discriminator, - borderAgentInfo.networkName, - borderAgentInfo.extendedPanId, - pskc, - networkCredential == null ? null : networkCredential.getEncoded()); - return borderAgentDatabase.insertBorderAgent(borderAgentRecord); + /** Adds given Thread Network Credential into database on the phone. */ + private ListenableFuture addThreadNetworkCredentials( + BorderAgentInfo borderAgentInfo, ThreadNetworkCredentials credentials) { + return CommissionerUtils.toListenableFuture( + threadNetworkClient.addCredentials( + ThreadBorderAgent.newBuilder(borderAgentInfo.id).build(), credentials)); } private void doCommissionJoinerDevice( @@ -177,8 +157,13 @@ private void doCommissionJoinerDevice( nativeCommissioner.petition( existingCommissionerId, borderAgentInfo.host.getHostAddress(), borderAgentInfo.port)); - // Save PSKc for current Border Agent once we have successfully connected to it. - addThreadNetworkCredential(borderAgentInfo, pskc, null).get(); + // Retrieves active dataset and saves it to GMS Core + ByteArray rawActiveDataset = new ByteArray(); + throwIfFail(nativeCommissioner.getRawActiveDataset(rawActiveDataset, 0xFFFF)); + ThreadNetworkCredentials credentials = + ThreadNetworkCredentials.fromActiveOperationalDataset( + CommissionerUtils.getByteArray(rawActiveDataset)); + addThreadNetworkCredentials(borderAgentInfo, credentials).get(); if (intermediateStateCallback != null) { intermediateStateCallback.onPetitioned(); @@ -235,8 +220,6 @@ private byte[] doFetchActiveDataset( getBorderAgentAddress(borderAgentInfo), borderAgentInfo.port)); - addThreadNetworkCredential(borderAgentInfo, pskc, null).get(); - if (intermediateStateCallback != null) { intermediateStateCallback.onPetitioned(); } @@ -245,10 +228,6 @@ private byte[] doFetchActiveDataset( ByteArray rawActiveDataset = new ByteArray(); throwIfFail(nativeCommissioner.getRawActiveDataset(rawActiveDataset, 0xFFFF)); return CommissionerUtils.getByteArray(rawActiveDataset); - } catch (InterruptedException e) { - throw new ThreadCommissionerException(ErrorCode.kUnknown, e.getMessage()); - } catch (ExecutionException e) { - throw new ThreadCommissionerException(ErrorCode.kUnknown, e.getMessage()); } finally { nativeCommissioner.resign(); } @@ -320,7 +299,7 @@ public void onEnergyReport(String aPeerAddr, ChannelMask aChannelMask, ByteArray @Override public void onDatasetChanged() { - Log.d(TAG, "Thread Network Dataset chanaged"); + Log.d(TAG, "Thread Network Dataset changed"); } private ByteArray getCurJoinerId() { diff --git a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadNetworkCredential.java b/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadNetworkCredential.java deleted file mode 100644 index 3d09edd31..000000000 --- a/android/openthread_commissioner/service/src/main/java/io/openthread/commissioner/service/ThreadNetworkCredential.java +++ /dev/null @@ -1,78 +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.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; - -public class ThreadNetworkCredential implements NetworkCredential, Parcelable { - - @NonNull private final byte[] activeOperationalDataset; - - public ThreadNetworkCredential(@NonNull byte[] activeOperationalDataset) { - this.activeOperationalDataset = activeOperationalDataset; - } - - protected ThreadNetworkCredential(Parcel in) { - activeOperationalDataset = in.createByteArray(); - } - - public static final Creator CREATOR = - new Creator() { - @Override - public ThreadNetworkCredential createFromParcel(Parcel in) { - return new ThreadNetworkCredential(in); - } - - @Override - public ThreadNetworkCredential[] newArray(int size) { - return new ThreadNetworkCredential[size]; - } - }; - - public byte[] getActiveOperationalDataset() { - return activeOperationalDataset; - } - - @Override - public byte[] getEncoded() { - return activeOperationalDataset; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeByteArray(activeOperationalDataset); - } -} diff --git a/android/openthread_commissioner/service/src/main/res/values/styles.xml b/android/openthread_commissioner/service/src/main/res/values/styles.xml index 3641696d0..9b1966677 100644 --- a/android/openthread_commissioner/service/src/main/res/values/styles.xml +++ b/android/openthread_commissioner/service/src/main/res/values/styles.xml @@ -10,4 +10,19 @@ + + + +