From dfb4e4eb4686a4e6e05694a92e6005b2b7d1c895 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Feb 2018 23:58:56 +0100 Subject: [PATCH] integrate qr code scanner. temporarily break omemo activity scan --- src/main/AndroidManifest.xml | 10 +- .../ui/ConversationActivity.java | 20 +- .../ui/ConversationsOverviewFragment.java | 8 +- .../siacs/conversations/ui/OmemoActivity.java | 9 +- .../siacs/conversations/ui/ScanActivity.java | 292 ++++++++++ .../conversations/ui/TrustKeysActivity.java | 4 +- .../conversations/ui/UriHandlerActivity.java | 211 ++++--- .../ui/service/CameraManager.java | 306 ++++++++++ .../conversations/ui/widget/ScannerView.java | 157 ++++++ .../utils/zxing/IntentIntegrator.java | 533 ------------------ .../utils/zxing/IntentResult.java | 93 --- src/main/res/layout/activity_scan.xml | 17 + src/main/res/values/colors.xml | 7 + src/main/res/values/dimens.xml | 4 + src/main/res/values/strings.xml | 1 + src/main/res/values/themes.xml | 8 + 16 files changed, 954 insertions(+), 726 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/ScanActivity.java create mode 100644 src/main/java/eu/siacs/conversations/ui/service/CameraManager.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/zxing/IntentResult.java create mode 100644 src/main/res/layout/activity_scan.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index d98c7a489..687f94f59 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -14,6 +14,8 @@ + + @@ -56,10 +58,14 @@ + + android:label="@string/title_activity_start_conversation"> diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 0c92192dd..b3c57f027 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -252,6 +252,7 @@ public class ConversationActivity extends XmppActivity implements OnConversation } private boolean processViewIntent(Intent intent) { + Log.d(Config.LOGTAG,"process view intent"); String uuid = intent.getStringExtra(EXTRA_CONVERSATION); Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null; if (conversation == null) { @@ -262,9 +263,13 @@ public class ConversationActivity extends XmppActivity implements OnConversation return true; } + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); + } + @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { - Log.d(Config.LOGTAG,"on activity result"); if (resultCode == RESULT_OK) { handlePositiveActivityResult(requestCode, data); } else { @@ -308,7 +313,12 @@ public class ConversationActivity extends XmppActivity implements OnConversation this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview); this.initializeFragments(); this.invalidateActionBarTitle(); - final Intent intent = getIntent(); + final Intent intent; + if (savedInstanceState == null) { + intent = getIntent(); + } else { + intent = savedInstanceState.getParcelable("intent"); + } if (isViewIntent(intent)) { pendingViewIntent.push(intent); setIntent(createLauncherIntent(this)); @@ -377,6 +387,12 @@ public class ConversationActivity extends XmppActivity implements OnConversation return super.onOptionsItemSelected(item); } + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putParcelable("intent", getIntent()); + super.onSaveInstanceState(savedInstanceState); + } + @Override protected void onStart() { final int theme = findTheme(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index 495f50ee9..dd951e124 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -152,10 +152,16 @@ public class ConversationsOverviewFragment extends XmppFragment implements Enhan @Override public void onSaveInstanceState(Bundle bundle) { super.onSaveInstanceState(bundle); - bundle.putParcelable(STATE_SCROLL_POSITION,getScrollState()); + ScrollState scrollState = getScrollState(); + if (scrollState != null) { + bundle.putParcelable(STATE_SCROLL_POSITION, scrollState); + } } private ScrollState getScrollState() { + if (this.binding == null) { + return null; + } int position = this.binding.list.getFirstVisiblePosition(); final View view = this.binding.list.getChildAt(0); if (view != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index 3b5f5e2f1..d7dfc2203 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -25,9 +25,6 @@ import eu.siacs.conversations.databinding.ContactKeyBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.utils.zxing.IntentIntegrator; -import eu.siacs.conversations.utils.zxing.IntentResult; - public abstract class OmemoActivity extends XmppActivity { @@ -76,7 +73,7 @@ public abstract class OmemoActivity extends XmppActivity { copyOmemoFingerprint(mSelectedFingerprint); break; case R.id.verify_scan: - new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); + //new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); break; } return true; @@ -84,7 +81,7 @@ public abstract class OmemoActivity extends XmppActivity { @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + /*IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (scanResult != null && scanResult.getFormatName() != null) { String data = scanResult.getContents(); XmppUri uri = new XmppUri(data); @@ -93,7 +90,7 @@ public abstract class OmemoActivity extends XmppActivity { } else { this.mPendingFingerprintVerificationUri =uri; } - } + }*/ } protected abstract void processFingerprintVerification(XmppUri uri); diff --git a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java new file mode 100644 index 000000000..149b9e2ef --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java @@ -0,0 +1,292 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * 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 eu.siacs.conversations.ui; + +import java.util.EnumMap; +import java.util.Map; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.Vibrator; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Surface; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.WindowManager; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.service.CameraManager; +import eu.siacs.conversations.ui.widget.ScannerView; + +/** + * @author Andreas Schildbach + */ +@SuppressWarnings("deprecation") +public final class ScanActivity extends Activity implements SurfaceTextureListener, ActivityCompat.OnRequestPermissionsResultCallback { + public static final String INTENT_EXTRA_RESULT = "result"; + + private static final long VIBRATE_DURATION = 50L; + private static final long AUTO_FOCUS_INTERVAL_MS = 2500L; + private static boolean DISABLE_CONTINUOUS_AUTOFOCUS = Build.MODEL.equals("GT-I9100") // Galaxy S2 + || Build.MODEL.equals("SGH-T989") // Galaxy S2 + || Build.MODEL.equals("SGH-T989D") // Galaxy S2 X + || Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket + || Build.MODEL.equals("GT-I9300") // Galaxy S3 + || Build.MODEL.equals("GT-N7000"); // Galaxy Note + private final CameraManager cameraManager = new CameraManager(); + private ScannerView scannerView; + private TextureView previewView; + private volatile boolean surfaceCreated = false; + private Vibrator vibrator; + private HandlerThread cameraThread; + private volatile Handler cameraHandler; + private final Runnable closeRunnable = new Runnable() { + @Override + public void run() { + cameraHandler.removeCallbacksAndMessages(null); + cameraManager.close(); + } + }; + private final Runnable fetchAndDecodeRunnable = new Runnable() { + private final QRCodeReader reader = new QRCodeReader(); + private final Map hints = new EnumMap(DecodeHintType.class); + + @Override + public void run() { + cameraManager.requestPreviewFrame((data, camera) -> decode(data)); + } + + private void decode(final byte[] data) { + final PlanarYUVLuminanceSource source = cameraManager.buildLuminanceSource(data); + final BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + + try { + hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, (ResultPointCallback) dot -> runOnUiThread(() -> scannerView.addDot(dot))); + final Result scanResult = reader.decode(bitmap, hints); + + runOnUiThread(() -> handleResult(scanResult)); + } catch (final ReaderException x) { + // retry + cameraHandler.post(fetchAndDecodeRunnable); + } finally { + reader.reset(); + } + } + }; + private final Runnable openRunnable = new Runnable() { + @Override + public void run() { + try { + final Camera camera = cameraManager.open(previewView, displayRotation(), !DISABLE_CONTINUOUS_AUTOFOCUS); + + final Rect framingRect = cameraManager.getFrame(); + final RectF framingRectInPreview = new RectF(cameraManager.getFramePreview()); + framingRectInPreview.offsetTo(0, 0); + final boolean cameraFlip = cameraManager.getFacing() == CameraInfo.CAMERA_FACING_FRONT; + final int cameraRotation = cameraManager.getOrientation(); + + runOnUiThread(() -> scannerView.setFraming(framingRect, framingRectInPreview, displayRotation(), cameraRotation, cameraFlip)); + + final String focusMode = camera.getParameters().getFocusMode(); + final boolean nonContinuousAutoFocus = Camera.Parameters.FOCUS_MODE_AUTO.equals(focusMode) + || Camera.Parameters.FOCUS_MODE_MACRO.equals(focusMode); + + if (nonContinuousAutoFocus) + cameraHandler.post(new AutoFocusRunnable(camera)); + + cameraHandler.post(fetchAndDecodeRunnable); + } catch (final Exception x) { + Log.d(Config.LOGTAG, "problem opening camera", x); + } + } + + private int displayRotation() { + final int rotation = getWindowManager().getDefaultDisplay().getRotation(); + if (rotation == Surface.ROTATION_0) + return 0; + else if (rotation == Surface.ROTATION_90) + return 90; + else if (rotation == Surface.ROTATION_180) + return 180; + else if (rotation == Surface.ROTATION_270) + return 270; + else + throw new IllegalStateException("rotation: " + rotation); + } + }; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + + setContentView(R.layout.activity_scan); + scannerView = findViewById(R.id.scan_activity_mask); + previewView = findViewById(R.id.scan_activity_preview); + previewView.setSurfaceTextureListener(this); + + cameraThread = new HandlerThread("cameraThread", Process.THREAD_PRIORITY_BACKGROUND); + cameraThread.start(); + cameraHandler = new Handler(cameraThread.getLooper()); + } + + @Override + protected void onResume() { + super.onResume(); + maybeOpenCamera(); + } + + @Override + protected void onPause() { + cameraHandler.post(closeRunnable); + + super.onPause(); + } + + @Override + protected void onDestroy() { + // cancel background thread + cameraHandler.removeCallbacksAndMessages(null); + cameraThread.quit(); + + previewView.setSurfaceTextureListener(null); + + super.onDestroy(); + } + + private void maybeOpenCamera() { + if (surfaceCreated && ContextCompat.checkSelfPermission(this, + Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) + cameraHandler.post(openRunnable); + } + + @Override + public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) { + surfaceCreated = true; + maybeOpenCamera(); + } + + @Override + public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) { + surfaceCreated = false; + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) { + } + + @Override + public void onSurfaceTextureUpdated(final SurfaceTexture surface) { + } + + @Override + public void onAttachedToWindow() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + + @Override + public void onBackPressed() { + scannerView.setVisibility(View.GONE); + setResult(RESULT_CANCELED); + postFinish(); + } + + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_FOCUS: + case KeyEvent.KEYCODE_CAMERA: + // don't launch camera app + return true; + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_UP: + cameraHandler.post(() -> cameraManager.setTorch(keyCode == KeyEvent.KEYCODE_VOLUME_UP)); + return true; + } + + return super.onKeyDown(keyCode, event); + } + + public void handleResult(final Result scanResult) { + vibrator.vibrate(VIBRATE_DURATION); + + scannerView.setIsResult(true); + + final Intent result = new Intent(); + result.putExtra(INTENT_EXTRA_RESULT, scanResult.getText()); + setResult(RESULT_OK, result); + postFinish(); + } + + private void postFinish() { + new Handler().postDelayed(() -> finish(), 50); + } + + private final class AutoFocusRunnable implements Runnable { + private final Camera camera; + private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(final boolean success, final Camera camera) { + // schedule again + cameraHandler.postDelayed(AutoFocusRunnable.this, AUTO_FOCUS_INTERVAL_MS); + } + }; + + public AutoFocusRunnable(final Camera camera) { + this.camera = camera; + } + + @Override + public void run() { + try { + camera.autoFocus(autoFocusCallback); + } catch (final Exception x) { + Log.d(Config.LOGTAG, "problem with auto-focus, will not schedule again", x); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index ae2990c00..6cb0cd891 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -36,12 +36,10 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.utils.zxing.IntentIntegrator; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import static android.databinding.DataBindingUtil.inflate; public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated { private List contactJids; @@ -135,7 +133,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat if (hasPendingKeyFetches()) { Toast.makeText(this, R.string.please_wait_for_keys_to_be_fetched, Toast.LENGTH_SHORT).show(); } else { - new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); + //new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); return true; } } diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index 59292d6af..1bbe7d238 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -1,115 +1,154 @@ package eu.siacs.conversations.ui; +import android.Manifest; import android.app.Activity; -import android.support.v7.app.AppCompatActivity ; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.v13.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; import android.content.Intent; import android.net.Uri; -import android.util.Log; +import android.widget.Toast; -import java.util.Arrays; import java.util.List; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.utils.zxing.IntentIntegrator; -import eu.siacs.conversations.utils.zxing.IntentResult; import eu.siacs.conversations.xmpp.jid.Jid; public class UriHandlerActivity extends AppCompatActivity { - public static final String ACTION_SCAN_QR_CODE = "scan_qr_code"; - @Override - public void onStart() { - super.onStart(); - handleIntent(getIntent()); - } + public static final String ACTION_SCAN_QR_CODE = "scan_qr_code"; + private static final int REQUEST_SCAN_QR_CODE = 0x1234; + private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789; - @Override - public void onNewIntent(Intent intent) { - handleIntent(intent); - } + private boolean handled = false; - private void handleUri(Uri uri) { - final Intent intent; - final XmppUri xmppUri = new XmppUri(uri); - final List accounts = DatabaseBackend.getInstance(this).getAccountJids(); + public static void scan(Activity activity) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + Intent intent = new Intent(activity, UriHandlerActivity.class); + intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + activity.startActivity(intent); + } else { + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN); + } + } - if (accounts.size() == 0) { - intent = new Intent(getApplicationContext(), WelcomeActivity.class); - WelcomeActivity.addInviteUri(intent, xmppUri); - startActivity(intent); - return; - } + public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) { + if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) { + return; + } + if (grantResults.length > 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + scan(activity); + } else { + Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show(); + } + } + } - if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) { - final Jid jid = xmppUri.getJid(); - final String body = xmppUri.getBody(); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled",false); + } - if (jid != null) { - intent = new Intent(getApplicationContext(), ShareViaAccountActivity.class); - intent.putExtra(ShareViaAccountActivity.EXTRA_CONTACT, jid.toString()); - intent.putExtra(ShareViaAccountActivity.EXTRA_BODY, body); - } else { - intent = new Intent(getApplicationContext(), ShareWithActivity.class); - intent.setAction(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, body); - } - } else if (accounts.contains(xmppUri.getJid())) { - intent = new Intent(getApplicationContext(), EditAccountActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra("jid", xmppUri.getJid().toBareJid().toString()); - intent.setData(uri); - } else { - intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.setData(uri); - } + @Override + public void onStart() { + super.onStart(); + handleIntent(getIntent()); + } - startActivity(intent); - } + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putBoolean("handled", this.handled); + super.onSaveInstanceState(savedInstanceState); + } - private void handleIntent(Intent data) { - if (data == null || data.getAction() == null) { - finish(); - return; - } + @Override + public void onNewIntent(Intent intent) { + handleIntent(intent); + } - switch (data.getAction()) { - case Intent.ACTION_VIEW: - case Intent.ACTION_SENDTO: - handleUri(data.getData()); - break; - case ACTION_SCAN_QR_CODE: - new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC", "QR_CODE")); - return; - } + private void handleUri(Uri uri) { + final Intent intent; + final XmppUri xmppUri = new XmppUri(uri); + final List accounts = DatabaseBackend.getInstance(this).getAccountJids(); //TODO only look at enabled accounts - finish(); - } + if (accounts.size() == 0) { + intent = new Intent(getApplicationContext(), WelcomeActivity.class); + WelcomeActivity.addInviteUri(intent, xmppUri); + startActivity(intent); + return; + } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) { + final Jid jid = xmppUri.getJid(); + final String body = xmppUri.getBody(); - if (scanResult != null && scanResult.getFormatName() != null) { - String data = scanResult.getContents(); - handleUri(Uri.parse(data)); - } - } + if (jid != null) { + intent = new Intent(getApplicationContext(), ShareViaAccountActivity.class); + intent.putExtra(ShareViaAccountActivity.EXTRA_CONTACT, jid.toString()); + intent.putExtra(ShareViaAccountActivity.EXTRA_BODY, body); + } else { + intent = new Intent(getApplicationContext(), ShareWithActivity.class); + intent.setAction(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, body); + } + } else if (accounts.contains(xmppUri.getJid())) { + intent = new Intent(getApplicationContext(), EditAccountActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra("jid", xmppUri.getJid().toBareJid().toString()); + intent.setData(uri); + } else { + intent = new Intent(getApplicationContext(), StartConversationActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.setData(uri); + } - finish(); - super.onActivityResult(requestCode, requestCode, intent); - } + startActivity(intent); + } - public static void scan(Activity activity) { - Intent intent = new Intent(activity, UriHandlerActivity.class); - intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - activity.startActivity(intent); - } + private void handleIntent(Intent data) { + if (handled) { + return; + } + if (data == null || data.getAction() == null) { + finish(); + return; + } + + handled = true; + + switch (data.getAction()) { + case Intent.ACTION_VIEW: + case Intent.ACTION_SENDTO: + handleUri(data.getData()); + break; + case ACTION_SCAN_QR_CODE: + Intent intent = new Intent(this, ScanActivity.class); + startActivityForResult(intent, REQUEST_SCAN_QR_CODE); + return; + } + + finish(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, requestCode, intent); + if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { + String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); + if (result != null) { + Uri uri = Uri.parse(result); + handleUri(uri); + } + } + finish(); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java b/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java new file mode 100644 index 000000000..a818368de --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java @@ -0,0 +1,306 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * 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 eu.siacs.conversations.ui.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import com.google.zxing.PlanarYUVLuminanceSource; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.PreviewCallback; +import android.util.Log; +import android.view.TextureView; + +import eu.siacs.conversations.Config; + +/** + * @author Andreas Schildbach + */ +@SuppressWarnings("deprecation") +public final class CameraManager { + private static final int MIN_FRAME_SIZE = 240; + private static final int MAX_FRAME_SIZE = 600; + private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen + private static final int MAX_PREVIEW_PIXELS = 1280 * 720; + + private Camera camera; + private CameraInfo cameraInfo = new CameraInfo(); + private Camera.Size cameraResolution; + private Rect frame; + private RectF framePreview; + + public Rect getFrame() { + return frame; + } + + public RectF getFramePreview() { + return framePreview; + } + + public int getFacing() { + return cameraInfo.facing; + } + + public int getOrientation() { + return cameraInfo.orientation; + } + + public Camera open(final TextureView textureView, final int displayOrientation, final boolean continuousAutoFocus) + throws IOException { + final int cameraId = determineCameraId(); + Camera.getCameraInfo(cameraId, cameraInfo); + + camera = Camera.open(cameraId); + + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) + camera.setDisplayOrientation((720 - displayOrientation - cameraInfo.orientation) % 360); + else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) + camera.setDisplayOrientation((720 - displayOrientation + cameraInfo.orientation) % 360); + else + throw new IllegalStateException("facing: " + cameraInfo.facing); + + camera.setPreviewTexture(textureView.getSurfaceTexture()); + + final Camera.Parameters parameters = camera.getParameters(); + + cameraResolution = findBestPreviewSizeValue(parameters, textureView.getWidth(), textureView.getHeight()); + + final int width = textureView.getWidth(); + final int height = textureView.getHeight(); + + final int rawSize = Math.min(width * 2 / 3, height * 2 / 3); + final int frameSize = Math.max(MIN_FRAME_SIZE, Math.min(MAX_FRAME_SIZE, rawSize)); + + final int leftOffset = (width - frameSize) / 2; + final int topOffset = (height - frameSize) / 2; + frame = new Rect(leftOffset, topOffset, leftOffset + frameSize, topOffset + frameSize); + framePreview = new RectF(frame.left * cameraResolution.width / width, + frame.top * cameraResolution.height / height, frame.right * cameraResolution.width / width, + frame.bottom * cameraResolution.height / height); + + final String savedParameters = parameters == null ? null : parameters.flatten(); + + try { + setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus); + } catch (final RuntimeException x) { + if (savedParameters != null) { + final Camera.Parameters parameters2 = camera.getParameters(); + parameters2.unflatten(savedParameters); + try { + camera.setParameters(parameters2); + setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus); + } catch (final RuntimeException x2) { + Log.d(Config.LOGTAG,"problem setting camera parameters", x2); + } + } + } + + try { + camera.startPreview(); + return camera; + } catch (final RuntimeException x) { + Log.w(Config.LOGTAG,"something went wrong while starting camera preview", x); + camera.release(); + throw x; + } + } + + private int determineCameraId() { + final int cameraCount = Camera.getNumberOfCameras(); + final CameraInfo cameraInfo = new CameraInfo(); + + // prefer back-facing camera + for (int i = 0; i < cameraCount; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) + return i; + } + + // fall back to front-facing camera + for (int i = 0; i < cameraCount; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) + return i; + } + + return -1; + } + + public void close() { + if (camera != null) { + try { + camera.stopPreview(); + } catch (final RuntimeException x) { + Log.w(Config.LOGTAG,"something went wrong while stopping camera preview", x); + } + + camera.release(); + } + } + + private static final Comparator numPixelComparator = new Comparator() { + @Override + public int compare(final Camera.Size size1, final Camera.Size size2) { + final int pixels1 = size1.height * size1.width; + final int pixels2 = size2.height * size2.width; + + if (pixels1 < pixels2) + return 1; + else if (pixels1 > pixels2) + return -1; + else + return 0; + } + }; + + private static Camera.Size findBestPreviewSizeValue(final Camera.Parameters parameters, int width, int height) { + if (height > width) { + final int temp = width; + width = height; + height = temp; + } + + final float screenAspectRatio = (float) width / (float) height; + + final List rawSupportedSizes = parameters.getSupportedPreviewSizes(); + if (rawSupportedSizes == null) + return parameters.getPreviewSize(); + + // sort by size, descending + final List supportedPreviewSizes = new ArrayList(rawSupportedSizes); + Collections.sort(supportedPreviewSizes, numPixelComparator); + + Camera.Size bestSize = null; + float diff = Float.POSITIVE_INFINITY; + + for (final Camera.Size supportedPreviewSize : supportedPreviewSizes) { + final int realWidth = supportedPreviewSize.width; + final int realHeight = supportedPreviewSize.height; + final int realPixels = realWidth * realHeight; + if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS) + continue; + + final boolean isCandidatePortrait = realWidth < realHeight; + final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth; + final int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight; + if (maybeFlippedWidth == width && maybeFlippedHeight == height) + return supportedPreviewSize; + + final float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight; + final float newDiff = Math.abs(aspectRatio - screenAspectRatio); + if (newDiff < diff) { + bestSize = supportedPreviewSize; + diff = newDiff; + } + } + + if (bestSize != null) + return bestSize; + else + return parameters.getPreviewSize(); + } + + @SuppressLint("InlinedApi") + private static void setDesiredCameraParameters(final Camera camera, final Camera.Size cameraResolution, + final boolean continuousAutoFocus) { + final Camera.Parameters parameters = camera.getParameters(); + if (parameters == null) + return; + + final List supportedFocusModes = parameters.getSupportedFocusModes(); + final String focusMode = continuousAutoFocus + ? findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, + Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, Camera.Parameters.FOCUS_MODE_AUTO, + Camera.Parameters.FOCUS_MODE_MACRO) + : findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_MACRO); + if (focusMode != null) + parameters.setFocusMode(focusMode); + + parameters.setPreviewSize(cameraResolution.width, cameraResolution.height); + + camera.setParameters(parameters); + } + + public void requestPreviewFrame(final PreviewCallback callback) { + try { + camera.setOneShotPreviewCallback(callback); + } catch (final RuntimeException x) { + Log.d(Config.LOGTAG,"problem requesting preview frame, callback won't be called", x); + } + } + + public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) { + return new PlanarYUVLuminanceSource(data, cameraResolution.width, cameraResolution.height, + (int) framePreview.left, (int) framePreview.top, (int) framePreview.width(), + (int) framePreview.height(), false); + } + + public void setTorch(final boolean enabled) { + if (enabled != getTorchEnabled(camera)) + setTorchEnabled(camera, enabled); + } + + private static boolean getTorchEnabled(final Camera camera) { + final Camera.Parameters parameters = camera.getParameters(); + if (parameters != null) { + final String flashMode = camera.getParameters().getFlashMode(); + return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode) + || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)); + } + + return false; + } + + private static void setTorchEnabled(final Camera camera, final boolean enabled) { + final Camera.Parameters parameters = camera.getParameters(); + + final List supportedFlashModes = parameters.getSupportedFlashModes(); + if (supportedFlashModes != null) { + final String flashMode; + if (enabled) + flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH, + Camera.Parameters.FLASH_MODE_ON); + else + flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF); + + if (flashMode != null) { + camera.cancelAutoFocus(); // autofocus can cause conflict + + parameters.setFlashMode(flashMode); + camera.setParameters(parameters); + } + } + } + + private static String findValue(final Collection values, final String... valuesToFind) { + for (final String valueToFind : valuesToFind) + if (values.contains(valueToFind)) + return valueToFind; + + return null; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java b/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java new file mode 100644 index 000000000..f1fe18f2c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * 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 eu.siacs.conversations.ui.widget; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.google.zxing.ResultPoint; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import eu.siacs.conversations.R; + +/** + * @author Andreas Schildbach + */ +public class ScannerView extends View { + private static final long LASER_ANIMATION_DELAY_MS = 100l; + private static final int DOT_OPACITY = 0xa0; + private static final int DOT_TTL_MS = 500; + + private final Paint maskPaint; + private final Paint laserPaint; + private final Paint dotPaint; + private boolean isResult; + private final int maskColor, maskResultColor; + private final int laserColor; + private final int dotColor, dotResultColor; + private final Map dots = new HashMap(16); + private Rect frame; + private final Matrix matrix = new Matrix(); + + public ScannerView(final Context context, final AttributeSet attrs) { + super(context, attrs); + + final Resources res = getResources(); + maskColor = res.getColor(R.color.scan_mask); + maskResultColor = res.getColor(R.color.scan_result_view); + laserColor = res.getColor(R.color.scan_laser); + dotColor = res.getColor(R.color.scan_dot); + dotResultColor = res.getColor(R.color.scan_result_dots); + + maskPaint = new Paint(); + maskPaint.setStyle(Style.FILL); + + laserPaint = new Paint(); + laserPaint.setStrokeWidth(res.getDimensionPixelSize(R.dimen.scan_laser_width)); + laserPaint.setStyle(Style.STROKE); + + dotPaint = new Paint(); + dotPaint.setAlpha(DOT_OPACITY); + dotPaint.setStyle(Style.STROKE); + dotPaint.setStrokeWidth(res.getDimension(R.dimen.scan_dot_size)); + dotPaint.setAntiAlias(true); + } + + public void setFraming(final Rect frame, final RectF framePreview, final int displayRotation, + final int cameraRotation, final boolean cameraFlip) { + this.frame = frame; + matrix.setRectToRect(framePreview, new RectF(frame), ScaleToFit.FILL); + matrix.postRotate(-displayRotation, frame.exactCenterX(), frame.exactCenterY()); + matrix.postScale(cameraFlip ? -1 : 1, 1, frame.exactCenterX(), frame.exactCenterY()); + matrix.postRotate(cameraRotation, frame.exactCenterX(), frame.exactCenterY()); + + invalidate(); + } + + public void setIsResult(final boolean isResult) { + this.isResult = isResult; + + invalidate(); + } + + public void addDot(final ResultPoint dot) { + dots.put(new float[] { dot.getX(), dot.getY() }, System.currentTimeMillis()); + + invalidate(); + } + + @Override + public void onDraw(final Canvas canvas) { + if (frame == null) + return; + + final long now = System.currentTimeMillis(); + + final int width = canvas.getWidth(); + final int height = canvas.getHeight(); + + final float[] point = new float[2]; + + // draw mask darkened + maskPaint.setColor(isResult ? maskResultColor : maskColor); + canvas.drawRect(0, 0, width, frame.top, maskPaint); + canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, maskPaint); + canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, maskPaint); + canvas.drawRect(0, frame.bottom + 1, width, height, maskPaint); + + if (isResult) { + laserPaint.setColor(dotResultColor); + laserPaint.setAlpha(160); + + dotPaint.setColor(dotResultColor); + } else { + laserPaint.setColor(laserColor); + final boolean laserPhase = (now / 600) % 2 == 0; + laserPaint.setAlpha(laserPhase ? 160 : 255); + + dotPaint.setColor(dotColor); + + // schedule redraw + postInvalidateDelayed(LASER_ANIMATION_DELAY_MS); + } + + canvas.drawRect(frame, laserPaint); + + // draw points + for (final Iterator> i = dots.entrySet().iterator(); i.hasNext();) { + final Map.Entry entry = i.next(); + final long age = now - entry.getValue(); + if (age < DOT_TTL_MS) { + dotPaint.setAlpha((int) ((DOT_TTL_MS - age) * 256 / DOT_TTL_MS)); + + matrix.mapPoints(point, entry.getKey()); + canvas.drawPoint(point[0], point[1], dotPaint); + } else { + i.remove(); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java b/src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java deleted file mode 100644 index a615569d8..000000000 --- a/src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Copyright 2009 ZXing authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.siacs.conversations.utils.zxing; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import android.app.Activity; -import android.support.v7.app.AlertDialog; -import android.app.Fragment; -import android.content.ActivityNotFoundException; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import eu.siacs.conversations.ui.UriHandlerActivity; - -/** - *

A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple - * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the - * project's source code.

- * - *

Initiating a barcode scan

- * - *

To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait - * for the result in your app.

- * - *

It does require that the Barcode Scanner (or work-alike) application is installed. The - * {@link #initiateScan()} method will prompt the user to download the application, if needed.

- * - *

There are a few steps to using this integration. First, your {@link Activity} must implement - * the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:

- * - *
{@code
- * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
- *   IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
- *   if (scanResult != null) {
- *     // handle scan result
- *   }
- *   // else continue with any other code you need in the method
- *   ...
- * }
- * }
- * - *

This is where you will handle a scan result.

- * - *

Second, just call this in response to a user action somewhere to begin the scan process:

- * - *
{@code
- * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
- * integrator.initiateScan();
- * }
- * - *

Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the - * user was prompted to download the application. This lets the calling app potentially manage the dialog. - * In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()} - * method.

- * - *

You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use - * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and - * yes/no button labels can be changed.

- * - *

Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used - * to invoke the scanner. This can be used to set additional options not directly exposed by this - * simplified API.

- * - *

By default, this will only allow applications that are known to respond to this intent correctly - * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}. - * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.

- * - *

Sharing text via barcode

- * - *

To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.

- * - *

Some code, particularly download integration, was contributed from the Anobiit application.

- * - *

Enabling experimental barcode formats

- * - *

Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as - * PDF417. Use {@link #initiateScan(Collection)} with - * a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such - * formats.

- * - * @author Sean Owen - * @author Fred Lin - * @author Isaac Potoczny-Jones - * @author Brad Drehmer - * @author gcstang - */ -public class IntentIntegrator { - - public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits - private static final String TAG = IntentIntegrator.class.getSimpleName(); - - public static final String DEFAULT_TITLE = "Install Barcode Scanner?"; - public static final String DEFAULT_MESSAGE = - "This application requires Barcode Scanner. Would you like to install it?"; - public static final String DEFAULT_YES = "Yes"; - public static final String DEFAULT_NO = "No"; - - private static final String BS_PACKAGE = "com.google.zxing.client.android"; - private static final String BSPLUS_PACKAGE = "com.srowen.bs.android"; - - // supported barcode formats - public static final Collection PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14"); - public static final Collection ONE_D_CODE_TYPES = - list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128", - "ITF", "RSS_14", "RSS_EXPANDED"); - public static final Collection QR_CODE_TYPES = Collections.singleton("QR_CODE"); - public static final Collection DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX"); - - public static final Collection ALL_CODE_TYPES = null; - - public static final List TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE); - public static final List TARGET_ALL_KNOWN = list( - BSPLUS_PACKAGE, // Barcode Scanner+ - BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple - BS_PACKAGE // Barcode Scanner - // What else supports this intent? - ); - - // Should be FLAG_ACTIVITY_NEW_DOCUMENT in API 21+. - // Defined once here because the current value is deprecated, so generates just one warning - private static final int FLAG_NEW_DOC = Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; - - private final Activity activity; - private final Fragment fragment; - - private String title; - private String message; - private String buttonYes; - private String buttonNo; - private List targetApplications; - private final Map moreExtras = new HashMap(3); - - /** - * @param activity {@link Activity} invoking the integration - */ - public IntentIntegrator(Activity activity) { - this.activity = activity; - this.fragment = null; - initializeConfiguration(); - } - - /** - * @param fragment {@link Fragment} invoking the integration. - * {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead - * of an {@link Activity} - */ - public IntentIntegrator(Fragment fragment) { - this.activity = fragment.getActivity(); - this.fragment = fragment; - initializeConfiguration(); - } - - private void initializeConfiguration() { - title = DEFAULT_TITLE; - message = DEFAULT_MESSAGE; - buttonYes = DEFAULT_YES; - buttonNo = DEFAULT_NO; - targetApplications = TARGET_ALL_KNOWN; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public void setTitleByID(int titleID) { - title = activity.getString(titleID); - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public void setMessageByID(int messageID) { - message = activity.getString(messageID); - } - - public String getButtonYes() { - return buttonYes; - } - - public void setButtonYes(String buttonYes) { - this.buttonYes = buttonYes; - } - - public void setButtonYesByID(int buttonYesID) { - buttonYes = activity.getString(buttonYesID); - } - - public String getButtonNo() { - return buttonNo; - } - - public void setButtonNo(String buttonNo) { - this.buttonNo = buttonNo; - } - - public void setButtonNoByID(int buttonNoID) { - buttonNo = activity.getString(buttonNoID); - } - - public Collection getTargetApplications() { - return targetApplications; - } - - public final void setTargetApplications(List targetApplications) { - if (targetApplications.isEmpty()) { - throw new IllegalArgumentException("No target applications"); - } - this.targetApplications = targetApplications; - } - - public void setSingleTargetApplication(String targetApplication) { - this.targetApplications = Collections.singletonList(targetApplication); - } - - public Map getMoreExtras() { - return moreExtras; - } - - public final void addExtra(String key, Object value) { - moreExtras.put(key, value); - } - - /** - * Initiates a scan for all known barcode types with the default camera. - * - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise. - */ - public final AlertDialog initiateScan() { - return initiateScan(ALL_CODE_TYPES, -1); - } - - /** - * Initiates a scan for all known barcode types with the specified camera. - * - * @param cameraId camera ID of the camera to use. A negative value means "no preference". - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise. - */ - public final AlertDialog initiateScan(int cameraId) { - return initiateScan(ALL_CODE_TYPES, cameraId); - } - - /** - * Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding - * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants - * like {@link #PRODUCT_CODE_TYPES} for example. - * - * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise. - */ - public final AlertDialog initiateScan(Collection desiredBarcodeFormats) { - return initiateScan(desiredBarcodeFormats, -1); - } - - /** - * Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding - * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants - * like {@link #PRODUCT_CODE_TYPES} for example. - * - * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for - * @param cameraId camera ID of the camera to use. A negative value means "no preference". - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise - */ - public final AlertDialog initiateScan(Collection desiredBarcodeFormats, int cameraId) { - Intent intentScan = new Intent(BS_PACKAGE + ".SCAN"); - intentScan.addCategory(Intent.CATEGORY_DEFAULT); - - // check which types of codes to scan for - if (desiredBarcodeFormats != null) { - // set the desired barcode types - StringBuilder joinedByComma = new StringBuilder(); - for (String format : desiredBarcodeFormats) { - if (joinedByComma.length() > 0) { - joinedByComma.append(','); - } - joinedByComma.append(format); - } - intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString()); - } - - // check requested camera ID - if (cameraId >= 0) { - intentScan.putExtra("SCAN_CAMERA_ID", cameraId); - } - - String targetAppPackage = findTargetAppPackage(intentScan); - if (targetAppPackage == null) { - return showDownloadDialog(); - } - intentScan.setPackage(targetAppPackage); - intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intentScan.addFlags(FLAG_NEW_DOC); - attachMoreExtras(intentScan); - startActivityForResult(intentScan, REQUEST_CODE); - return null; - } - - /** - * Start an activity. This method is defined to allow different methods of activity starting for - * newer versions of Android and for compatibility library. - * - * @param intent Intent to start. - * @param code Request code for the activity - * @see Activity#startActivityForResult(Intent, int) - * @see Fragment#startActivityForResult(Intent, int) - */ - protected void startActivityForResult(Intent intent, int code) { - if (fragment == null) { - activity.startActivityForResult(intent, code); - } else { - fragment.startActivityForResult(intent, code); - } - } - - private String findTargetAppPackage(Intent intent) { - PackageManager pm = activity.getPackageManager(); - List availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (availableApps != null) { - for (String targetApp : targetApplications) { - if (contains(availableApps, targetApp)) { - return targetApp; - } - } - } - return null; - } - - private static boolean contains(Iterable availableApps, String targetApp) { - for (ResolveInfo availableApp : availableApps) { - String packageName = availableApp.activityInfo.packageName; - if (targetApp.equals(packageName)) { - return true; - } - } - return false; - } - - private AlertDialog showDownloadDialog() { - AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); - downloadDialog.setTitle(title); - downloadDialog.setMessage(message); - downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - String packageName; - if (targetApplications.contains(BS_PACKAGE)) { - // Prefer to suggest download of BS if it's anywhere in the list - packageName = BS_PACKAGE; - } else { - // Otherwise, first option: - packageName = targetApplications.get(0); - } - Uri uri = Uri.parse("market://details?id=" + packageName); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - try { - if (fragment == null) { - activity.startActivity(intent); - finishIfNeeded(); - } else { - fragment.startActivity(intent); - } - } catch (ActivityNotFoundException anfe) { - // Hmm, market is not installed - Log.w(TAG, "Google Play is not installed; cannot install " + packageName); - } - } - }); - downloadDialog.setNegativeButton(buttonNo, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - finishIfNeeded(); - } - }); - downloadDialog.setCancelable(true); - downloadDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - finishIfNeeded(); - } - }); - return downloadDialog.show(); - } - - private void finishIfNeeded() { - if (fragment != null) { - return; - } - if (activity != null && activity instanceof UriHandlerActivity) { - activity.finish(); - } - } - - - /** - *

Call this from your {@link Activity}'s - * {@link Activity#onActivityResult(int, int, Intent)} method.

- * - * @param requestCode request code from {@code onActivityResult()} - * @param resultCode result code from {@code onActivityResult()} - * @param intent {@link Intent} from {@code onActivityResult()} - * @return null if the event handled here was not related to this class, or - * else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning, - * the fields will be null. - */ - public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) { - if (requestCode == REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - String contents = intent.getStringExtra("SCAN_RESULT"); - String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT"); - byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES"); - int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE); - Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation; - String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL"); - return new IntentResult(contents, - formatName, - rawBytes, - orientation, - errorCorrectionLevel); - } - return new IntentResult(); - } - return null; - } - - - /** - * Defaults to type "TEXT_TYPE". - * - * @param text the text string to encode as a barcode - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise - * @see #shareText(CharSequence, CharSequence) - */ - public final AlertDialog shareText(CharSequence text) { - return shareText(text, "TEXT_TYPE"); - } - - /** - * Shares the given text by encoding it as a barcode, such that another user can - * scan the text off the screen of the device. - * - * @param text the text string to encode as a barcode - * @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants. - * @return the {@link AlertDialog} that was shown to the user prompting them to download the app - * if a prompt was needed, or null otherwise - */ - public final AlertDialog shareText(CharSequence text, CharSequence type) { - Intent intent = new Intent(); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setAction(BS_PACKAGE + ".ENCODE"); - intent.putExtra("ENCODE_TYPE", type); - intent.putExtra("ENCODE_DATA", text); - String targetAppPackage = findTargetAppPackage(intent); - if (targetAppPackage == null) { - return showDownloadDialog(); - } - intent.setPackage(targetAppPackage); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.addFlags(FLAG_NEW_DOC); - attachMoreExtras(intent); - if (fragment == null) { - activity.startActivity(intent); - } else { - fragment.startActivity(intent); - } - return null; - } - - private static List list(String... values) { - return Collections.unmodifiableList(Arrays.asList(values)); - } - - private void attachMoreExtras(Intent intent) { - for (Map.Entry entry : moreExtras.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - // Kind of hacky - if (value instanceof Integer) { - intent.putExtra(key, (Integer) value); - } else if (value instanceof Long) { - intent.putExtra(key, (Long) value); - } else if (value instanceof Boolean) { - intent.putExtra(key, (Boolean) value); - } else if (value instanceof Double) { - intent.putExtra(key, (Double) value); - } else if (value instanceof Float) { - intent.putExtra(key, (Float) value); - } else if (value instanceof Bundle) { - intent.putExtra(key, (Bundle) value); - } else { - intent.putExtra(key, value.toString()); - } - } - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/zxing/IntentResult.java b/src/main/java/eu/siacs/conversations/utils/zxing/IntentResult.java deleted file mode 100644 index 4d0e4f818..000000000 --- a/src/main/java/eu/siacs/conversations/utils/zxing/IntentResult.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2009 ZXing authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.siacs.conversations.utils.zxing; - -/** - *

Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.

- * - * @author Sean Owen - */ -public final class IntentResult { - - private final String contents; - private final String formatName; - private final byte[] rawBytes; - private final Integer orientation; - private final String errorCorrectionLevel; - - IntentResult() { - this(null, null, null, null, null); - } - - IntentResult(String contents, - String formatName, - byte[] rawBytes, - Integer orientation, - String errorCorrectionLevel) { - this.contents = contents; - this.formatName = formatName; - this.rawBytes = rawBytes; - this.orientation = orientation; - this.errorCorrectionLevel = errorCorrectionLevel; - } - - /** - * @return raw content of barcode - */ - public String getContents() { - return contents; - } - - /** - * @return name of format, like "QR_CODE", "UPC_A". See {@code BarcodeFormat} for more format names. - */ - public String getFormatName() { - return formatName; - } - - /** - * @return raw bytes of the barcode content, if applicable, or null otherwise - */ - public byte[] getRawBytes() { - return rawBytes; - } - - /** - * @return rotation of the image, in degrees, which resulted in a successful scan. May be null. - */ - public Integer getOrientation() { - return orientation; - } - - /** - * @return name of the error correction level used in the barcode, if applicable - */ - public String getErrorCorrectionLevel() { - return errorCorrectionLevel; - } - - @Override - public String toString() { - int rawBytesLength = rawBytes == null ? 0 : rawBytes.length; - return "Format: " + formatName + '\n' + - "Contents: " + contents + '\n' + - "Raw bytes: (" + rawBytesLength + " bytes)\n" + - "Orientation: " + orientation + '\n' + - "EC level: " + errorCorrectionLevel + '\n'; - } - -} diff --git a/src/main/res/layout/activity_scan.xml b/src/main/res/layout/activity_scan.xml new file mode 100644 index 000000000..b4e88a3e8 --- /dev/null +++ b/src/main/res/layout/activity_scan.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 9794b8e09..d53f17790 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -23,4 +23,11 @@ #ff4b9b4a #ff4b9b4a #ff326130 + + + #60000000 + #cc0000 + #ff6600 + #b0000000 + #c099cc00 \ No newline at end of file diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 5bd04d25e..7523cf91a 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -11,4 +11,8 @@ 224dp 32dp 16dp + + + 4dp + 8dp diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e11634bdc..89bdea25a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -743,4 +743,5 @@ Certificate details: Certificate Verification Once + The QR code scanner needs access to the camera diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 3fcf87397..866d2ce59 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -177,4 +177,12 @@ 22sp + + \ No newline at end of file