integrate qr code scanner. temporarily break omemo activity scan

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                               |  10 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java          |  20 
src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java |   8 
src/main/java/eu/siacs/conversations/ui/OmemoActivity.java                 |   9 
src/main/java/eu/siacs/conversations/ui/ScanActivity.java                  | 292 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java             |   4 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java            | 239 
src/main/java/eu/siacs/conversations/ui/service/CameraManager.java         | 306 
src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java            | 157 
src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java     | 533 
src/main/java/eu/siacs/conversations/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, 968 insertions(+), 740 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -14,6 +14,8 @@
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
 
+    <uses-permission android:name="android.permission.CAMERA" />
+
     <uses-permission
         android:name="android.permission.READ_PHONE_STATE"
         tools:node="remove" />
@@ -56,10 +58,14 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".ui.ScanActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/ConversationsTheme.FullScreen"
+            android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.UriHandlerActivity"
-            android:label="@string/title_activity_start_conversation"
-            android:launchMode="singleTop">
+            android:label="@string/title_activity_start_conversation">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 

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();

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) {

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);

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 <http://www.gnu.org/licenses/>.
+ */
+
+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<DecodeHintType, Object> hints = new EnumMap<DecodeHintType, Object>(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);
+			}
+		}
+	}
+}

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<Jid> 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;
 				}
 		}

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());
-    }
-
-    @Override
-    public void onNewIntent(Intent intent) {
-        handleIntent(intent);
-    }
-
-    private void handleUri(Uri uri) {
-        final Intent intent;
-        final XmppUri xmppUri = new XmppUri(uri);
-        final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids();
-
-        if (accounts.size() == 0) {
-            intent = new Intent(getApplicationContext(), WelcomeActivity.class);
-            WelcomeActivity.addInviteUri(intent, xmppUri);
-            startActivity(intent);
-            return;
-        }
-
-        if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
-            final Jid jid = xmppUri.getJid();
-            final String body = xmppUri.getBody();
-
-            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);
-        }
-
-        startActivity(intent);
-    }
-
-    private void handleIntent(Intent data) {
-        if (data == null || data.getAction() == null) {
-            finish();
-            return;
-        }
-
-        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;
-        }
-
-        finish();
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
-        if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
-            IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
-
-            if (scanResult != null && scanResult.getFormatName() != null) {
-                String data = scanResult.getContents();
-                handleUri(Uri.parse(data));
-            }
-        }
-
-        finish();
-        super.onActivityResult(requestCode, requestCode, 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);
-    }
+
+	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;
+
+	private boolean handled = false;
+
+	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);
+		}
+	}
+
+	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();
+			}
+		}
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled",false);
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		handleIntent(getIntent());
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle savedInstanceState) {
+		savedInstanceState.putBoolean("handled", this.handled);
+		super.onSaveInstanceState(savedInstanceState);
+	}
+
+	@Override
+	public void onNewIntent(Intent intent) {
+		handleIntent(intent);
+	}
+
+	private void handleUri(Uri uri) {
+		final Intent intent;
+		final XmppUri xmppUri = new XmppUri(uri);
+		final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(); //TODO only look at enabled accounts
+
+		if (accounts.size() == 0) {
+			intent = new Intent(getApplicationContext(), WelcomeActivity.class);
+			WelcomeActivity.addInviteUri(intent, xmppUri);
+			startActivity(intent);
+			return;
+		}
+
+		if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
+			final Jid jid = xmppUri.getJid();
+			final String body = xmppUri.getBody();
+
+			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);
+		}
+
+		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();
+	}
 }

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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Camera.Size> numPixelComparator = new Comparator<Camera.Size>() {
+        @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<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
+        if (rawSupportedSizes == null)
+            return parameters.getPreviewSize();
+
+        // sort by size, descending
+        final List<Camera.Size> supportedPreviewSizes = new ArrayList<Camera.Size>(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<String> 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<String> 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<String> values, final String... valuesToFind) {
+        for (final String valueToFind : valuesToFind)
+            if (values.contains(valueToFind))
+                return valueToFind;
+
+        return null;
+    }
+}

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 <http://www.gnu.org/licenses/>.
+ */
+
+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<float[], Long> dots = new HashMap<float[], Long>(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<Map.Entry<float[], Long>> i = dots.entrySet().iterator(); i.hasNext();) {
+            final Map.Entry<float[], Long> 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();
+            }
+        }
+    }
+}

src/main/java/eu/siacs/conversations/utils/zxing/IntentIntegrator.java 🔗

@@ -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;
-
-/**
- * <p>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.</p>
- *
- * <h2>Initiating a barcode scan</h2>
- *
- * <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
- * for the result in your app.</p>
- *
- * <p>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.</p>
- *
- * <p>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:</p>
- *
- * <pre>{@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
- *   ...
- * }
- * }</pre>
- *
- * <p>This is where you will handle a scan result.</p>
- *
- * <p>Second, just call this in response to a user action somewhere to begin the scan process:</p>
- *
- * <pre>{@code
- * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
- * integrator.initiateScan();
- * }</pre>
- *
- * <p>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.</p>
- * 
- * <p>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.</p>
- *
- * <p>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.</p>
- * 
- * <p>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.</p>
- *
- * <h2>Sharing text via barcode</h2>
- *
- * <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p>
- *
- * <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
- *
- * <h2>Enabling experimental barcode formats</h2>
- *
- * <p>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.</p>
- *
- * @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<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
-  public static final Collection<String> 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<String> QR_CODE_TYPES = Collections.singleton("QR_CODE");
-  public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
-
-  public static final Collection<String> ALL_CODE_TYPES = null;
-  
-  public static final List<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
-  public static final List<String> 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<String> targetApplications;
-  private final Map<String,Object> moreExtras = new HashMap<String,Object>(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<String> getTargetApplications() {
-    return targetApplications;
-  }
-  
-  public final void setTargetApplications(List<String> 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<String,?> 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<String> 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<String> 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<ResolveInfo> 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<ResolveInfo> 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();
-    }
-  }
-
-
-  /**
-   * <p>Call this from your {@link Activity}'s
-   * {@link Activity#onActivityResult(int, int, Intent)} method.</p>
-   *
-   * @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<String> list(String... values) {
-    return Collections.unmodifiableList(Arrays.asList(values));
-  }
-
-  private void attachMoreExtras(Intent intent) {
-    for (Map.Entry<String,Object> 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());
-      }
-    }
-  }
-
-}

src/main/java/eu/siacs/conversations/utils/zxing/IntentResult.java 🔗

@@ -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;
-
-/**
- * <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p>
- *
- * @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';
-  }
-
-}

src/main/res/layout/activity_scan.xml 🔗

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <TextureView
+        android:id="@+id/scan_activity_preview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:keepScreenOn="true" />
+
+    <eu.siacs.conversations.ui.widget.ScannerView
+        android:id="@+id/scan_activity_mask"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</merge>

src/main/res/values/colors.xml 🔗

@@ -23,4 +23,11 @@
 	<color name="bubble">#ff4b9b4a</color>
 	<color name="unreadcountlight">#ff4b9b4a</color>
 	<color name="unreadcountdark">#ff326130</color>
+
+	<!-- scanner -->
+	<color name="scan_mask">#60000000</color>
+	<color name="scan_laser">#cc0000</color>
+	<color name="scan_dot">#ff6600</color>
+	<color name="scan_result_view">#b0000000</color>
+	<color name="scan_result_dots">#c099cc00</color>
 </resources>

src/main/res/values/dimens.xml 🔗

@@ -11,4 +11,8 @@
 	<dimen name="audio_player_width">224dp</dimen>
 	<dimen name="swipe_handle_size">32dp</dimen>
 	<dimen name="avatar_item_distance">16dp</dimen>
+
+	<!-- scanner -->
+	<dimen name="scan_laser_width">4dp</dimen>
+	<dimen name="scan_dot_size">8dp</dimen>
 </resources>

src/main/res/values/strings.xml 🔗

@@ -743,4 +743,5 @@
 	<string name="mtm_cert_details">Certificate details:</string>
 	<string name="mtm_notification">Certificate Verification</string>
 	<string name="once">Once</string>
+    <string name="qr_code_scanner_needs_access_to_camera">The QR code scanner needs access to the camera</string>
 </resources>

src/main/res/values/themes.xml 🔗

@@ -177,4 +177,12 @@
         <item name="TextSizeHeadline">22sp</item>
     </style>
 
+    <style name="ConversationsTheme.FullScreen" parent="@style/Theme.AppCompat.Light">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowBackground">@android:color/black</item>
+    </style>
+
 </resources>