mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 20:15:21 +00:00
Hook up signal device linking view.
This commit is contained in:
parent
549631848d
commit
0f5db5aa33
1441
AndroidManifest.xml
1441
AndroidManifest.xml
File diff suppressed because it is too large
Load Diff
@ -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" />
|
||||
|
||||
<!--
|
||||
<TextView android:id="@+id/created"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -29,5 +31,5 @@
|
||||
android:textColor="?attr/conversation_list_item_subject_color"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
-->
|
||||
</org.thoughtcrime.securesms.DeviceListItem>
|
@ -299,6 +299,7 @@
|
||||
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
|
||||
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
|
||||
<string name="DeviceListActivity_network_failed">Network failed!</string>
|
||||
<string name="DeviceListActivity_unlinked_device">Successfully unlinked device</string>
|
||||
|
||||
<!-- DeviceListItem -->
|
||||
<string name="DeviceListItem_unnamed_device">Unnamed device</string>
|
||||
@ -1576,7 +1577,7 @@
|
||||
<string name="activity_settings_public_key_copied_message">Copied to clipboard</string>
|
||||
<string name="activity_settings_share_public_key_button_title">Share Public Key</string>
|
||||
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string>
|
||||
<string name="activity_settings_link_device_button_title">Link Device</string>
|
||||
<string name="activity_settings_linked_devices_button_title">Linked Device</string>
|
||||
<string name="activity_settings_show_seed_button_title">Show Seed</string>
|
||||
<string name="activity_settings_seed_dialog_title">Your Seed</string>
|
||||
<string name="activity_settings_seed_dialog_copy_button_title">Copy</string>
|
||||
|
@ -41,8 +41,8 @@
|
||||
android:title="@string/activity_settings_show_qr_code_button_title"
|
||||
android:icon="@drawable/icon_qr_code"/>
|
||||
|
||||
<Preference android:key="preference_category_link_device"
|
||||
android:title="Link Device"
|
||||
<Preference android:key="preference_category_linked_devices"
|
||||
android:title="@string/activity_settings_linked_devices_button_title"
|
||||
android:icon="@drawable/icon_link"/>
|
||||
|
||||
<Preference android:key="preference_category_seed"
|
||||
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
|
||||
import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.loki.QRCodeDialog;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
@ -89,7 +90,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
// private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key";
|
||||
private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code";
|
||||
private static final String PREFERENCE_CATEGORY_LINK_DEVICE = "preference_category_link_device";
|
||||
private static final String PREFERENCE_CATEGORY_LINKED_DEVICES = "preference_category_linked_devices";
|
||||
private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
@ -192,20 +193,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
|
||||
|
||||
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
|
||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
|
||||
// Disable if we hit the cap of 1 linked device
|
||||
if (isMasterDevice) {
|
||||
Context context = getContext();
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).size() <= 1;
|
||||
linkDevicePreference.setEnabled(isDeviceLinkingEnabled);
|
||||
linkDevicePreference.getIcon().setAlpha(isDeviceLinkingEnabled ? 255 : 124);
|
||||
} else {
|
||||
// Hide if this is a slave device
|
||||
linkDevicePreference.setVisible(false);
|
||||
}
|
||||
Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
|
||||
linkDevicesPreference.setVisible(isMasterDevice);
|
||||
linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINKED_DEVICES));
|
||||
|
||||
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
||||
// Hide if this is a slave device
|
||||
@ -299,7 +289,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
// this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
|
||||
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode);
|
||||
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE).setIcon(linkDevice);
|
||||
this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES).setIcon(linkDevice);
|
||||
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
|
||||
}
|
||||
|
||||
@ -360,7 +350,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
case PREFERENCE_CATEGORY_QR_CODE:
|
||||
QRCodeDialog.INSTANCE.show(getContext());
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_LINK_DEVICE:
|
||||
case PREFERENCE_CATEGORY_LINKED_DEVICES:
|
||||
Intent intent = new Intent(getActivity(), LinkedDevicesActivity.class);
|
||||
startActivity(intent);
|
||||
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_SEED:
|
||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ListFragment;
|
||||
@ -17,25 +16,21 @@ import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -46,14 +41,13 @@ public class DeviceListFragment extends ListFragment
|
||||
|
||||
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
||||
|
||||
@Inject
|
||||
SignalServiceAccountManager accountManager;
|
||||
|
||||
private File languageFileDirectory;
|
||||
private Locale locale;
|
||||
private View empty;
|
||||
private View progressContainer;
|
||||
private FloatingActionButton addDeviceButton;
|
||||
private Button.OnClickListener addDeviceButtonListener;
|
||||
private Function<String, Void> 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<String, Void> handler) {
|
||||
this.handleDisconnectDevice = handler;
|
||||
}
|
||||
|
||||
public void setAddDeviceButtonVisible(boolean visible) {
|
||||
addDeviceButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<List<Device>> 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<Void, Void, Void>(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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<List<Device>> {
|
||||
|
||||
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<Device> loadInBackground() {
|
||||
try {
|
||||
List<Device> devices = Stream.of(accountManager.getDevices())
|
||||
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
.map(this::mapToDevice)
|
||||
.toList();
|
||||
|
||||
String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
|
||||
List<Device> 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<Device> {
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() }
|
||||
|
76
src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
Normal file
76
src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
36
src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
Normal file
36
src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
Normal file
@ -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(" ")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user