diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f663d0f592..2703174659 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -262,13 +262,6 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
-
-
-
-
diff --git a/res/drawable-hdpi/ic_check_white_48dp.png b/res/drawable-hdpi/ic_check_white_48dp.png
new file mode 100644
index 0000000000..2c2ad771f7
Binary files /dev/null and b/res/drawable-hdpi/ic_check_white_48dp.png differ
diff --git a/res/drawable-hdpi/ic_close_white_48dp.png b/res/drawable-hdpi/ic_close_white_48dp.png
new file mode 100644
index 0000000000..6b717e0dda
Binary files /dev/null and b/res/drawable-hdpi/ic_close_white_48dp.png differ
diff --git a/res/drawable-mdpi/ic_check_white_48dp.png b/res/drawable-mdpi/ic_check_white_48dp.png
new file mode 100644
index 0000000000..3b2b65d262
Binary files /dev/null and b/res/drawable-mdpi/ic_check_white_48dp.png differ
diff --git a/res/drawable-mdpi/ic_close_white_48dp.png b/res/drawable-mdpi/ic_close_white_48dp.png
new file mode 100644
index 0000000000..b7c7ffd0e7
Binary files /dev/null and b/res/drawable-mdpi/ic_close_white_48dp.png differ
diff --git a/res/drawable-xhdpi/ic_check_white_48dp.png b/res/drawable-xhdpi/ic_check_white_48dp.png
new file mode 100644
index 0000000000..d670618c7e
Binary files /dev/null and b/res/drawable-xhdpi/ic_check_white_48dp.png differ
diff --git a/res/drawable-xhdpi/ic_close_white_48dp.png b/res/drawable-xhdpi/ic_close_white_48dp.png
new file mode 100644
index 0000000000..3964192192
Binary files /dev/null and b/res/drawable-xhdpi/ic_close_white_48dp.png differ
diff --git a/res/drawable-xxhdpi/ic_check_white_48dp.png b/res/drawable-xxhdpi/ic_check_white_48dp.png
new file mode 100644
index 0000000000..bfd7b82aaa
Binary files /dev/null and b/res/drawable-xxhdpi/ic_check_white_48dp.png differ
diff --git a/res/drawable-xxhdpi/ic_close_white_48dp.png b/res/drawable-xxhdpi/ic_close_white_48dp.png
new file mode 100644
index 0000000000..4927bc242e
Binary files /dev/null and b/res/drawable-xxhdpi/ic_close_white_48dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_check_white_48dp.png b/res/drawable-xxxhdpi/ic_check_white_48dp.png
new file mode 100644
index 0000000000..23a197082a
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_check_white_48dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_close_white_48dp.png b/res/drawable-xxxhdpi/ic_close_white_48dp.png
new file mode 100644
index 0000000000..1ab2312754
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_close_white_48dp.png differ
diff --git a/res/drawable/qr_code_background.xml b/res/drawable/qr_code_background.xml
new file mode 100644
index 0000000000..a5266a706e
--- /dev/null
+++ b/res/drawable/qr_code_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/verify_display_fragment.xml b/res/layout/verify_display_fragment.xml
new file mode 100644
index 0000000000..633756b39d
--- /dev/null
+++ b/res/layout/verify_display_fragment.xml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/verify_identity_activity.xml b/res/layout/verify_identity_activity.xml
deleted file mode 100644
index 78146b758c..0000000000
--- a/res/layout/verify_identity_activity.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/layout/verify_scan_fragment.xml b/res/layout/verify_scan_fragment.xml
new file mode 100644
index 0000000000..d379c60023
--- /dev/null
+++ b/res/layout/verify_scan_fragment.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/view_identity_activity.xml b/res/layout/view_identity_activity.xml
deleted file mode 100644
index 1bc52c3ac2..0000000000
--- a/res/layout/view_identity_activity.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e6780cdfeb..907033bdeb 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -129,6 +129,8 @@
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e5023275ae..3d277d300c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -554,16 +554,10 @@
Disappearing message time set to %s
- You do not have an identity key.
- Recipient has no identity key.
- Recipient has no identity key!
- Scan contact\'s QR code
- Display your QR code
- WARNING, the scanned key DOES NOT match! Please check the fingerprint text carefully.
- NOT Verified!
- Their key is correct. It is also necessary to verify your key with them as well.
- Verified!
- You don\'t have an identity key!
+ Your contact is running an old version of Signal, please ask them to update before verifying security numbers.
+ You\'re attempting to verify security numbers with %1$s, but scanned %2$s instead.
+ The scanned QR code is not a correctly formatted security number verification code. Please try scanning again.
+
You do not have an identity key.
@@ -935,10 +929,10 @@
Enter a name or number
Add member
-
- Their identity (they read):
- Your identity (you read):
-
+
+ Scan the code on your contact\'s phone, or ask them to scan your code, to verify that your messages are end-to-end encrypted. You can alternately compare the number above.
+ Tap to scan
+
Some issues need your attention.
Sent
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 029e17535a..3b466a9c72 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -227,5 +227,10 @@
- @null
+
diff --git a/res/values/themes.xml b/res/values/themes.xml
index d816fcae1b..6f02a51683 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -127,6 +127,8 @@
- @color/gray65
- @color/black
+ - @color/gray5
+
- @color/gray12
- #66555555
- #44555555
@@ -235,6 +237,8 @@
- #BFffffff
- @drawable/conversation_item_sent_indicator_text_shape_dark
+ - #ff333333
+
- @drawable/ic_info_outline_dark
- @drawable/ic_warning_dark
diff --git a/res/xml/recipient_preferences.xml b/res/xml/recipient_preferences.xml
index ee9e38ff14..5185e1fcc1 100644
--- a/res/xml/recipient_preferences.xml
+++ b/res/xml/recipient_preferences.xml
@@ -38,7 +38,8 @@
+ android:persistent="false"
+ android:enabled="false"/>
= Build.VERSION_CODES.LOLLIPOP) {
diff --git a/src/org/thoughtcrime/securesms/DeviceAddFragment.java b/src/org/thoughtcrime/securesms/DeviceAddFragment.java
index a4de3916b2..4090e82bbb 100644
--- a/src/org/thoughtcrime/securesms/DeviceAddFragment.java
+++ b/src/org/thoughtcrime/securesms/DeviceAddFragment.java
@@ -31,20 +31,17 @@ import com.google.zxing.qrcode.QRCodeReader;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.components.camera.CameraView.PreviewCallback;
import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame;
+import org.thoughtcrime.securesms.qr.ScanListener;
+import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
-public class DeviceAddFragment extends Fragment implements PreviewCallback {
-
- private static final String TAG = DeviceAddFragment.class.getSimpleName();
-
- private final QRCodeReader reader = new QRCodeReader();
+public class DeviceAddFragment extends Fragment {
private ViewGroup container;
private LinearLayout overlay;
private ImageView devicesImage;
private CameraView scannerView;
- private PreviewFrame previewFrame;
private ScanningThread scanningThread;
private ScanListener scanListener;
@@ -54,8 +51,6 @@ public class DeviceAddFragment extends Fragment implements PreviewCallback {
this.overlay = ViewUtil.findById(this.container, R.id.overlay);
this.scannerView = ViewUtil.findById(this.container, R.id.scanner);
this.devicesImage = ViewUtil.findById(this.container, R.id.devices);
- this.scannerView.onResume();
- this.scannerView.setPreviewCallback(this);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
@@ -86,10 +81,10 @@ public class DeviceAddFragment extends Fragment implements PreviewCallback {
@Override
public void onResume() {
super.onResume();
- this.scannerView.onResume();
- this.scannerView.setPreviewCallback(this);
- this.previewFrame = null;
this.scanningThread = new ScanningThread();
+ this.scanningThread.setScanListener(scanListener);
+ this.scannerView.onResume();
+ this.scannerView.setPreviewCallback(scanningThread);
this.scanningThread.start();
}
@@ -113,24 +108,9 @@ public class DeviceAddFragment extends Fragment implements PreviewCallback {
}
this.scannerView.onResume();
- this.scannerView.setPreviewCallback(this);
+ this.scannerView.setPreviewCallback(scanningThread);
}
- @Override
- public void onPreviewFrame(@NonNull PreviewFrame previewFrame) {
- Context context = getActivity();
-
- try {
- if (context != null) {
- synchronized (this) {
- this.previewFrame = previewFrame;
- this.notify();
- }
- }
- } catch (RuntimeException e) {
- Log.w(TAG, e);
- }
- }
public ImageView getDevicesImage() {
return devicesImage;
@@ -138,83 +118,11 @@ public class DeviceAddFragment extends Fragment implements PreviewCallback {
public void setScanListener(ScanListener scanListener) {
this.scanListener = scanListener;
- }
- private class ScanningThread extends Thread {
-
- private boolean scanning = true;
-
- @Override
- public void run() {
- while (true) {
- PreviewFrame ourFrame;
-
- synchronized (DeviceAddFragment.this) {
- while (scanning && previewFrame == null) {
- Util.wait(DeviceAddFragment.this, 0);
- }
-
- if (!scanning) return;
- else ourFrame = previewFrame;
-
- previewFrame = null;
- }
-
- String url = getUrl(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation());
-
- if (url != null && scanListener != null) {
- Uri uri = Uri.parse(url);
- scanListener.onUrlFound(uri);
- return;
- }
- }
- }
-
- public void stopScanning() {
- synchronized (DeviceAddFragment.this) {
- scanning = false;
- DeviceAddFragment.this.notify();
- }
- }
-
- private @Nullable String getUrl(byte[] data, int width, int height, int orientation) {
- try {
- if (orientation == Configuration.ORIENTATION_PORTRAIT) {
- byte[] rotatedData = new byte[data.length];
-
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- rotatedData[x * height + height - y - 1] = data[x + y * width];
- }
- }
-
- int tmp = width;
- width = height;
- height = tmp;
- data = rotatedData;
- }
-
- PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height,
- 0, 0, width, height,
- false);
-
- BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
-
- Result result = reader.decode(bitmap);
-
- if (result != null) return result.getText();
-
- } catch (NullPointerException | ChecksumException | FormatException e) {
- Log.w(TAG, e);
- } catch (NotFoundException e) {
- // Thanks ZXing...
- }
-
- return null;
+ if (this.scanningThread != null) {
+ this.scanningThread.setScanListener(scanListener);
}
}
- public interface ScanListener {
- public void onUrlFound(Uri uri);
- }
+
}
diff --git a/src/org/thoughtcrime/securesms/KeyScanningActivity.java b/src/org/thoughtcrime/securesms/KeyScanningActivity.java
deleted file mode 100644
index 234e558298..0000000000
--- a/src/org/thoughtcrime/securesms/KeyScanningActivity.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * Copyright (C) 2011 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.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.widget.Toast;
-
-import com.google.zxing.integration.android.IntentIntegrator;
-import com.google.zxing.integration.android.IntentResult;
-
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.util.Base64;
-import org.thoughtcrime.securesms.util.Dialogs;
-import org.thoughtcrime.securesms.util.DynamicLanguage;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-import org.whispersystems.libsignal.IdentityKey;
-
-/**
- * Activity for initiating/receiving key QR code scans.
- *
- * @author Moxie Marlinspike
- */
-public abstract class KeyScanningActivity extends PassphraseRequiredActionBarActivity {
-
- private final DynamicTheme dynamicTheme = new DynamicTheme();
- private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
-
- @Override
- protected void onPreCreate() {
- dynamicTheme.onCreate(this);
- dynamicLanguage.onCreate(this);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- dynamicLanguage.onResume(this);
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
-
- MenuInflater inflater = this.getMenuInflater();
- menu.clear();
-
- inflater.inflate(R.menu.key_scanning, menu);
-
- menu.findItem(R.id.menu_scan).setTitle(getScanString());
- menu.findItem(R.id.menu_get_scanned).setTitle(getDisplayString());
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- super.onOptionsItemSelected(item);
-
- switch (item.getItemId()) {
- case R.id.menu_scan: initiateScan(); return true;
- case R.id.menu_get_scanned: initiateDisplay(); return true;
- case android.R.id.home: finish(); return true;
- }
-
- return false;
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent intent) {
- IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
-
- if ((scanResult != null) && (scanResult.getContents() != null)) {
- String data = scanResult.getContents();
-
- if (data.equals(Base64.encodeBytes(getIdentityKeyToCompare().serialize()))) {
- Dialogs.showInfoDialog(this, getVerifiedTitle(), getVerifiedMessage());
- } else {
- Dialogs.showAlertDialog(this, getNotVerifiedTitle(), getNotVerifiedMessage());
- }
- } else {
- Toast.makeText(this, R.string.KeyScanningActivity_no_scanned_key_found_exclamation,
- Toast.LENGTH_LONG).show();
- }
- }
-
- private IntentIntegrator getIntentIntegrator() {
- IntentIntegrator intentIntegrator = new IntentIntegrator(this);
- intentIntegrator.setButtonYesByID(R.string.yes);
- intentIntegrator.setButtonNoByID(R.string.no);
- intentIntegrator.setTitleByID(R.string.KeyScanningActivity_install_barcode_Scanner);
- intentIntegrator.setMessageByID(R.string.KeyScanningActivity_this_application_requires_barcode_scanner_would_you_like_to_install_it);
- return intentIntegrator;
- }
-
- protected void initiateScan() {
- IntentIntegrator intentIntegrator = getIntentIntegrator();
- intentIntegrator.initiateScan();
- }
-
- protected void initiateDisplay() {
- IntentIntegrator intentIntegrator = getIntentIntegrator();
- intentIntegrator.shareText(Base64.encodeBytes(getIdentityKeyToDisplay().serialize()));
- }
-
- protected abstract String getScanString();
- protected abstract String getDisplayString();
-
- protected abstract String getNotVerifiedTitle();
- protected abstract String getNotVerifiedMessage();
-
- protected abstract IdentityKey getIdentityKeyToCompare();
- protected abstract IdentityKey getIdentityKeyToDisplay();
-
- protected abstract String getVerifiedTitle();
- protected abstract String getVerifiedMessage();
-
-}
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index 863df016ae..53a948e393 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -29,7 +29,9 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
@@ -37,11 +39,22 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference;
import org.thoughtcrime.securesms.preferences.ColorPreference;
+import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
+import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.state.SessionRecord;
+import org.whispersystems.libsignal.state.SessionStore;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.util.concurrent.ExecutionException;
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements Recipients.RecipientsModifiedListener
{
@@ -179,8 +192,9 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final Handler handler = new Handler();
- private Recipients recipients;
+ private Recipients recipients;
private BroadcastReceiver staleReceiver;
+ private MasterSecret masterSecret;
@Override
public void onCreate(Bundle icicle) {
@@ -189,6 +203,8 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
addPreferencesFromResource(R.xml.recipient_preferences);
initializeRecipients();
+ this.masterSecret = getArguments().getParcelable("master_secret");
+
this.findPreference(PREFERENCE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener());
this.findPreference(PREFERENCE_VIBRATE)
@@ -199,8 +215,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
.setOnPreferenceClickListener(new BlockClickedListener());
this.findPreference(PREFERENCE_COLOR)
.setOnPreferenceChangeListener(new ColorChangeListener());
- this.findPreference(PREFERENCE_IDENTITY)
- .setOnPreferenceClickListener(new IdentityClickedListener());
}
@Override
@@ -245,7 +259,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
ListPreference vibratePreference = (ListPreference) this.findPreference(PREFERENCE_VIBRATE);
ColorPreference colorPreference = (ColorPreference) this.findPreference(PREFERENCE_COLOR);
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
- Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
+ final Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
mutePreference.setChecked(recipients.isMuted());
@@ -281,6 +295,23 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
if (recipients.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
+
+ getRemoteIdentityKey(getActivity(), masterSecret, recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() {
+ @Override
+ public void onSuccess(Optional result) {
+ if (result.isPresent()) {
+ identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
+ identityPreference.setEnabled(true);
+ } else {
+ getPreferenceScreen().removePreference(identityPreference);
+ }
+ }
+
+ @Override
+ public void onFailure(ExecutionException e) {
+ getPreferenceScreen().removePreference(identityPreference);
+ }
+ });
}
}
@@ -294,6 +325,36 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
});
}
+ private ListenableFuture> getRemoteIdentityKey(final Context context,
+ final MasterSecret masterSecret,
+ final Recipient recipient)
+ {
+ final SettableFuture> future = new SettableFuture<>();
+
+ new AsyncTask>() {
+ @Override
+ protected Optional doInBackground(Recipient... recipient) {
+ SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
+ SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(recipient[0].getNumber(), SignalServiceAddress.DEFAULT_DEVICE_ID);
+ SessionRecord record = sessionStore.loadSession(axolotlAddress);
+
+ if (record == null) {
+ return Optional.absent();
+ }
+
+ return Optional.fromNullable(record.getSessionState().getRemoteIdentityKey());
+ }
+
+ @Override
+ protected void onPostExecute(Optional result) {
+ future.set(result);
+ }
+ }.execute(recipient);
+
+ return future;
+ }
+
+
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
@@ -413,10 +474,18 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
}
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
+
+ private final IdentityKey identityKey;
+
+ private IdentityClickedListener(IdentityKey identityKey) {
+ this.identityKey = identityKey;
+ }
+
@Override
public boolean onPreferenceClick(Preference preference) {
Intent verifyIdentityIntent = new Intent(getActivity(), VerifyIdentityActivity.class);
- verifyIdentityIntent.putExtra("recipient", recipients.getPrimaryRecipient().getRecipientId());
+ verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipients.getPrimaryRecipient().getRecipientId());
+ verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
startActivity(verifyIdentityIntent);
return true;
diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
index 7bc3fe22e0..935d02d4da 100644
--- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
+++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
@@ -1,6 +1,5 @@
/**
- * Copyright (C) 2011 Whisper Systems
- * Copyright (C) 2013 Open Whisper Systems
+ * Copyright (C) 2016 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
@@ -17,155 +16,360 @@
*/
package org.thoughtcrime.securesms;
+import android.content.Context;
+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.Bundle;
+import android.os.Vibrator;
+import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnticipateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
+import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
+import org.thoughtcrime.securesms.qr.QrCode;
+import org.thoughtcrime.securesms.qr.ScanListener;
+import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
-import org.thoughtcrime.securesms.util.Hex;
-import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+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.state.SessionRecord;
-import org.whispersystems.libsignal.state.SessionStore;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.libsignal.fingerprint.Fingerprint;
+import org.whispersystems.libsignal.fingerprint.FingerprintIdentifierMismatchException;
+import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
+import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
+import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
/**
* Activity for verifying identity keys.
*
* @author Moxie Marlinspike
*/
-public class VerifyIdentityActivity extends KeyScanningActivity {
+public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
- private Recipient recipient;
- private MasterSecret masterSecret;
+ private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
- private TextView localIdentityFingerprint;
- private TextView remoteIdentityFingerprint;
+ public static final String RECIPIENT_ID = "recipient_id";
+ public static final String RECIPIENT_IDENTITY = "recipient_identity";
+
+ private final DynamicTheme dynamicTheme = new DynamicTheme();
+ private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ private VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
+ private VerifyScanFragment scanFragment = new VerifyScanFragment();
+
+ @Override
+ public void onPreCreate() {
+ dynamicTheme.onCreate(this);
+ dynamicLanguage.onCreate(this);
+ }
@Override
protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) {
- this.masterSecret = masterSecret;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_identity);
- setContentView(R.layout.verify_identity_activity);
+ Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID, -1), true);
- this.localIdentityFingerprint = (TextView)findViewById(R.id.you_read);
- this.remoteIdentityFingerprint = (TextView)findViewById(R.id.friend_reads);
+ Bundle extras = new Bundle();
+ extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(RECIPIENT_IDENTITY));
+ extras.putString(VerifyDisplayFragment.REMOTE_NUMBER, recipient.getNumber());
+ extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
+ extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
+
+ scanFragment.setScanListener(this);
+ displayFragment.setClickListener(this);
+
+ initFragment(android.R.id.content, displayFragment, masterSecret, dynamicLanguage.getCurrentLocale(), extras);
}
@Override
- public void onResume() {
- super.onResume();
-
- this.recipient = RecipientFactory.getRecipientForId(this, this.getIntent().getLongExtra("recipient", -1), true);
-
- initializeFingerprints();
- }
-
- private void initializeFingerprints() {
- if (!IdentityKeyUtil.hasIdentityKey(this)) {
- localIdentityFingerprint.setText(R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key);
- return;
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home: finish(); return true;
}
- localIdentityFingerprint.setText(Hex.toString(IdentityKeyUtil.getIdentityKey(this).serialize()));
+ return false;
+ }
- IdentityKey identityKey = getRemoteIdentityKey(masterSecret, recipient);
+ @Override
+ public void onQrDataFound(final String data) {
+ Util.runOnMain(new Runnable() {
+ @Override
+ public void run() {
+ ((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
- if (identityKey == null) {
- remoteIdentityFingerprint.setText(R.string.VerifyIdentityActivity_recipient_has_no_identity_key);
- } else {
- remoteIdentityFingerprint.setText(Hex.toString(identityKey.serialize()));
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ transaction.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_top);
+
+ displayFragment.setScannedFingerprint(data);
+ transaction.replace(android.R.id.content, displayFragment)
+ .commit();
+ }
+ });
+ }
+
+ @Override
+ public void onClick(View v) {
+ 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)
+ .commit();
+ }
+
+ public static class VerifyDisplayFragment extends Fragment {
+
+ 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";
+
+ private String localNumber;
+ private String remoteNumber;
+
+ private IdentityKey localIdentity;
+ private IdentityKey remoteIdentity;
+
+ private Fingerprint fingerprint;
+
+ private View container;
+ private ImageView qrCode;
+ private ImageView qrVerified;
+ private View.OnClickListener clickListener;
+
+ private TextView[] codes = new TextView[12];
+ private boolean animateSuccessOnDraw = false;
+ private boolean animateFailureOnDraw = false;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
+ this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
+ this.qrCode = ViewUtil.findById(container, R.id.qr_code);
+ this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
+ this.codes[0] = ViewUtil.findById(container, R.id.code_first);
+ this.codes[1] = ViewUtil.findById(container, R.id.code_second);
+ this.codes[2] = ViewUtil.findById(container, R.id.code_third);
+ this.codes[3] = ViewUtil.findById(container, R.id.code_fourth);
+ this.codes[4] = ViewUtil.findById(container, R.id.code_fifth);
+ this.codes[5] = ViewUtil.findById(container, R.id.code_sixth);
+ this.codes[6] = ViewUtil.findById(container, R.id.code_seventh);
+ this.codes[7] = ViewUtil.findById(container, R.id.code_eighth);
+ this.codes[8] = ViewUtil.findById(container, R.id.code_ninth);
+ this.codes[9] = ViewUtil.findById(container, R.id.code_tenth);
+ this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh);
+ this.codes[11] = ViewUtil.findById(container, R.id.code_twelth);
+
+ this.qrCode.setOnClickListener(clickListener);
+
+ return container;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ this.localNumber = getArguments().getString(LOCAL_NUMBER);
+ this.localIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(LOCAL_IDENTITY)).get();
+ this.remoteNumber = getArguments().getString(REMOTE_NUMBER);
+ this.remoteIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(REMOTE_IDENTITY)).get();
+ this.fingerprint = new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
+ remoteNumber, remoteIdentity);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ setFingerprintViews(fingerprint);
+
+ if (animateSuccessOnDraw) {
+ animateSuccessOnDraw = false;
+ animateVerifiedSuccess();
+ } else if (animateFailureOnDraw) {
+ animateFailureOnDraw = false;
+ animateVerifiedFailure();;
+ }
+ }
+
+ 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);
+ Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
+ } catch (FingerprintIdentifierMismatchException e) {
+ Log.w(TAG, e);
+ Toast.makeText(getActivity(), getActivity().getString(R.string.VerifyIdentityActivity_you_re_attempting_to_verify_security_numbers_with, e.getRemoteIdentifier(), e.getScannedRemoteIdentifier()), Toast.LENGTH_LONG).show();
+ } catch (FingerprintParsingException e) {
+ Log.w(TAG, e);
+ Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_security_number, Toast.LENGTH_LONG).show();
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void setClickListener(View.OnClickListener listener) {
+ this.clickListener = listener;
+ }
+
+ private void setFingerprintViews(Fingerprint fingerprint) {
+ String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
+ int partSize = digits.length() / codes.length;
+
+ for (int i=0;i.
- */
-package org.thoughtcrime.securesms;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.widget.TextView;
-
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.util.Hex;
-import org.whispersystems.libsignal.IdentityKey;
-import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
-
-/**
- * Activity for displaying an identity key.
- *
- * @author Moxie Marlinspike
- */
-public class ViewIdentityActivity extends KeyScanningActivity {
-
- public static final String IDENTITY_KEY = "identity_key";
- public static final String TITLE = "title";
-
- private TextView identityFingerprint;
- private IdentityKey identityKey;
-
- @Override
- protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- setContentView(R.layout.view_identity_activity);
-
- initialize();
- }
-
- protected void initialize() {
- initializeResources();
- initializeFingerprint();
- }
-
- private void initializeFingerprint() {
- if (identityKey == null) {
- identityFingerprint.setText(R.string.ViewIdentityActivity_you_do_not_have_an_identity_key);
- } else {
- identityFingerprint.setText(Hex.toString(identityKey.serialize()));
- }
- }
-
- private void initializeResources() {
- IdentityKeyParcelable identityKeyParcelable = getIntent().getParcelableExtra(IDENTITY_KEY);
-
- if (identityKeyParcelable == null) {
- throw new AssertionError("No identity key!");
- }
-
- this.identityKey = identityKeyParcelable.get();
- this.identityFingerprint = (TextView)findViewById(R.id.identity_fingerprint);
- String title = getIntent().getStringExtra(TITLE);
-
- if (title != null) {
- getSupportActionBar().setTitle(getIntent().getStringExtra(TITLE));
- }
- }
-
- @Override
- protected String getScanString() {
- return getString(R.string.ViewIdentityActivity_scan_contacts_qr_code);
- }
-
- @Override
- protected String getDisplayString() {
- return getString(R.string.ViewIdentityActivity_display_your_qr_code);
- }
-
- @Override
- protected IdentityKey getIdentityKeyToCompare() {
- return identityKey;
- }
-
- @Override
- protected IdentityKey getIdentityKeyToDisplay() {
- return identityKey;
- }
-
- @Override
- protected String getNotVerifiedMessage() {
- return getString(R.string.ViewIdentityActivity_warning_the_scanned_key_does_not_match_exclamation);
- }
-
- @Override
- protected String getNotVerifiedTitle() {
- return getString(R.string.ViewIdentityActivity_not_verified_exclamation);
- }
-
- @Override
- protected String getVerifiedMessage() {
- return getString(R.string.ViewIdentityActivity_the_scanned_key_matches_exclamation);
- }
-
- @Override
- protected String getVerifiedTitle() {
- return getString(R.string.ViewIdentityActivity_verified_exclamation);
- }
-}
diff --git a/src/org/thoughtcrime/securesms/ViewLocalIdentityActivity.java b/src/org/thoughtcrime/securesms/ViewLocalIdentityActivity.java
deleted file mode 100644
index 676f56124d..0000000000
--- a/src/org/thoughtcrime/securesms/ViewLocalIdentityActivity.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2011 Whisper Systems
- * Copyright (C) 2013 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.os.Bundle;
-import android.support.annotation.NonNull;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-
-import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
-import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-
-/**
- * Activity that displays the local identity key and offers the option to regenerate it.
- *
- * @author Moxie Marlinspike
- */
-public class ViewLocalIdentityActivity extends ViewIdentityActivity {
-
- @Override
- protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) {
- getIntent().putExtra(ViewIdentityActivity.IDENTITY_KEY,
- new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
- getIntent().putExtra(ViewIdentityActivity.TITLE,
- getString(R.string.ViewIdentityActivity_your_identity_fingerprint));
- super.onCreate(icicle, masterSecret);
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
-
- MenuInflater inflater = this.getMenuInflater();
- inflater.inflate(R.menu.local_identity, menu);
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- super.onOptionsItemSelected(item);
-
- switch (item.getItemId()) {
- case android.R.id.home:finish(); return true;
- }
-
- return false;
- }
-}
diff --git a/src/org/thoughtcrime/securesms/components/SquareImageView.java b/src/org/thoughtcrime/securesms/components/SquareImageView.java
new file mode 100644
index 0000000000..57ba997203
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/SquareImageView.java
@@ -0,0 +1,31 @@
+package org.thoughtcrime.securesms.components;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class SquareImageView extends ImageView {
+ public SquareImageView(Context context) {
+ super(context);
+ }
+
+ public SquareImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/qr/QrCode.java b/src/org/thoughtcrime/securesms/qr/QrCode.java
new file mode 100644
index 0000000000..e6d2b5dc11
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/qr/QrCode.java
@@ -0,0 +1,37 @@
+package org.thoughtcrime.securesms.qr;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+
+public class QrCode {
+
+ public static final String TAG = QrCode.class.getSimpleName();
+
+ public static @NonNull Bitmap create(String data) {
+ try {
+ BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, 512, 512);
+ Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
+
+ for (int y = 0; y < result.getHeight(); y++) {
+ for (int x = 0; x < result.getWidth(); x++) {
+ if (result.get(x, y)) {
+ bitmap.setPixel(x, y, Color.BLACK);
+ }
+ }
+ }
+
+ return bitmap;
+ } catch (WriterException e) {
+ Log.w(TAG, e);
+ return Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888);
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/qr/ScanListener.java b/src/org/thoughtcrime/securesms/qr/ScanListener.java
new file mode 100644
index 0000000000..83faae9907
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/qr/ScanListener.java
@@ -0,0 +1,5 @@
+package org.thoughtcrime.securesms.qr;
+
+public interface ScanListener {
+ public void onQrDataFound(String data);
+}
diff --git a/src/org/thoughtcrime/securesms/qr/ScanningThread.java b/src/org/thoughtcrime/securesms/qr/ScanningThread.java
new file mode 100644
index 0000000000..f37374e296
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/qr/ScanningThread.java
@@ -0,0 +1,125 @@
+package org.thoughtcrime.securesms.qr;
+
+import android.content.res.Configuration;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.ChecksumException;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.FormatException;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.QRCodeReader;
+
+import org.thoughtcrime.securesms.components.camera.CameraView;
+import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class ScanningThread extends Thread implements CameraView.PreviewCallback {
+
+ private static final String TAG = ScanningThread.class.getSimpleName();
+
+ private final QRCodeReader reader = new QRCodeReader();
+ private final AtomicReference scanListener = new AtomicReference<>();
+ private final Map hints = new HashMap<>();
+
+ private boolean scanning = true;
+ private PreviewFrame previewFrame;
+
+ public void setCharacterSet(String characterSet) {
+ hints.put(DecodeHintType.CHARACTER_SET, characterSet);
+ }
+
+ public void setScanListener(ScanListener scanListener) {
+ this.scanListener.set(scanListener);
+ }
+
+ @Override
+ public void onPreviewFrame(@NonNull PreviewFrame previewFrame) {
+ try {
+ synchronized (this) {
+ this.previewFrame = previewFrame;
+ this.notify();
+ }
+ } catch (RuntimeException e) {
+ Log.w(TAG, e);
+ }
+ }
+
+
+ @Override
+ public void run() {
+ while (true) {
+ PreviewFrame ourFrame;
+
+ synchronized (this) {
+ while (scanning && previewFrame == null) {
+ Util.wait(this, 0);
+ }
+
+ if (!scanning) return;
+ else ourFrame = previewFrame;
+
+ previewFrame = null;
+ }
+
+ String data = getScannedData(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation());
+ ScanListener scanListener = this.scanListener.get();
+
+ if (data != null && scanListener != null) {
+ scanListener.onQrDataFound(data);
+ return;
+ }
+ }
+ }
+
+ public void stopScanning() {
+ synchronized (this) {
+ scanning = false;
+ notify();
+ }
+ }
+
+ private @Nullable String getScannedData(byte[] data, int width, int height, int orientation) {
+ try {
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ byte[] rotatedData = new byte[data.length];
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ rotatedData[x * height + height - y - 1] = data[x + y * width];
+ }
+ }
+
+ int tmp = width;
+ width = height;
+ height = tmp;
+ data = rotatedData;
+ }
+
+ PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height,
+ 0, 0, width, height,
+ false);
+
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ Result result = reader.decode(bitmap, hints);
+
+ if (result != null) return result.getText();
+
+ } catch (NullPointerException | ChecksumException | FormatException e) {
+ Log.w(TAG, e);
+ } catch (NotFoundException e) {
+ // Thanks ZXing...
+ }
+
+ return null;
+ }
+}