From 0f5db5aa33d9753babd344d02139a28f140443ea Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 20 Nov 2019 09:50:40 +1100 Subject: [PATCH] Hook up signal device linking view. --- AndroidManifest.xml | 1441 +++++++++-------- res/layout/device_list_item_view.xml | 4 +- res/values/strings.xml | 3 +- res/xml/preferences.xml | 4 +- .../ApplicationPreferencesActivity.java | 26 +- .../securesms/DeviceListFragment.java | 63 +- .../securesms/DeviceListItem.java | 10 +- .../database/loaders/DeviceListLoader.java | 81 +- .../securesms/devicelist/Device.java | 6 +- .../securesms/loki/DeviceLinkingView.kt | 27 +- .../securesms/loki/LinkedDevicesActivity.kt | 76 + .../securesms/loki/LokiAPIDatabase.kt | 5 + .../securesms/loki/MnemonicUtilities.kt | 36 + 13 files changed, 919 insertions(+), 863 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt create mode 100644 src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3b18e8b60b..3ed51067ca 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,35 +1,50 @@ + xmlns:tools="http://schemas.android.com/tools" + package="network.loki.messenger"> - + - + - - - - - - - + + + + - - - - - - - - + + --> - - - - - - - + - - - - - + + - + - - - - + - - - - - - - - - - - - - + + + + + + - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + - - - - - - - - + + + - + + + - - - - - + + - - - - - - - + + + + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/device_list_item_view.xml b/res/layout/device_list_item_view.xml index c6919fb7d5..b5a0432d32 100644 --- a/res/layout/device_list_item_view.xml +++ b/res/layout/device_list_item_view.xml @@ -12,9 +12,11 @@ android:singleLine="true" android:ellipsize="marquee" android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 307c000c62..0c78c98786 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -299,6 +299,7 @@ Unlinking device... Unlinking device Network failed! + Successfully unlinked device Unnamed device @@ -1576,7 +1577,7 @@ Copied to clipboard Share Public Key Show QR Code - Link Device + Linked Device Show Seed Your Seed Copy diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 270c9254d3..5cce165b4e 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -41,8 +41,8 @@ android:title="@string/activity_settings_show_qr_code_button_title" android:icon="@drawable/icon_qr_code"/> - handleDisconnectDevice; @Override public void onCreate(Bundle savedInstanceState) { @@ -82,6 +76,7 @@ public class DeviceListFragment extends ListFragment @Override public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); + this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext()); getLoaderManager().initLoader(0, null, this); getListView().setOnItemClickListener(this); } @@ -90,12 +85,20 @@ public class DeviceListFragment extends ListFragment this.addDeviceButtonListener = listener; } + public void setHandleDisconnectDevice(Function handler) { + this.handleDisconnectDevice = handler; + } + + public void setAddDeviceButtonVisible(boolean visible) { + addDeviceButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + @Override public @NonNull Loader> onCreateLoader(int id, Bundle args) { empty.setVisibility(View.GONE); progressContainer.setVisibility(View.VISIBLE); - return new DeviceListLoader(getActivity(), accountManager); + return new DeviceListLoader(getActivity(), languageFileDirectory); } @Override @@ -125,7 +128,7 @@ public class DeviceListFragment extends ListFragment @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final String deviceName = ((DeviceListItem)view).getDeviceName(); - final long deviceId = ((DeviceListItem)view).getDeviceId(); + final String deviceId = ((DeviceListItem)view).getDeviceId(); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName)); @@ -134,12 +137,16 @@ public class DeviceListFragment extends ListFragment builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - handleDisconnectDevice(deviceId); + if (handleDisconnectDevice != null) { handleDisconnectDevice.apply(deviceId); } } }); builder.show(); } + public void refresh() { + getLoaderManager().restartLoader(0, null, DeviceListFragment.this); + } + private void handleLoaderFailed() { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.DeviceListActivity_network_connection_failed); @@ -167,34 +174,6 @@ public class DeviceListFragment extends ListFragment builder.show(); } - private void handleDisconnectDevice(final long deviceId) { - new ProgressDialogAsyncTask(getActivity(), - R.string.DeviceListActivity_unlinking_device_no_ellipsis, - R.string.DeviceListActivity_unlinking_device) - { - @Override - protected Void doInBackground(Void... params) { - try { - accountManager.removeDevice(deviceId); - - ApplicationContext.getInstance(getContext()) - .getJobManager() - .add(new RefreshUnidentifiedDeliveryAbilityJob()); - } catch (IOException e) { - Log.w(TAG, e); - Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show(); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - getLoaderManager().restartLoader(0, null, DeviceListFragment.this); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - @Override public void onClick(View v) { if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v); diff --git a/src/org/thoughtcrime/securesms/DeviceListItem.java b/src/org/thoughtcrime/securesms/DeviceListItem.java index 47331a549d..9f38c89dd0 100644 --- a/src/org/thoughtcrime/securesms/DeviceListItem.java +++ b/src/org/thoughtcrime/securesms/DeviceListItem.java @@ -15,7 +15,7 @@ import network.loki.messenger.R; public class DeviceListItem extends LinearLayout { - private long deviceId; + private String deviceId; private TextView name; private TextView created; private TextView lastActive; @@ -32,14 +32,15 @@ public class DeviceListItem extends LinearLayout { public void onFinishInflate() { super.onFinishInflate(); this.name = (TextView) findViewById(R.id.name); - this.created = (TextView) findViewById(R.id.created); - this.lastActive = (TextView) findViewById(R.id.active); + // this.created = (TextView) findViewById(R.id.created); + // this.lastActive = (TextView) findViewById(R.id.active); } public void set(Device deviceInfo, Locale locale) { if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device); else this.name.setText(deviceInfo.getName()); + /* this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s, DateUtils.getDayPrecisionTimeSpanString(getContext(), locale, @@ -49,11 +50,12 @@ public class DeviceListItem extends LinearLayout { DateUtils.getDayPrecisionTimeSpanString(getContext(), locale, deviceInfo.getLastSeen()))); + */ this.deviceId = deviceInfo.getId(); } - public long getDeviceId() { + public String getDeviceId() { return deviceId; } diff --git a/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java index 7d653dff84..1f661b67db 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java @@ -7,10 +7,13 @@ import android.text.TextUtils; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.devicelist.Device; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.MnemonicUtilities; import org.thoughtcrime.securesms.util.AsyncLoader; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECPrivateKey; @@ -19,7 +22,10 @@ import org.whispersystems.libsignal.util.ByteUtil; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.loki.api.LokiStorageAPI; +import org.whispersystems.signalservice.loki.crypto.MnemonicCodec; +import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.MessageDigest; @@ -33,93 +39,42 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*; +import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded; public class DeviceListLoader extends AsyncLoader> { private static final String TAG = DeviceListLoader.class.getSimpleName(); + private MnemonicCodec mnemonicCodec; - private final SignalServiceAccountManager accountManager; - - public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) { + public DeviceListLoader(Context context, File languageFileDirectory) { super(context); - this.accountManager = accountManager; + this.mnemonicCodec = new MnemonicCodec(languageFileDirectory); } @Override public List loadInBackground() { try { - List devices = Stream.of(accountManager.getDevices()) - .filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID) - .map(this::mapToDevice) - .toList(); - + String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext()); + List secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get(); + List devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList(); Collections.sort(devices, new DeviceComparator()); - return devices; - } catch (IOException e) { + } catch (Exception e) { Log.w(TAG, e); return null; } } - private Device mapToDevice(@NonNull DeviceInfo deviceInfo) { - try { - if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) { - throw new IOException("Invalid DeviceInfo name."); - } - - DeviceName deviceName = DeviceName.parseFrom(Base64.decode(deviceInfo.getName())); - - if (!deviceName.hasCiphertext() || !deviceName.hasEphemeralPublic() || !deviceName.hasSyntheticIv()) { - throw new IOException("Got a DeviceName that wasn't properly populated."); - } - - byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray(); - byte[] cipherText = deviceName.getCiphertext().toByteArray(); - ECPrivateKey identityKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey(); - ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0); - byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey); - - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(masterSecret, "HmacSHA256")); - byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes()); - - mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256")); - byte[] cipherKey = mac.doFinal(syntheticIv); - - Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16])); - final byte[] plaintext = cipher.doFinal(cipherText); - - mac.init(new SecretKeySpec(masterSecret, "HmacSHA256")); - byte[] verificationPart1 = mac.doFinal("auth".getBytes()); - - mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256")); - byte[] verificationPart2 = mac.doFinal(plaintext); - byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16); - - if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) { - throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv."); - } - - return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen()); - - } catch (IOException e) { - Log.w(TAG, "Failed while reading the protobuf.", e); - } catch (GeneralSecurityException | InvalidKeyException e) { - Log.w(TAG, "Failed during decryption.", e); - } - - return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen()); + private Device mapToDevice(@NonNull String hexEncodedPublicKey) { + long now = System.currentTimeMillis(); + return new Device(hexEncodedPublicKey, MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey), now, now); } private static class DeviceComparator implements Comparator { @Override public int compare(Device lhs, Device rhs) { - if (lhs.getCreated() < rhs.getCreated()) return -1; - else if (lhs.getCreated() != rhs.getCreated()) return 1; - else return 0; + return lhs.getName().compareTo(rhs.getName()); } } } diff --git a/src/org/thoughtcrime/securesms/devicelist/Device.java b/src/org/thoughtcrime/securesms/devicelist/Device.java index 1cf302596d..00cdb6df34 100644 --- a/src/org/thoughtcrime/securesms/devicelist/Device.java +++ b/src/org/thoughtcrime/securesms/devicelist/Device.java @@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.devicelist; public class Device { - private final long id; + private final String id; private final String name; private final long created; private final long lastSeen; - public Device(long id, String name, long created, long lastSeen) { + public Device(String id, String name, long created, long lastSeen) { this.id = id; this.name = name; this.created = created; this.lastSeen = lastSeen; } - public long getId() { + public String getId() { return id; } diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt index b7983cc92b..35c5041d7e 100644 --- a/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt +++ b/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt @@ -32,31 +32,10 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe private constructor(context: Context) : this(context, null) init { - setUpLanguageFileDirectory() + languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(context) setUpViewHierarchy() } - private fun setUpLanguageFileDirectory() { - val languages = listOf( "english", "japanese", "portuguese", "spanish" ) - val directory = File(context.applicationInfo.dataDir) - for (language in languages) { - val fileName = "$language.txt" - if (directory.list().contains(fileName)) { continue } - val inputStream = context.assets.open("mnemonic/$fileName") - val file = File(directory, fileName) - val outputStream = FileOutputStream(file) - val buffer = ByteArray(1024) - while (true) { - val count = inputStream.read(buffer) - if (count < 0) { break } - outputStream.write(buffer, 0, count) - } - inputStream.close() - outputStream.close() - } - languageFileDirectory = directory - } - private fun setUpViewHierarchy() { inflate(context, R.layout.view_device_linking, this) spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN) @@ -72,8 +51,8 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe explanationTextView.text = resources.getString(explanationID) mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE if (mode == Mode.Slave) { - val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() - mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ") + val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) + mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey) } authorizeButton.visibility = View.GONE authorizeButton.setOnClickListener { authorizePairing() } diff --git a/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt b/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt new file mode 100644 index 0000000000..7a61384ad8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.loki + +import android.os.Bundle +import android.view.MenuItem +import android.widget.Toast +import org.thoughtcrime.securesms.* +import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.DynamicLanguage +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.sms.MessageSender +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.loki.api.LokiStorageAPI + +class LinkedDevicesActivity : PassphraseRequiredActionBarActivity() { + + companion object { + private val TAG = DeviceActivity::class.java.simpleName + } + + private val dynamicTheme = DynamicTheme() + private val dynamicLanguage = DynamicLanguage() + private lateinit var deviceListFragment: DeviceListFragment + + public override fun onPreCreate() { + dynamicTheme.onCreate(this) + dynamicLanguage.onCreate(this) + } + + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + super.onCreate(savedInstanceState, ready) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setTitle(R.string.AndroidManifest__linked_devices) + this.deviceListFragment = DeviceListFragment() + this.deviceListFragment.setAddDeviceButtonListener { + // TODO: Hook up add device + } + this.deviceListFragment.setHandleDisconnectDevice { devicePublicKey -> + // Purge the device pairing from our database + val ourPublicKey = TextSecurePreferences.getLocalNumber(this) + val database = DatabaseFactory.getLokiAPIDatabase(this) + database.removePairingAuthorisation(ourPublicKey, devicePublicKey) + // Update mapping on the file server + LokiStorageAPI.shared.updateUserDeviceMappings() + // Send a background message to let the device know that it has been revoked + MessageSender.sendBackgroundMessage(this, devicePublicKey) + // Refresh the list + refresh() + Toast.makeText(this, R.string.DeviceListActivity_unlinked_device, Toast.LENGTH_LONG).show() + return@setHandleDisconnectDevice null + } + initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.currentLocale) + refresh() + } + + private fun refresh() { + val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this) + val isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(this).getPairingAuthorisations(userHexEncodedPublicKey).isEmpty() + this.deviceListFragment.setAddDeviceButtonVisible(isDeviceLinkingEnabled) + this.deviceListFragment.refresh() + } + + public override fun onResume() { + super.onResume() + dynamicTheme.onResume(this) + dynamicLanguage.onResume(this) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return false + } +} diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 9ac5dc0ca0..85d8f38408 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -189,6 +189,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val database = databaseHelper.readableDatabase database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) } + + fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) { + val database = databaseHelper.readableDatabase + database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey )) + } } // region Convenience diff --git a/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt b/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt new file mode 100644 index 0000000000..a6e6a64538 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.loki + +import android.content.Context +import org.whispersystems.signalservice.loki.crypto.MnemonicCodec +import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded +import java.io.File +import java.io.FileOutputStream + +object MnemonicUtilities { + @JvmStatic + public fun getLanguageFileDirectory(context: Context): File { + val languages = listOf( "english", "japanese", "portuguese", "spanish" ) + val directory = File(context.applicationInfo.dataDir) + for (language in languages) { + val fileName = "$language.txt" + if (directory.list().contains(fileName)) { continue } + val inputStream = context.assets.open("mnemonic/$fileName") + val file = File(directory, fileName) + val outputStream = FileOutputStream(file) + val buffer = ByteArray(1024) + while (true) { + val count = inputStream.read(buffer) + if (count < 0) { break } + outputStream.write(buffer, 0, count) + } + inputStream.close() + outputStream.close() + } + return directory + } + + @JvmStatic + public fun getFirst3Words(codec: MnemonicCodec, hexEncodedPublicKey: String): String { + return codec.encode(hexEncodedPublicKey.removing05PrefixIfNeeded()).split(" ").slice(0 until 3).joinToString(" ") + } +} \ No newline at end of file