From 20d2c43356270002224153de036c9e02a5c95506 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 13 Dec 2021 16:54:19 -0400 Subject: [PATCH] Migrate identity verification activity to fragment. --- app/src/main/AndroidManifest.xml | 2 +- .../securesms/DeviceActivity.java | 2 +- .../securesms/VerifyIdentityActivity.java | 774 ------------------ .../ConversationSettingsFragment.kt | 2 +- .../conversation/ConversationActivity.java | 2 +- .../conversation/ConversationFragment.java | 3 +- .../conversation/ConversationUpdateItem.java | 4 +- .../ui/error/SafetyNumberChangeDialog.java | 3 +- .../securesms/qr/ScanListener.java | 4 +- .../bottomsheet/RecipientDialogViewModel.java | 3 +- .../securesms/util/VerifySpan.java | 2 +- .../verify/VerifyDisplayFragment.java | 592 ++++++++++++++ .../verify/VerifyIdentityActivity.java | 85 ++ .../verify/VerifyIdentityFragment.kt | 98 +++ .../securesms/verify/VerifyScanFragment.kt | 72 ++ 15 files changed, 859 insertions(+), 789 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3bf92947d0..426dbd7c37 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -380,7 +380,7 @@ android:label="@string/AndroidManifest__change_passphrase" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> - diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index 961499eee9..b97d6088ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -120,7 +120,7 @@ public class DeviceActivity extends PassphraseRequiredActivity } @Override - public void onQrDataFound(final String data) { + public void onQrDataFound(@NonNull final String data) { ThreadUtil.runOnMain(() -> { ((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50); Uri uri = Uri.parse(data); diff --git a/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java deleted file mode 100644 index 8cc2963ac8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ /dev/null @@ -1,774 +0,0 @@ -/* - * Copyright (C) 2016-2017 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms; - -import android.Manifest; -import android.animation.TypeEvaluator; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.PorterDuff; -import android.graphics.drawable.BitmapDrawable; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Vibrator; -import android.text.Html; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.animation.Animation; -import android.view.animation.AnticipateInterpolator; -import android.view.animation.ScaleAnimation; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.ScrollView; -import android.widget.TextSwitcher; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.core.view.OneShotPreDrawListener; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.signal.core.util.ThreadUtil; -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.components.ShapeScrim; -import org.thoughtcrime.securesms.components.camera.CameraView; -import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; -import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.database.IdentityDatabase; -import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; -import org.thoughtcrime.securesms.database.model.IdentityRecord; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.qr.QrCode; -import org.thoughtcrime.securesms.qr.ScanListener; -import org.thoughtcrime.securesms.qr.ScanningThread; -import org.thoughtcrime.securesms.recipients.LiveRecipient; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; -import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.IdentityUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; -import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; -import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.util.UuidUtil; - -import java.nio.charset.Charset; -import java.util.Locale; - -/** - * Activity for verifying identity keys. - * - * @author Moxie Marlinspike - */ -@SuppressLint("StaticFieldLeak") -public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener { - - private static final String TAG = Log.tag(VerifyIdentityActivity.class); - - private static final String RECIPIENT_EXTRA = "recipient_id"; - private static final String IDENTITY_EXTRA = "recipient_identity"; - private static final String VERIFIED_EXTRA = "verified_state"; - - private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); - - private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment(); - private final VerifyScanFragment scanFragment = new VerifyScanFragment(); - - public static Intent newIntent(@NonNull Context context, - @NonNull IdentityRecord identityRecord) - { - return newIntent(context, - identityRecord.getRecipientId(), - identityRecord.getIdentityKey(), - identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED); - } - - public static Intent newIntent(@NonNull Context context, - @NonNull IdentityRecord identityRecord, - boolean verified) - { - return newIntent(context, - identityRecord.getRecipientId(), - identityRecord.getIdentityKey(), - verified); - } - - public static Intent newIntent(@NonNull Context context, - @NonNull RecipientId recipientId, - @NonNull IdentityKey identityKey, - boolean verified) - { - Intent intent = new Intent(context, VerifyIdentityActivity.class); - - intent.putExtra(RECIPIENT_EXTRA, recipientId); - intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey)); - intent.putExtra(VERIFIED_EXTRA, verified); - - return intent; - } - - @Override - public void onPreCreate() { - dynamicTheme.onCreate(this); - } - - @Override - protected void onCreate(Bundle state, boolean ready) { - Bundle extras = new Bundle(); - extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA)); - extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA)); - extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this))); - extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, Recipient.self().requireE164()); - extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false)); - - scanFragment.setScanListener(this); - displayFragment.setClickListener(this); - - initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: finish(); return true; - } - - return false; - } - - @Override - public void onQrDataFound(final String data) { - ThreadUtil.runOnMain(() -> { - ((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50); - - getSupportFragmentManager().popBackStack(); - displayFragment.setScannedFingerprint(data); - }); - } - - @Override - public void onClick(View v) { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied)) - .onAllGranted(() -> { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom, - R.anim.slide_from_bottom, R.anim.slide_to_top); - - transaction.replace(android.R.id.content, scanFragment) - .addToBackStack(null) - .commitAllowingStateLoss(); - }) - .onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show()) - .execute(); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener { - - public static final String RECIPIENT_ID = "recipient_id"; - public static final String REMOTE_NUMBER = "remote_number"; - public static final String REMOTE_IDENTITY = "remote_identity"; - public static final String LOCAL_IDENTITY = "local_identity"; - public static final String LOCAL_NUMBER = "local_number"; - public static final String VERIFIED_STATE = "verified_state"; - - private LiveRecipient recipient; - private IdentityKey localIdentity; - private IdentityKey remoteIdentity; - private Fingerprint fingerprint; - - private Toolbar toolbar; - private ScrollView scrollView; - private View container; - private View numbersContainer; - private View loading; - private View qrCodeContainer; - private ImageView qrCode; - private ImageView qrVerified; - private TextSwitcher tapLabel; - private TextView description; - private View.OnClickListener clickListener; - private Button verifyButton; - private View toolbarShadow; - private View bottomShadow; - - private TextView[] codes = new TextView[12]; - private boolean animateSuccessOnDraw = false; - private boolean animateFailureOnDraw = false; - private boolean currentVerifiedState = false; - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { - this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment); - this.toolbar = container.findViewById(R.id.toolbar); - this.scrollView = container.findViewById(R.id.scroll_view); - this.numbersContainer = container.findViewById(R.id.number_table); - this.loading = container.findViewById(R.id.loading); - this.qrCodeContainer = container.findViewById(R.id.qr_code_container); - this.qrCode = container.findViewById(R.id.qr_code); - this.verifyButton = container.findViewById(R.id.verify_button); - this.qrVerified = container.findViewById(R.id.qr_verified); - this.description = container.findViewById(R.id.description); - this.tapLabel = container.findViewById(R.id.tap_label); - this.toolbarShadow = container.findViewById(R.id.toolbar_shadow); - this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow); - this.codes[0] = container.findViewById(R.id.code_first); - this.codes[1] = container.findViewById(R.id.code_second); - this.codes[2] = container.findViewById(R.id.code_third); - this.codes[3] = container.findViewById(R.id.code_fourth); - this.codes[4] = container.findViewById(R.id.code_fifth); - this.codes[5] = container.findViewById(R.id.code_sixth); - this.codes[6] = container.findViewById(R.id.code_seventh); - this.codes[7] = container.findViewById(R.id.code_eighth); - this.codes[8] = container.findViewById(R.id.code_ninth); - this.codes[9] = container.findViewById(R.id.code_tenth); - this.codes[10] = container.findViewById(R.id.code_eleventh); - this.codes[11] = container.findViewById(R.id.code_twelth); - - this.qrCodeContainer.setOnClickListener(clickListener); - this.registerForContextMenu(numbersContainer); - - updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false); - this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true))); - - this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this); - - ((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar); - ((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number); - - return container; - } - - @Override public void onDestroyView() { - this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this); - super.onDestroyView(); - } - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - - RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID); - IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY); - IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY); - - if (recipientId == null) throw new AssertionError("RecipientId required"); - if (localIdentityParcelable == null) throw new AssertionError("local identity required"); - if (remoteIdentityParcelable == null) throw new AssertionError("remote identity required"); - - this.localIdentity = localIdentityParcelable.get(); - this.recipient = Recipient.live(recipientId); - this.remoteIdentity = remoteIdentityParcelable.get(); - - int version; - byte[] localId; - byte[] remoteId; - - //noinspection WrongThread - Recipient resolved = recipient.resolve(); - - if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) { - Log.i(TAG, "Using UUID (version 2)."); - version = 2; - localId = Recipient.self().requireAci().toByteArray(); - remoteId = resolved.requireAci().toByteArray(); - } else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) { - Log.i(TAG, "Using E164 (version 1)."); - version = 1; - localId = Recipient.self().requireE164().getBytes(); - remoteId = resolved.requireE164().getBytes(); - } else { - Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getAci().isPresent(), resolved.getE164().isPresent())); - new MaterialAlertDialogBuilder(requireContext()) - .setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext()))) - .setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish()) - .setOnDismissListener(dialog -> { - requireActivity().finish(); - dialog.dismiss(); - }) - .show(); - return; - } - - this.recipient.observe(this, this::setRecipientText); - - new AsyncTask() { - @Override - protected Fingerprint doInBackground(Void... params) { - return new NumericFingerprintGenerator(5200).createFor(version, - localId, localIdentity, - remoteId, remoteIdentity); - } - - @Override - protected void onPostExecute(Fingerprint fingerprint) { - if (getActivity() == null) return; - VerifyDisplayFragment.this.fingerprint = fingerprint; - setFingerprintViews(fingerprint, true); - getActivity().supportInvalidateOptionsMenu(); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - setHasOptionsMenu(true); - } - - @Override - public void onResume() { - super.onResume(); - - setRecipientText(recipient.get()); - - if (fingerprint != null) { - setFingerprintViews(fingerprint, false); - } - - if (animateSuccessOnDraw) { - animateSuccessOnDraw = false; - animateVerifiedSuccess(); - } else if (animateFailureOnDraw) { - animateFailureOnDraw = false; - animateVerifiedFailure(); - } - - ThreadUtil.postToMain(this::onScrollChanged); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, - ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - - if (fingerprint != null) { - MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.verify_display_fragment_context_menu, menu); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (fingerprint == null) return super.onContextItemSelected(item); - - switch (item.getItemId()) { - case R.id.menu_copy: handleCopyToClipboard(fingerprint, codes.length); return true; - case R.id.menu_compare: handleCompareWithClipboard(fingerprint); return true; - default: return super.onContextItemSelected(item); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - if (fingerprint != null) { - inflater.inflate(R.menu.verify_identity, menu); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.verify_identity__share: handleShare(fingerprint, codes.length); return true; - } - - return false; - } - - public void setScannedFingerprint(String scanned) { - try { - if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) { - this.animateSuccessOnDraw = true; - } else { - this.animateFailureOnDraw = true; - } - } catch (FingerprintVersionMismatchException e) { - Log.w(TAG, e); - if (e.getOurVersion() < e.getTheirVersion()) { - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show(); - } - this.animateFailureOnDraw = true; - } catch (Exception e) { - Log.w(TAG, e); - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show(); - this.animateFailureOnDraw = true; - } - } - - public void setClickListener(View.OnClickListener listener) { - this.clickListener = listener; - } - - private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) { - String[] segments = getSegments(fingerprint, segmentCount); - StringBuilder result = new StringBuilder(); - - for (int i = 0; i < segments.length; i++) { - result.append(segments[i]); - - if (i != segments.length - 1) { - if (((i+1) % 4) == 0) result.append('\n'); - else result.append(' '); - } - } - - return result.toString(); - } - - private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) { - Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount)); - } - - private void handleCompareWithClipboard(Fingerprint fingerprint) { - String clipboardData = Util.readTextFromClipboard(getActivity()); - - if (clipboardData == null) { - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show(); - return; - } - - String numericClipboardData = clipboardData.replaceAll("\\D", ""); - - if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) { - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show(); - return; - } - - if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) { - animateVerifiedSuccess(); - } else { - animateVerifiedFailure(); - } - } - - private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) { - String shareString = - getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" + - getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n"; - - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, shareString); - intent.setType("text/plain"); - - try { - startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via))); - } catch (ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show(); - } - } - - private void setRecipientText(Recipient recipient) { - description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext())))); - description.setMovementMethod(LinkMovementMethod.getInstance()); - } - - private void setFingerprintViews(Fingerprint fingerprint, boolean animate) { - String[] segments = getSegments(fingerprint, codes.length); - - for (int i=0;i() { - public Integer evaluate(float fraction, Integer startValue, Integer endValue) { - return Math.round(startValue + (endValue - startValue) * fraction); - } - }); - - valueAnimator.setDuration(1000); - valueAnimator.start(); - } - - private String[] getSegments(Fingerprint fingerprint, int segmentCount) { - String[] segments = new String[segmentCount]; - String digits = fingerprint.getDisplayableFingerprint().getDisplayText(); - int partSize = digits.length() / segmentCount; - - for (int i=0;i { - try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) { - if (verified) { - Log.i(TAG, "Saving identity: " + recipientId); - ApplicationDependencies.getIdentityStore() - .saveIdentityWithoutSideEffects(recipientId, - remoteIdentity, - VerifiedStatus.VERIFIED, - false, - System.currentTimeMillis(), - true); - } else { - ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT); - } - - ApplicationDependencies.getJobManager() - .add(new MultiDeviceVerifiedUpdateJob(recipientId, - remoteIdentity, - verified ? VerifiedStatus.VERIFIED - : VerifiedStatus.DEFAULT)); - StorageSyncHelper.scheduleSyncForDataChange(); - - IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false); - } - }); - } - } - - - @Override public void onScrollChanged() { - if (scrollView.canScrollVertically(-1)) { - if (toolbarShadow.getVisibility() != View.VISIBLE) { - ViewUtil.fadeIn(toolbarShadow, 250); - } - } else { - if (toolbarShadow.getVisibility() != View.GONE) { - ViewUtil.fadeOut(toolbarShadow, 250); - } - } - - if (scrollView.canScrollVertically(1)) { - if (bottomShadow.getVisibility() != View.VISIBLE) { - ViewUtil.fadeIn(bottomShadow, 250); - } - } else { - ViewUtil.fadeOut(bottomShadow, 250); - } - } - } - - public static class VerifyScanFragment extends Fragment { - - private View container; - private CameraView cameraView; - private ShapeScrim cameraScrim; - private ImageView cameraMarks; - private ScanningThread scanningThread; - private ScanListener scanListener; - - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { - this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment); - this.cameraView = container.findViewById(R.id.scanner); - this.cameraScrim = container.findViewById(R.id.camera_scrim); - this.cameraMarks = container.findViewById(R.id.camera_marks); - - OneShotPreDrawListener.add(cameraScrim, () -> { - int width = cameraScrim.getScrimWidth(); - int height = cameraScrim.getScrimHeight(); - - ViewUtil.updateLayoutParams(cameraMarks, width, height); - }); - - return container; - } - - @Override - public void onResume() { - super.onResume(); - this.scanningThread = new ScanningThread(); - this.scanningThread.setScanListener(scanListener); - this.scanningThread.setCharacterSet("ISO-8859-1"); - this.cameraView.onResume(); - this.cameraView.setPreviewCallback(scanningThread); - this.scanningThread.start(); - } - - @Override - public void onPause() { - super.onPause(); - this.cameraView.onPause(); - this.scanningThread.stopScanning(); - } - - @Override - public void onConfigurationChanged(Configuration newConfiguration) { - super.onConfigurationChanged(newConfiguration); - this.cameraView.onPause(); - this.cameraView.onResume(); - this.cameraView.setPreviewCallback(scanningThread); - } - - public void setScanListener(ScanListener listener) { - if (this.scanningThread != null) scanningThread.setScanListener(listener); - this.scanListener = listener; - } - - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 7e418389d4..de987818a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.MuteDialog import org.thoughtcrime.securesms.PushContactSelectionActivity import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.VerifyIdentityActivity import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.Badges.displayBadges @@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.views.SimpleProgressDialog +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity private const val REQUEST_CODE_VIEW_CONTACT = 1 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 4c5bb917b8..f451b90ee1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -105,7 +105,7 @@ import org.thoughtcrime.securesms.PromptMmsActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.ShortcutLauncherActivity; import org.thoughtcrime.securesms.TransportOption; -import org.thoughtcrime.securesms.VerifyIdentityActivity; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.audio.AudioRecorder; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index d0aa879abe..b662a23c96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -66,13 +66,12 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import org.signal.core.util.ThreadUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.VerifyIdentityActivity; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.components.ConversationScrollToView; import org.thoughtcrime.securesms.components.ConversationTypingView; import org.thoughtcrime.securesms.components.TooltipPopup; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index cb9f90d2b7..dbeb2e310f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -23,7 +23,7 @@ import com.google.common.collect.Sets; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.VerifyIdentityActivity; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.conversation.colors.Colorizer; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart; import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog; @@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.Projection; import org.thoughtcrime.securesms.util.ProjectionList; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -53,7 +52,6 @@ import java.util.Collection; import java.util.Locale; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ExecutionException; public final class ConversationUpdateItem extends FrameLayout diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java index c1141c60be..f2d7a0ec26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java @@ -26,8 +26,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.VerifyIdentityActivity; -import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; diff --git a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java b/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java index 83faae9907..93224ae18c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.qr; +import androidx.annotation.NonNull; + public interface ScanListener { - public void onQrDataFound(String data); + void onQrDataFound(@NonNull String data); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index abf684a5e5..fd2d73420b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -18,10 +18,9 @@ import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.BlockUnblockDialog; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.VerifyIdentityActivity; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity; import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.LiveGroup; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VerifySpan.java b/app/src/main/java/org/thoughtcrime/securesms/util/VerifySpan.java index 0a3f61d323..7f9d2ccada 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VerifySpan.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VerifySpan.java @@ -6,7 +6,7 @@ import android.view.View; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.VerifyIdentityActivity; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.libsignal.IdentityKey; diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java new file mode 100644 index 0000000000..2a89ab4d66 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java @@ -0,0 +1,592 @@ +package org.thoughtcrime.securesms.verify; + +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; +import android.text.Html; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.Animation; +import android.view.animation.AnticipateInterpolator; +import android.view.animation.ScaleAnimation; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.TextSwitcher; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.signal.core.util.ThreadUtil; +import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; +import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; +import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob; +import org.thoughtcrime.securesms.qr.QrCode; +import org.thoughtcrime.securesms.recipients.LiveRecipient; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.IdentityUtil; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.nio.charset.Charset; +import java.util.Locale; + +/** + * Fragment to display a user's identity key. + */ +public class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener { + + private static final String TAG = Log.tag(VerifyDisplayFragment.class); + + private static final String RECIPIENT_ID = "recipient_id"; + private static final String REMOTE_IDENTITY = "remote_identity"; + private static final String LOCAL_IDENTITY = "local_identity"; + private static final String LOCAL_NUMBER = "local_number"; + private static final String VERIFIED_STATE = "verified_state"; + + private LiveRecipient recipient; + private IdentityKey localIdentity; + private IdentityKey remoteIdentity; + private Fingerprint fingerprint; + + private Toolbar toolbar; + private ScrollView scrollView; + private View numbersContainer; + private View loading; + private View qrCodeContainer; + private ImageView qrCode; + private ImageView qrVerified; + private TextSwitcher tapLabel; + private TextView description; + private Callback callback; + private Button verifyButton; + private View toolbarShadow; + private View bottomShadow; + + private TextView[] codes = new TextView[12]; + private boolean animateSuccessOnDraw = false; + private boolean animateFailureOnDraw = false; + private boolean currentVerifiedState = false; + + static VerifyDisplayFragment create(@NonNull RecipientId recipientId, + @NonNull IdentityKeyParcelable remoteIdentity, + @NonNull IdentityKeyParcelable localIdentity, + @NonNull String localNumber, + boolean verifiedState) + { + Bundle extras = new Bundle(); + extras.putParcelable(RECIPIENT_ID, recipientId); + extras.putParcelable(REMOTE_IDENTITY, remoteIdentity); + extras.putParcelable(LOCAL_IDENTITY, localIdentity); + extras.putString(LOCAL_NUMBER, localNumber); + extras.putBoolean(VERIFIED_STATE, verifiedState); + + VerifyDisplayFragment fragment = new VerifyDisplayFragment(); + fragment.setArguments(extras); + + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof Callback) { + callback = (Callback) context; + } else if (getParentFragment() instanceof Callback) { + callback = (Callback) getParentFragment(); + } else { + throw new ClassCastException("Cannot find ScanListener in parent component"); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { + return ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + this.toolbar = view.findViewById(R.id.toolbar); + this.scrollView = view.findViewById(R.id.scroll_view); + this.numbersContainer = view.findViewById(R.id.number_table); + this.loading = view.findViewById(R.id.loading); + this.qrCodeContainer = view.findViewById(R.id.qr_code_container); + this.qrCode = view.findViewById(R.id.qr_code); + this.verifyButton = view.findViewById(R.id.verify_button); + this.qrVerified = view.findViewById(R.id.qr_verified); + this.description = view.findViewById(R.id.description); + this.tapLabel = view.findViewById(R.id.tap_label); + this.toolbarShadow = view.findViewById(R.id.toolbar_shadow); + this.bottomShadow = view.findViewById(R.id.verify_identity_bottom_shadow); + this.codes[0] = view.findViewById(R.id.code_first); + this.codes[1] = view.findViewById(R.id.code_second); + this.codes[2] = view.findViewById(R.id.code_third); + this.codes[3] = view.findViewById(R.id.code_fourth); + this.codes[4] = view.findViewById(R.id.code_fifth); + this.codes[5] = view.findViewById(R.id.code_sixth); + this.codes[6] = view.findViewById(R.id.code_seventh); + this.codes[7] = view.findViewById(R.id.code_eighth); + this.codes[8] = view.findViewById(R.id.code_ninth); + this.codes[9] = view.findViewById(R.id.code_tenth); + this.codes[10] = view.findViewById(R.id.code_eleventh); + this.codes[11] = view.findViewById(R.id.code_twelth); + + this.qrCodeContainer.setOnClickListener(v -> callback.onQrCodeContainerClicked()); + this.registerForContextMenu(numbersContainer); + + updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false); + this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true))); + + this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this); + + toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed()); + toolbar.setOnMenuItemClickListener(this::onToolbarOptionsItemSelected); + toolbar.setTitle(R.string.AndroidManifest__verify_safety_number); + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + initializeFingerprint(); + } + + @Override + public void onDestroyView() { + this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this); + super.onDestroyView(); + } + + private void initializeFingerprint() { + RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID); + IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY); + IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY); + + this.localIdentity = localIdentityParcelable.get(); + this.recipient = Recipient.live(recipientId); + this.remoteIdentity = remoteIdentityParcelable.get(); + + int version; + byte[] localId; + byte[] remoteId; + + //noinspection WrongThread + Recipient resolved = recipient.resolve(); + + if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) { + Log.i(TAG, "Using UUID (version 2)."); + version = 2; + localId = Recipient.self().requireAci().toByteArray(); + remoteId = resolved.requireAci().toByteArray(); + } else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) { + Log.i(TAG, "Using E164 (version 1)."); + version = 1; + localId = Recipient.self().requireE164().getBytes(); + remoteId = resolved.requireE164().getBytes(); + } else { + Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getAci().isPresent(), resolved.getE164().isPresent())); + new MaterialAlertDialogBuilder(requireContext()) + .setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext()))) + .setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish()) + .setOnDismissListener(dialog -> { + requireActivity().finish(); + dialog.dismiss(); + }) + .show(); + return; + } + + this.recipient.observe(this, this::setRecipientText); + + SimpleTask.run(() -> new NumericFingerprintGenerator(5200).createFor(version, + localId, localIdentity, + remoteId, remoteIdentity), + fingerprint -> { + if (getActivity() == null) return; + VerifyDisplayFragment.this.fingerprint = fingerprint; + setFingerprintViews(fingerprint, true); + initializeOptionsMenu(); + }); + } + + @Override + public void onResume() { + super.onResume(); + + setRecipientText(recipient.get()); + + if (fingerprint != null) { + setFingerprintViews(fingerprint, false); + } + + if (animateSuccessOnDraw) { + animateSuccessOnDraw = false; + animateVerifiedSuccess(); + } else if (animateFailureOnDraw) { + animateFailureOnDraw = false; + animateVerifiedFailure(); + } + + ThreadUtil.postToMain(this::onScrollChanged); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, + ContextMenu.ContextMenuInfo menuInfo) + { + super.onCreateContextMenu(menu, view, menuInfo); + + if (fingerprint != null) { + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.verify_display_fragment_context_menu, menu); + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (fingerprint == null) return super.onContextItemSelected(item); + + switch (item.getItemId()) { + case R.id.menu_copy: + handleCopyToClipboard(fingerprint, codes.length); + return true; + case R.id.menu_compare: + handleCompareWithClipboard(fingerprint); + return true; + default: + return super.onContextItemSelected(item); + } + } + + private void initializeOptionsMenu() { + if (fingerprint != null) { + requireActivity().getMenuInflater().inflate(R.menu.verify_identity, toolbar.getMenu()); + } + } + + public boolean onToolbarOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.verify_identity__share) { + handleShare(fingerprint, codes.length); + return true; + } + + return false; + } + + public void setScannedFingerprint(String scanned) { + try { + if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) { + this.animateSuccessOnDraw = true; + } else { + this.animateFailureOnDraw = true; + } + } catch (FingerprintVersionMismatchException e) { + Log.w(TAG, e); + if (e.getOurVersion() < e.getTheirVersion()) { + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show(); + } + this.animateFailureOnDraw = true; + } catch (Exception e) { + Log.w(TAG, e); + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show(); + this.animateFailureOnDraw = true; + } + } + + private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) { + String[] segments = getSegments(fingerprint, segmentCount); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < segments.length; i++) { + result.append(segments[i]); + + if (i != segments.length - 1) { + if (((i + 1) % 4) == 0) result.append('\n'); + else result.append(' '); + } + } + + return result.toString(); + } + + private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) { + Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount)); + } + + private void handleCompareWithClipboard(Fingerprint fingerprint) { + String clipboardData = Util.readTextFromClipboard(getActivity()); + + if (clipboardData == null) { + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show(); + return; + } + + String numericClipboardData = clipboardData.replaceAll("\\D", ""); + + if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) { + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show(); + return; + } + + if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) { + animateVerifiedSuccess(); + } else { + animateVerifiedFailure(); + } + } + + private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) { + String shareString = + getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" + + getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n"; + + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, shareString); + intent.setType("text/plain"); + + try { + startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via))); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show(); + } + } + + private void setRecipientText(Recipient recipient) { + description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext())))); + description.setMovementMethod(LinkMovementMethod.getInstance()); + } + + private void setFingerprintViews(Fingerprint fingerprint, boolean animate) { + String[] segments = getSegments(fingerprint, codes.length); + + for (int i = 0; i < codes.length; i++) { + if (animate) setCodeSegment(codes[i], segments[i]); + else codes[i].setText(segments[i]); + } + + byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized(); + String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1")); + Bitmap qrCodeBitmap = QrCode.create(qrCodeString); + + qrCode.setImageBitmap(qrCodeBitmap); + + if (animate) { + ViewUtil.fadeIn(qrCode, 1000); + ViewUtil.fadeIn(tapLabel, 1000); + ViewUtil.fadeOut(loading, 300, View.GONE); + } else { + qrCode.setVisibility(View.VISIBLE); + tapLabel.setVisibility(View.VISIBLE); + loading.setVisibility(View.GONE); + } + } + + private void setCodeSegment(final TextView codeView, String segment) { + ValueAnimator valueAnimator = new ValueAnimator(); + valueAnimator.setObjectValues(0, Integer.parseInt(segment)); + + valueAnimator.addUpdateListener(animation -> { + int value = (int) animation.getAnimatedValue(); + codeView.setText(String.format(Locale.getDefault(), "%05d", value)); + }); + + valueAnimator.setEvaluator(new TypeEvaluator() { + public Integer evaluate(float fraction, Integer startValue, Integer endValue) { + return Math.round(startValue + (endValue - startValue) * fraction); + } + }); + + valueAnimator.setDuration(1000); + valueAnimator.start(); + } + + private String[] getSegments(Fingerprint fingerprint, int segmentCount) { + String[] segments = new String[segmentCount]; + String digits = fingerprint.getDisplayableFingerprint().getDisplayText(); + int partSize = digits.length() / segmentCount; + + for (int i = 0; i < segmentCount; i++) { + segments[i] = digits.substring(i * partSize, (i * partSize) + partSize); + } + + return segments; + } + + private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Bitmap check = BitmapFactory.decodeResource(getResources(), id); + float offset = (width - check.getWidth()) / 2; + + canvas.drawBitmap(check, offset, offset, null); + + return bitmap; + } + + private void animateVerifiedSuccess() { + Bitmap qrBitmap = ((BitmapDrawable) qrCode.getDrawable()).getBitmap(); + Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp); + + qrVerified.setImageBitmap(qrSuccess); + qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY); + + tapLabel.setText(getString(R.string.verify_display_fragment__successful_match)); + + animateVerified(); + } + + private void animateVerifiedFailure() { + Bitmap qrBitmap = ((BitmapDrawable) qrCode.getDrawable()).getBitmap(); + Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp); + + qrVerified.setImageBitmap(qrSuccess); + qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY); + + tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number)); + + animateVerified(); + } + + private void animateVerified() { + ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1, + ScaleAnimation.RELATIVE_TO_SELF, 0.5f, + ScaleAnimation.RELATIVE_TO_SELF, 0.5f); + scaleAnimation.setInterpolator(new FastOutSlowInInterpolator()); + scaleAnimation.setDuration(800); + scaleAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + qrVerified.postDelayed(() -> { + ScaleAnimation scaleAnimation1 = new ScaleAnimation(1, 0, 1, 0, + ScaleAnimation.RELATIVE_TO_SELF, 0.5f, + ScaleAnimation.RELATIVE_TO_SELF, 0.5f); + + scaleAnimation1.setInterpolator(new AnticipateInterpolator()); + scaleAnimation1.setDuration(500); + ViewUtil.animateOut(qrVerified, scaleAnimation1, View.GONE); + ViewUtil.fadeIn(qrCode, 800); + qrCodeContainer.setEnabled(true); + tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan)); + }, 2000); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + + ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE); + ViewUtil.animateIn(qrVerified, scaleAnimation); + qrCodeContainer.setEnabled(false); + } + + private void updateVerifyButton(boolean verified, boolean update) { + currentVerifiedState = verified; + + if (verified) { + verifyButton.setText(R.string.verify_display_fragment__clear_verification); + } else { + verifyButton.setText(R.string.verify_display_fragment__mark_as_verified); + } + + if (update) { + final RecipientId recipientId = recipient.getId(); + final Context context = requireContext().getApplicationContext(); + + SignalExecutors.BOUNDED.execute(() -> { + try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) { + if (verified) { + Log.i(TAG, "Saving identity: " + recipientId); + ApplicationDependencies.getIdentityStore() + .saveIdentityWithoutSideEffects(recipientId, + remoteIdentity, + IdentityDatabase.VerifiedStatus.VERIFIED, + false, + System.currentTimeMillis(), + true); + } else { + ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, IdentityDatabase.VerifiedStatus.DEFAULT); + } + + ApplicationDependencies.getJobManager() + .add(new MultiDeviceVerifiedUpdateJob(recipientId, + remoteIdentity, + verified ? IdentityDatabase.VerifiedStatus.VERIFIED + : IdentityDatabase.VerifiedStatus.DEFAULT)); + StorageSyncHelper.scheduleSyncForDataChange(); + + IdentityUtil.markIdentityVerified(context, recipient.get(), verified, false); + } + }); + } + } + + + @Override public void onScrollChanged() { + if (scrollView.canScrollVertically(-1)) { + if (toolbarShadow.getVisibility() != View.VISIBLE) { + ViewUtil.fadeIn(toolbarShadow, 250); + } + } else { + if (toolbarShadow.getVisibility() != View.GONE) { + ViewUtil.fadeOut(toolbarShadow, 250); + } + } + + if (scrollView.canScrollVertically(1)) { + if (bottomShadow.getVisibility() != View.VISIBLE) { + ViewUtil.fadeIn(bottomShadow, 250); + } + } else { + ViewUtil.fadeOut(bottomShadow, 250); + } + } + + interface Callback { + void onQrCodeContainerClicked(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityActivity.java b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityActivity.java new file mode 100644 index 0000000000..832b1c1756 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityActivity.java @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.verify; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.PassphraseRequiredActivity; +import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; +import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.database.model.IdentityRecord; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.whispersystems.libsignal.IdentityKey; + +/** + * Activity for verifying identity keys. + * + * @author Moxie Marlinspike + */ +public class VerifyIdentityActivity extends PassphraseRequiredActivity { + + private static final String RECIPIENT_EXTRA = "recipient_id"; + private static final String IDENTITY_EXTRA = "recipient_identity"; + private static final String VERIFIED_EXTRA = "verified_state"; + + private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); + + public static Intent newIntent(@NonNull Context context, + @NonNull IdentityRecord identityRecord) + { + return newIntent(context, + identityRecord.getRecipientId(), + identityRecord.getIdentityKey(), + identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED); + } + + public static Intent newIntent(@NonNull Context context, + @NonNull IdentityRecord identityRecord, + boolean verified) + { + return newIntent(context, + identityRecord.getRecipientId(), + identityRecord.getIdentityKey(), + verified); + } + + public static Intent newIntent(@NonNull Context context, + @NonNull RecipientId recipientId, + @NonNull IdentityKey identityKey, + boolean verified) + { + Intent intent = new Intent(context, VerifyIdentityActivity.class); + + intent.putExtra(RECIPIENT_EXTRA, recipientId); + intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey)); + intent.putExtra(VERIFIED_EXTRA, verified); + + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + super.onCreate(savedInstanceState, ready); + dynamicTheme.onCreate(this); + + VerifyIdentityFragment fragment = VerifyIdentityFragment.create( + getIntent().getParcelableExtra(RECIPIENT_EXTRA), + getIntent().getParcelableExtra(IDENTITY_EXTRA), + getIntent().getBooleanExtra(VERIFIED_EXTRA, false) + ); + + getSupportFragmentManager().beginTransaction() + .replace(android.R.id.content, fragment) + .commitAllowingStateLoss(); + } + + @Override + protected void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt new file mode 100644 index 0000000000..daa1920c5b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -0,0 +1,98 @@ +package org.thoughtcrime.securesms.verify + +import android.Manifest +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import org.signal.core.util.ThreadUtil +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.qr.ScanListener +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.ServiceUtil + +/** + * Fragment to assist user in verifying recipient identity utilizing keys. + */ +class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListener, VerifyDisplayFragment.Callback { + + companion object { + private const val EXTRA_RECIPIENT = "extra.recipient.id" + private const val EXTRA_IDENTITY = "extra.recipient.identity" + private const val EXTRA_VERIFIED = "extra.verified.state" + + @JvmStatic + fun create( + recipientId: RecipientId, + remoteIdentity: IdentityKeyParcelable, + verified: Boolean + ): VerifyIdentityFragment { + return VerifyIdentityFragment().apply { + arguments = Bundle().apply { + putParcelable(EXTRA_RECIPIENT, recipientId) + putParcelable(EXTRA_IDENTITY, remoteIdentity) + putBoolean(EXTRA_VERIFIED, verified) + } + } + } + } + + private val displayFragment by lazy { + VerifyDisplayFragment.create( + recipientId, + remoteIdentity, + IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(requireContext())), + Recipient.self().requireE164(), + isVerified + ) + } + + private val scanFragment = VerifyScanFragment() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + childFragmentManager.beginTransaction() + .replace(R.id.fragment_container, displayFragment) + .commitAllowingStateLoss() + } + + private val recipientId: RecipientId + get() = requireArguments().getParcelable(EXTRA_RECIPIENT)!! + + private val remoteIdentity: IdentityKeyParcelable + get() = requireArguments().getParcelable(EXTRA_IDENTITY)!! + + private val isVerified: Boolean + get() = requireArguments().getBoolean(EXTRA_VERIFIED) + + override fun onQrDataFound(data: String) { + ThreadUtil.runOnMain { + ServiceUtil.getVibrator(context).vibrate(50) + childFragmentManager.popBackStack() + displayFragment.setScannedFingerprint(data) + } + } + + override fun onQrCodeContainerClicked() { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied)) + .onAllGranted { + childFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom, R.anim.slide_from_bottom, R.anim.slide_to_top) + .replace(R.id.fragment_container, scanFragment) + .addToBackStack(null) + .commitAllowingStateLoss() + } + .onAnyDenied { Toast.makeText(requireContext(), R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show() } + .execute() + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt new file mode 100644 index 0000000000..9c70dbd461 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.verify + +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.view.OneShotPreDrawListener +import androidx.fragment.app.Fragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ShapeScrim +import org.thoughtcrime.securesms.components.camera.CameraView +import org.thoughtcrime.securesms.keyboard.findListener +import org.thoughtcrime.securesms.qr.ScanListener +import org.thoughtcrime.securesms.qr.ScanningThread +import org.thoughtcrime.securesms.util.ViewUtil + +/** + * QR Scanner for identity verification + */ +class VerifyScanFragment : Fragment() { + private lateinit var cameraView: CameraView + private lateinit var cameraScrim: ShapeScrim + private lateinit var cameraMarks: ImageView + private lateinit var scanningThread: ScanningThread + private lateinit var scanListener: ScanListener + + override fun onAttach(context: Context) { + super.onAttach(context) + scanListener = findListener()!! + } + + override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { + return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_scan_fragment) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cameraView = view.findViewById(R.id.scanner) + cameraScrim = view.findViewById(R.id.camera_scrim) + cameraMarks = view.findViewById(R.id.camera_marks) + OneShotPreDrawListener.add(cameraScrim) { + val width = cameraScrim.scrimWidth + val height = cameraScrim.scrimHeight + ViewUtil.updateLayoutParams(cameraMarks, width, height) + } + } + + override fun onResume() { + super.onResume() + scanningThread = ScanningThread() + scanningThread.setScanListener(scanListener) + scanningThread.setCharacterSet("ISO-8859-1") + cameraView.onResume() + cameraView.setPreviewCallback(scanningThread) + scanningThread.start() + } + + override fun onPause() { + super.onPause() + cameraView.onPause() + scanningThread.stopScanning() + } + + override fun onConfigurationChanged(newConfiguration: Configuration) { + super.onConfigurationChanged(newConfiguration) + cameraView.onPause() + cameraView.onResume() + cameraView.setPreviewCallback(scanningThread) + } +}