mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 02:25:19 +00:00
Hook up signal device linking view.
This commit is contained in:
parent
549631848d
commit
0f5db5aa33
1437
AndroidManifest.xml
1437
AndroidManifest.xml
File diff suppressed because it is too large
Load Diff
@ -12,9 +12,11 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<!--
|
||||||
<TextView android:id="@+id/created"
|
<TextView android:id="@+id/created"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -29,5 +31,5 @@
|
|||||||
android:textColor="?attr/conversation_list_item_subject_color"
|
android:textColor="?attr/conversation_list_item_subject_color"
|
||||||
android:fontFamily="sans-serif-light"
|
android:fontFamily="sans-serif-light"
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
-->
|
||||||
</org.thoughtcrime.securesms.DeviceListItem>
|
</org.thoughtcrime.securesms.DeviceListItem>
|
@ -299,6 +299,7 @@
|
|||||||
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
|
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
|
||||||
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
|
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
|
||||||
<string name="DeviceListActivity_network_failed">Network failed!</string>
|
<string name="DeviceListActivity_network_failed">Network failed!</string>
|
||||||
|
<string name="DeviceListActivity_unlinked_device">Successfully unlinked device</string>
|
||||||
|
|
||||||
<!-- DeviceListItem -->
|
<!-- DeviceListItem -->
|
||||||
<string name="DeviceListItem_unnamed_device">Unnamed device</string>
|
<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_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_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_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_show_seed_button_title">Show Seed</string>
|
||||||
<string name="activity_settings_seed_dialog_title">Your Seed</string>
|
<string name="activity_settings_seed_dialog_title">Your Seed</string>
|
||||||
<string name="activity_settings_seed_dialog_copy_button_title">Copy</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:title="@string/activity_settings_show_qr_code_button_title"
|
||||||
android:icon="@drawable/icon_qr_code"/>
|
android:icon="@drawable/icon_qr_code"/>
|
||||||
|
|
||||||
<Preference android:key="preference_category_link_device"
|
<Preference android:key="preference_category_linked_devices"
|
||||||
android:title="Link Device"
|
android:title="@string/activity_settings_linked_devices_button_title"
|
||||||
android:icon="@drawable/icon_link"/>
|
android:icon="@drawable/icon_link"/>
|
||||||
|
|
||||||
<Preference android:key="preference_category_seed"
|
<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.DeviceLinkingDialog;
|
||||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
|
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
|
||||||
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
|
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
|
||||||
|
import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
|
||||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||||
import org.thoughtcrime.securesms.loki.QRCodeDialog;
|
import org.thoughtcrime.securesms.loki.QRCodeDialog;
|
||||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
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_ADVANCED = "preference_category_advanced";
|
||||||
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key";
|
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_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 static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
@ -192,20 +193,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
||||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
|
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
|
||||||
|
|
||||||
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
|
Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
|
||||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
|
linkDevicesPreference.setVisible(isMasterDevice);
|
||||||
|
linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINKED_DEVICES));
|
||||||
// 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 seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
||||||
// Hide if this is a slave device
|
// 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_ADVANCED).setIcon(advanced);
|
||||||
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
|
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
|
||||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode);
|
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);
|
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +350,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||||||
case PREFERENCE_CATEGORY_QR_CODE:
|
case PREFERENCE_CATEGORY_QR_CODE:
|
||||||
QRCodeDialog.INSTANCE.show(getContext());
|
QRCodeDialog.INSTANCE.show(getContext());
|
||||||
break;
|
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);
|
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
|
||||||
break;
|
break;
|
||||||
case PREFERENCE_CATEGORY_SEED:
|
case PREFERENCE_CATEGORY_SEED:
|
||||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.ListFragment;
|
import android.support.v4.app.ListFragment;
|
||||||
@ -17,25 +16,21 @@ import android.widget.AdapterView;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.melnykov.fab.FloatingActionButton;
|
import com.melnykov.fab.FloatingActionButton;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.devicelist.Device;
|
import org.thoughtcrime.securesms.devicelist.Device;
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
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.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import org.whispersystems.libsignal.util.guava.Function;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -46,14 +41,13 @@ public class DeviceListFragment extends ListFragment
|
|||||||
|
|
||||||
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
||||||
|
|
||||||
@Inject
|
private File languageFileDirectory;
|
||||||
SignalServiceAccountManager accountManager;
|
|
||||||
|
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
private View empty;
|
private View empty;
|
||||||
private View progressContainer;
|
private View progressContainer;
|
||||||
private FloatingActionButton addDeviceButton;
|
private FloatingActionButton addDeviceButton;
|
||||||
private Button.OnClickListener addDeviceButtonListener;
|
private Button.OnClickListener addDeviceButtonListener;
|
||||||
|
private Function<String, Void> handleDisconnectDevice;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
@ -82,6 +76,7 @@ public class DeviceListFragment extends ListFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle bundle) {
|
public void onActivityCreated(Bundle bundle) {
|
||||||
super.onActivityCreated(bundle);
|
super.onActivityCreated(bundle);
|
||||||
|
this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext());
|
||||||
getLoaderManager().initLoader(0, null, this);
|
getLoaderManager().initLoader(0, null, this);
|
||||||
getListView().setOnItemClickListener(this);
|
getListView().setOnItemClickListener(this);
|
||||||
}
|
}
|
||||||
@ -90,12 +85,20 @@ public class DeviceListFragment extends ListFragment
|
|||||||
this.addDeviceButtonListener = listener;
|
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
|
@Override
|
||||||
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
|
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
|
||||||
empty.setVisibility(View.GONE);
|
empty.setVisibility(View.GONE);
|
||||||
progressContainer.setVisibility(View.VISIBLE);
|
progressContainer.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
return new DeviceListLoader(getActivity(), accountManager);
|
return new DeviceListLoader(getActivity(), languageFileDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -125,7 +128,7 @@ public class DeviceListFragment extends ListFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
final String deviceName = ((DeviceListItem)view).getDeviceName();
|
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());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
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() {
|
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
handleDisconnectDevice(deviceId);
|
if (handleDisconnectDevice != null) { handleDisconnectDevice.apply(deviceId); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||||
|
}
|
||||||
|
|
||||||
private void handleLoaderFailed() {
|
private void handleLoaderFailed() {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
|
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
|
||||||
@ -167,34 +174,6 @@ public class DeviceListFragment extends ListFragment
|
|||||||
builder.show();
|
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
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);
|
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);
|
||||||
|
@ -15,7 +15,7 @@ import network.loki.messenger.R;
|
|||||||
|
|
||||||
public class DeviceListItem extends LinearLayout {
|
public class DeviceListItem extends LinearLayout {
|
||||||
|
|
||||||
private long deviceId;
|
private String deviceId;
|
||||||
private TextView name;
|
private TextView name;
|
||||||
private TextView created;
|
private TextView created;
|
||||||
private TextView lastActive;
|
private TextView lastActive;
|
||||||
@ -32,14 +32,15 @@ public class DeviceListItem extends LinearLayout {
|
|||||||
public void onFinishInflate() {
|
public void onFinishInflate() {
|
||||||
super.onFinishInflate();
|
super.onFinishInflate();
|
||||||
this.name = (TextView) findViewById(R.id.name);
|
this.name = (TextView) findViewById(R.id.name);
|
||||||
this.created = (TextView) findViewById(R.id.created);
|
// this.created = (TextView) findViewById(R.id.created);
|
||||||
this.lastActive = (TextView) findViewById(R.id.active);
|
// this.lastActive = (TextView) findViewById(R.id.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set(Device deviceInfo, Locale locale) {
|
public void set(Device deviceInfo, Locale locale) {
|
||||||
if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device);
|
if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device);
|
||||||
else this.name.setText(deviceInfo.getName());
|
else this.name.setText(deviceInfo.getName());
|
||||||
|
|
||||||
|
/*
|
||||||
this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s,
|
this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s,
|
||||||
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
||||||
locale,
|
locale,
|
||||||
@ -49,11 +50,12 @@ public class DeviceListItem extends LinearLayout {
|
|||||||
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
||||||
locale,
|
locale,
|
||||||
deviceInfo.getLastSeen())));
|
deviceInfo.getLastSeen())));
|
||||||
|
*/
|
||||||
|
|
||||||
this.deviceId = deviceInfo.getId();
|
this.deviceId = deviceInfo.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDeviceId() {
|
public String getDeviceId() {
|
||||||
return deviceId;
|
return deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,13 @@ import android.text.TextUtils;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.devicelist.Device;
|
import org.thoughtcrime.securesms.devicelist.Device;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.libsignal.ecc.Curve;
|
import org.whispersystems.libsignal.ecc.Curve;
|
||||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
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.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
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.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@ -33,93 +39,42 @@ import javax.crypto.spec.IvParameterSpec;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
|
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
|
||||||
|
import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded;
|
||||||
|
|
||||||
public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
||||||
|
|
||||||
private static final String TAG = DeviceListLoader.class.getSimpleName();
|
private static final String TAG = DeviceListLoader.class.getSimpleName();
|
||||||
|
private MnemonicCodec mnemonicCodec;
|
||||||
|
|
||||||
private final SignalServiceAccountManager accountManager;
|
public DeviceListLoader(Context context, File languageFileDirectory) {
|
||||||
|
|
||||||
public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) {
|
|
||||||
super(context);
|
super(context);
|
||||||
this.accountManager = accountManager;
|
this.mnemonicCodec = new MnemonicCodec(languageFileDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> loadInBackground() {
|
public List<Device> loadInBackground() {
|
||||||
try {
|
try {
|
||||||
List<Device> devices = Stream.of(accountManager.getDevices())
|
String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||||
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID)
|
List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
|
||||||
.map(this::mapToDevice)
|
List<Device> devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
Collections.sort(devices, new DeviceComparator());
|
Collections.sort(devices, new DeviceComparator());
|
||||||
|
|
||||||
return devices;
|
return devices;
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Device mapToDevice(@NonNull DeviceInfo deviceInfo) {
|
private Device mapToDevice(@NonNull String hexEncodedPublicKey) {
|
||||||
try {
|
long now = System.currentTimeMillis();
|
||||||
if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) {
|
return new Device(hexEncodedPublicKey, MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey), now, now);
|
||||||
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 static class DeviceComparator implements Comparator<Device> {
|
private static class DeviceComparator implements Comparator<Device> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compare(Device lhs, Device rhs) {
|
public int compare(Device lhs, Device rhs) {
|
||||||
if (lhs.getCreated() < rhs.getCreated()) return -1;
|
return lhs.getName().compareTo(rhs.getName());
|
||||||
else if (lhs.getCreated() != rhs.getCreated()) return 1;
|
|
||||||
else return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.devicelist;
|
|||||||
|
|
||||||
public class Device {
|
public class Device {
|
||||||
|
|
||||||
private final long id;
|
private final String id;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final long created;
|
private final long created;
|
||||||
private final long lastSeen;
|
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.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.created = created;
|
this.created = created;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,31 +32,10 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
|
|||||||
private constructor(context: Context) : this(context, null)
|
private constructor(context: Context) : this(context, null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setUpLanguageFileDirectory()
|
languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(context)
|
||||||
setUpViewHierarchy()
|
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() {
|
private fun setUpViewHierarchy() {
|
||||||
inflate(context, R.layout.view_device_linking, this)
|
inflate(context, R.layout.view_device_linking, this)
|
||||||
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
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)
|
explanationTextView.text = resources.getString(explanationID)
|
||||||
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||||
if (mode == Mode.Slave) {
|
if (mode == Mode.Slave) {
|
||||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
|
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
authorizeButton.visibility = View.GONE
|
authorizeButton.visibility = View.GONE
|
||||||
authorizeButton.setOnClickListener { authorizePairing() }
|
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
|
val database = databaseHelper.readableDatabase
|
||||||
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
|
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
|
// 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