From c9219fd1be0c1684149a902b29b9dfa015c85a05 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 19 Feb 2019 19:24:46 +1300 Subject: [PATCH 01/65] Added 2 files for testing new plugin. --- .../cordova-plugin-qrreader/www/qrreader.js | 87 +++++++++++ .../bitcoin/cordova/qrreader/QRReader.java | 146 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js new file mode 100644 index 000000000..3616efdf4 --- /dev/null +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -0,0 +1,87 @@ +cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * +*/ + +/* +var argscheck = require('cordova/argscheck'); +var channel = require('cordova/channel'); +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var cordova = require('cordova'); + +channel.createSticky('onCordovaInfoReady'); +// Tell cordova channel to wait on the CordovaInfoReady event +channel.waitForInitialization('onCordovaInfoReady'); + + +function QRReader () { + this.available = false; + this.platform = null; + this.version = null; + this.uuid = null; + this.cordova = null; + this.model = null; + this.manufacturer = null; + this.isVirtual = null; + this.serial = null; + + var me = this; + + channel.onCordovaReady.subscribe(function () { + me.getInfo(function (info) { + // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js + // TODO: CB-5105 native implementations should not return info.cordova + var buildLabel = cordova.version; + me.available = true; + me.platform = info.platform; + me.version = info.version; + me.uuid = info.uuid; + me.cordova = buildLabel; + me.model = info.model; + me.isVirtual = info.isVirtual; + me.manufacturer = info.manufacturer || 'unknown'; + me.serial = info.serial || 'unknown'; + channel.onCordovaInfoReady.fire(); + }, function (e) { + me.available = false; + utils.alert('[ERROR] Error initializing Cordova: ' + e); + }); + }); +} +*/ + +/** + * Get device info + * + * @param {Function} successCallback The function to call when the heading data is available + * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) + */ +/* +QRReader.prototype.getInfo = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.getInfo', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); +}; + +module.exports = new QRReader(); +*/ +module.exports = {}; + +}); diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java new file mode 100644 index 000000000..29737f813 --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -0,0 +1,146 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 com.bitcoin.cordova.qrreader; + +import java.util.TimeZone; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaInterface; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.provider.Settings; + +public class QRReader extends CordovaPlugin { + public static final String TAG = "QRReader"; + + public static String platform; // Device OS + public static String uuid; // Device UUID + + private static final String ANDROID_PLATFORM = "Android"; + private static final String AMAZON_PLATFORM = "amazon-fireos"; + private static final String AMAZON_DEVICE = "Amazon"; + + + public QRReader() { + } + + + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + QRReader.uuid = getUuid(); + } + + + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + if ("getDeviceInfo".equals(action)) { + JSONObject r = new JSONObject(); + r.put("uuid", QRReader.uuid); + r.put("version", this.getOSVersion()); + r.put("platform", this.getPlatform()); + r.put("model", this.getModel()); + r.put("manufacturer", this.getManufacturer()); + r.put("isVirtual", this.isVirtual()); + r.put("serial", this.getSerialNumber()); + callbackContext.success(r); + } + else { + return false; + } + return true; + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + + public String getPlatform() { + String platform; + if (isAmazonDevice()) { + platform = AMAZON_PLATFORM; + } else { + platform = ANDROID_PLATFORM; + } + return platform; + } + + + public String getUuid() { + String uuid = Settings.Secure.getString(this.cordova.getActivity().getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); + return uuid; + } + + public String getModel() { + String model = android.os.Build.MODEL; + return model; + } + + public String getProductName() { + String productname = android.os.Build.PRODUCT; + return productname; + } + + public String getManufacturer() { + String manufacturer = android.os.Build.MANUFACTURER; + return manufacturer; + } + + public String getSerialNumber() { + String serial = android.os.Build.SERIAL; + return serial; + } + + + public String getOSVersion() { + String osversion = android.os.Build.VERSION.RELEASE; + return osversion; + } + + public String getSDKVersion() { + @SuppressWarnings("deprecation") + String sdkversion = android.os.Build.VERSION.SDK; + return sdkversion; + } + + public String getTimeZoneID() { + TimeZone tz = TimeZone.getDefault(); + return (tz.getID()); + } + + + public boolean isAmazonDevice() { + if (android.os.Build.MANUFACTURER.equals(AMAZON_DEVICE)) { + return true; + } + return false; + } + + public boolean isVirtual() { + return android.os.Build.FINGERPRINT.contains("generic") || + android.os.Build.PRODUCT.contains("sdk"); + } + +} +*/ From 776d37473ac0b9e15535c24fd3144f413de8b5dd Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 19 Feb 2019 19:43:24 +1300 Subject: [PATCH 02/65] Got test string back from JS plugin module. --- .../cordova-plugin-qrreader/www/qrreader.js | 15 +++++++++------ src/js/routes.js | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index 3616efdf4..d8020e2b8 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -30,9 +30,11 @@ var cordova = require('cordova'); channel.createSticky('onCordovaInfoReady'); // Tell cordova channel to wait on the CordovaInfoReady event channel.waitForInitialization('onCordovaInfoReady'); +*/ - -function QRReader () { +function QRReader() { + this.testString = 'hello1'; + /* this.available = false; this.platform = null; this.version = null; @@ -65,8 +67,9 @@ function QRReader () { utils.alert('[ERROR] Error initializing Cordova: ' + e); }); }); + */ } -*/ + /** * Get device info @@ -79,9 +82,9 @@ QRReader.prototype.getInfo = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.getInfo', arguments); exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); }; - -module.exports = new QRReader(); */ -module.exports = {}; +module.exports = new QRReader(); + +//module.exports = {}; }); diff --git a/src/js/routes.js b/src/js/routes.js index b76824146..6b7c92628 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1430,6 +1430,14 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr channel = "firebase"; } + //console.log('calling getInfo()...'); + //window.Device.getInfo(); + if (window.qrreader) { + console.log('qrreader present with testString:', window.qrreader.testString); + } else { + console.log('qrreader missing.'); + } + // Send a log to test var log = new window.BitAnalytics.LogEvent("wallet_opened", [{}, {}, {}], [channel, 'leanplum']); window.BitAnalytics.LogEventHandlers.postEvent(log); From 9ce22b1a4ca6155ceec10b7f5d0a1ecc609452ae Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 19 Feb 2019 20:39:16 +1300 Subject: [PATCH 03/65] Sending data back from Java. --- .../cordova-plugin-qrreader/www/qrreader.js | 10 ++++++++-- .../bitcoin/cordova/qrreader/QRReader.java | 19 ++++++++++++++----- src/js/routes.js | 9 +++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index d8020e2b8..fcd7af313 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -20,13 +20,13 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, mo * */ -/* + var argscheck = require('cordova/argscheck'); var channel = require('cordova/channel'); var utils = require('cordova/utils'); var exec = require('cordova/exec'); var cordova = require('cordova'); - +/* channel.createSticky('onCordovaInfoReady'); // Tell cordova channel to wait on the CordovaInfoReady event channel.waitForInitialization('onCordovaInfoReady'); @@ -83,6 +83,12 @@ QRReader.prototype.getInfo = function (successCallback, errorCallback) { exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); }; */ + +QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); +}; + module.exports = new QRReader(); //module.exports = {}; diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 29737f813..c83eb7a38 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -17,7 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one under the License. */ -/* + package com.bitcoin.cordova.qrreader; import java.util.TimeZone; @@ -49,13 +49,15 @@ public QRReader() { public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); - QRReader.uuid = getUuid(); + //QRReader.uuid = getUuid(); } public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - if ("getDeviceInfo".equals(action)) { + if ("getTestInfo".equals(action)) { + JSONObject r = new JSONObject(); + /* r.put("uuid", QRReader.uuid); r.put("version", this.getOSVersion()); r.put("platform", this.getPlatform()); @@ -63,6 +65,8 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo r.put("manufacturer", this.getManufacturer()); r.put("isVirtual", this.isVirtual()); r.put("serial", this.getSerialNumber()); + */ + r.put("something", this.getTestInfo()); callbackContext.success(r); } else { @@ -75,7 +79,11 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo // LOCAL METHODS //-------------------------------------------------------------------------- - + public String getTestInfo() { + return "Hello Java World 1"; + } + + /* public String getPlatform() { String platform; if (isAmazonDevice()) { @@ -141,6 +149,7 @@ public boolean isVirtual() { return android.os.Build.FINGERPRINT.contains("generic") || android.os.Build.PRODUCT.contains("sdk"); } + */ } -*/ + diff --git a/src/js/routes.js b/src/js/routes.js index 6b7c92628..62d4c7a91 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1434,6 +1434,15 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr //window.Device.getInfo(); if (window.qrreader) { console.log('qrreader present with testString:', window.qrreader.testString); + console.log('qrreader get test info.'); + window.qrreader.getTestInfo( + function onSuccess(result) { + console.log('qrreader getTestInfo() result:', result); + }, + function onError(error) { + console.error('qrreader getTestInfo() error:', error); + }); + } else { console.log('qrreader missing.'); } From 6a9c05d50cbdd8a67a3146c90f03d09e2fd59a9c Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 19 Feb 2019 20:45:38 +1300 Subject: [PATCH 04/65] Start and stop calls using Java. --- .../cordova-plugin-qrreader/www/qrreader.js | 10 ++++++++++ .../com/bitcoin/cordova/qrreader/QRReader.java | 7 +++++-- src/js/routes.js | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index fcd7af313..f2679a6f4 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -89,6 +89,16 @@ QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); }; +QRReader.prototype.startReading = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.startReading', arguments); + exec(successCallback, errorCallback, 'QRReader', 'startReading', []); +}; + +QRReader.prototype.stopReading = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.stopReading', arguments); + exec(successCallback, errorCallback, 'QRReader', 'stopReading', []); +}; + module.exports = new QRReader(); //module.exports = {}; diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index c83eb7a38..061e7619d 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -68,8 +68,11 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo */ r.put("something", this.getTestInfo()); callbackContext.success(r); - } - else { + } else if ("startReading".equals(action)) { + callbackContext.success("started"); + } else if ("stopReading".equals(action)) { + callbackContext.success("stopped"); + } else { return false; } return true; diff --git a/src/js/routes.js b/src/js/routes.js index 62d4c7a91..0575bcc00 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1442,6 +1442,22 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr function onError(error) { console.error('qrreader getTestInfo() error:', error); }); + + window.qrreader.startReading( + function onSuccess(result) { + console.log('qrreader startReading() result:', result); + }, + function onError(error) { + console.error('qrreader startReading() error:', error); + }); + + window.qrreader.stopReading( + function onSuccess(result) { + console.log('qrreader stopReading() result:', result); + }, + function onError(error) { + console.error('qrreader stopReading() error:', error); + }); } else { console.log('qrreader missing.'); From 5efdf1f30239112ff4b4a4f59ccf90af0aa18a13 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 19 Feb 2019 21:10:44 +1300 Subject: [PATCH 05/65] Plugin responding from Kotlin. --- .../bitcoin/cordova/qrreader/QRReader.java | 158 ------------------ .../com/bitcoin/cordova/qrreader/QRReader.kt | 56 +++++++ 2 files changed, 56 insertions(+), 158 deletions(-) delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java deleted file mode 100644 index 061e7619d..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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 com.bitcoin.cordova.qrreader; - -import java.util.TimeZone; - -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaInterface; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.provider.Settings; - -public class QRReader extends CordovaPlugin { - public static final String TAG = "QRReader"; - - public static String platform; // Device OS - public static String uuid; // Device UUID - - private static final String ANDROID_PLATFORM = "Android"; - private static final String AMAZON_PLATFORM = "amazon-fireos"; - private static final String AMAZON_DEVICE = "Amazon"; - - - public QRReader() { - } - - - public void initialize(CordovaInterface cordova, CordovaWebView webView) { - super.initialize(cordova, webView); - //QRReader.uuid = getUuid(); - } - - - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - if ("getTestInfo".equals(action)) { - - JSONObject r = new JSONObject(); - /* - r.put("uuid", QRReader.uuid); - r.put("version", this.getOSVersion()); - r.put("platform", this.getPlatform()); - r.put("model", this.getModel()); - r.put("manufacturer", this.getManufacturer()); - r.put("isVirtual", this.isVirtual()); - r.put("serial", this.getSerialNumber()); - */ - r.put("something", this.getTestInfo()); - callbackContext.success(r); - } else if ("startReading".equals(action)) { - callbackContext.success("started"); - } else if ("stopReading".equals(action)) { - callbackContext.success("stopped"); - } else { - return false; - } - return true; - } - - //-------------------------------------------------------------------------- - // LOCAL METHODS - //-------------------------------------------------------------------------- - - public String getTestInfo() { - return "Hello Java World 1"; - } - - /* - public String getPlatform() { - String platform; - if (isAmazonDevice()) { - platform = AMAZON_PLATFORM; - } else { - platform = ANDROID_PLATFORM; - } - return platform; - } - - - public String getUuid() { - String uuid = Settings.Secure.getString(this.cordova.getActivity().getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); - return uuid; - } - - public String getModel() { - String model = android.os.Build.MODEL; - return model; - } - - public String getProductName() { - String productname = android.os.Build.PRODUCT; - return productname; - } - - public String getManufacturer() { - String manufacturer = android.os.Build.MANUFACTURER; - return manufacturer; - } - - public String getSerialNumber() { - String serial = android.os.Build.SERIAL; - return serial; - } - - - public String getOSVersion() { - String osversion = android.os.Build.VERSION.RELEASE; - return osversion; - } - - public String getSDKVersion() { - @SuppressWarnings("deprecation") - String sdkversion = android.os.Build.VERSION.SDK; - return sdkversion; - } - - public String getTimeZoneID() { - TimeZone tz = TimeZone.getDefault(); - return (tz.getID()); - } - - - public boolean isAmazonDevice() { - if (android.os.Build.MANUFACTURER.equals(AMAZON_DEVICE)) { - return true; - } - return false; - } - - public boolean isVirtual() { - return android.os.Build.FINGERPRINT.contains("generic") || - android.os.Build.PRODUCT.contains("sdk"); - } - */ - -} - diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt new file mode 100644 index 000000000..b4fceb7a9 --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt @@ -0,0 +1,56 @@ +package com.bitcoin.cordova.qrreader + +import org.apache.cordova.CordovaWebView +import org.apache.cordova.CallbackContext +import org.apache.cordova.CordovaPlugin +import org.apache.cordova.CordovaInterface +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +import android.provider.Settings + + +class QRReader : CordovaPlugin() { + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + val testInfo: String + get() = "Hello Kotlin World 1" + + + override fun initialize(cordova: CordovaInterface, webView: CordovaWebView) { + super.initialize(cordova, webView) + } + + + @Throws(JSONException::class) + override fun execute(action: String, args: JSONArray, callbackContext: CallbackContext): Boolean { + if ("getTestInfo" == action) { + + val r = JSONObject() + + r.put("something", this.testInfo) + callbackContext.success(r) + } else if ("startReading" == action) { + callbackContext.success("started Kt") + } else if ("stopReading" == action) { + callbackContext.success("stopped Kt") + } else { + return false + } + return true + } + + companion object { + val TAG = "QRReader" + + private val ANDROID_PLATFORM = "Android" + private val AMAZON_PLATFORM = "amazon-fireos" + private val AMAZON_DEVICE = "Amazon" + } + +} + From 6330f43e899f35f3d8d1603a44314bf25ea27340 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 12:00:48 +1300 Subject: [PATCH 06/65] Getting camera permissions. --- .../bitcoin/cordova/qrreader/QRReader.java | 147 ++++++++++++++++++ .../com/bitcoin/cordova/qrreader/QRReader.kt | 56 ------- 2 files changed, 147 insertions(+), 56 deletions(-) create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java new file mode 100644 index 000000000..3aefb5e8e --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -0,0 +1,147 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 com.bitcoin.cordova.qrreader; + + + +import java.util.HashMap; +import java.util.Map; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.util.Log; +import android.view.ViewGroup; +import com.google.android.gms.vision.barcode.Barcode; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaInterface; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.provider.Settings; + +public class QRReader extends CordovaPlugin { + public static final String TAG = "QRReader"; + + public static String platform; // Device OS + public static String uuid; // Device UUID + + private static final String ANDROID_PLATFORM = "Android"; + private static final String AMAZON_PLATFORM = "amazon-fireos"; + private static final String AMAZON_DEVICE = "Amazon"; + + public static final String CAMERA = Manifest.permission.CAMERA; + public static final int CAMERA_REQ_CODE = 774980; + + + private Map mBarcodes = new HashMap(); + private CallbackContext mPermissionCallbackContext; + + public QRReader() { + } + + + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + //QRReader.uuid = getUuid(); + } + + + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + if ("getTestInfo".equals(action)) { + + JSONObject r = new JSONObject(); + r.put("something", this.getTestInfo()); + callbackContext.success(r); + + } else if ("startReading".equals(action)) { + startReading(callbackContext); + + } else if ("stopReading".equals(action)) { + callbackContext.success("stopped"); + } else { + return false; + } + return true; + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + private void getCameraPermission(CallbackContext callbackContext) { + mPermissionCallbackContext = callbackContext; + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + } + + public String getTestInfo() { + return "Hello Java World 1"; + } + + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException + { + Log.d(TAG, "onRequestPermissionResult()"); + + if (requestCode == CAMERA_REQ_CODE) { + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + if (this.mPermissionCallbackContext != null) { + this.mPermissionCallbackContext.error("Camera permission denied."); + } + return; + } + } + if (this.mPermissionCallbackContext != null) { + startReadingWithPermission(mPermissionCallbackContext); + } + } + + } + + private void startReading(CallbackContext callbackContext) { + Log.d(TAG, "startReading()"); + + if(cordova.hasPermission(CAMERA)) + { + startReadingWithPermission(callbackContext); + } + else + { + getCameraPermission(callbackContext); + } + } + + private void startReadingWithPermission(CallbackContext callbackContext) { + Log.d(TAG, "startReadingWithPermission()"); + + ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); + if (viewGroup == null) { + callbackContext.error("Failed to get view group."); + return; + } + callbackContext.success("Got view group."); + } + +} + diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt deleted file mode 100644 index b4fceb7a9..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.bitcoin.cordova.qrreader - -import org.apache.cordova.CordovaWebView -import org.apache.cordova.CallbackContext -import org.apache.cordova.CordovaPlugin -import org.apache.cordova.CordovaInterface -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -import android.provider.Settings - - -class QRReader : CordovaPlugin() { - - //-------------------------------------------------------------------------- - // LOCAL METHODS - //-------------------------------------------------------------------------- - - val testInfo: String - get() = "Hello Kotlin World 1" - - - override fun initialize(cordova: CordovaInterface, webView: CordovaWebView) { - super.initialize(cordova, webView) - } - - - @Throws(JSONException::class) - override fun execute(action: String, args: JSONArray, callbackContext: CallbackContext): Boolean { - if ("getTestInfo" == action) { - - val r = JSONObject() - - r.put("something", this.testInfo) - callbackContext.success(r) - } else if ("startReading" == action) { - callbackContext.success("started Kt") - } else if ("stopReading" == action) { - callbackContext.success("stopped Kt") - } else { - return false - } - return true - } - - companion object { - val TAG = "QRReader" - - private val ANDROID_PLATFORM = "Android" - private val AMAZON_PLATFORM = "amazon-fireos" - private val AMAZON_DEVICE = "Amazon" - } - -} - From fd9394cc458a54ce8d3d494eb6203ee5a03f4a96 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 12:02:23 +1300 Subject: [PATCH 07/65] Extra plugin files. --- .../cordova/qrreader/BarcodeMapTracker.java | 91 ++ .../qrreader/BarcodeMapTrackerFactory.java | 29 + .../qrreader/BarcodeUpdateListener.java | 13 + .../cordova/qrreader/CameraSource.java | 1200 +++++++++++++++++ .../cordova/qrreader/CameraSourcePreview.java | 180 +++ 5 files changed, 1513 insertions(+) create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java create mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java new file mode 100644 index 000000000..811c835de --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java @@ -0,0 +1,91 @@ +package com.bitcoin.cordova.qrreader; + + +import android.content.Context; +import android.util.Log; + + +import com.google.android.gms.vision.Detector; +import com.google.android.gms.vision.Tracker; +import com.google.android.gms.vision.barcode.Barcode; + +import java.util.HashMap; +import java.util.Map; + +/** + * Generic tracker which is used for tracking or reading a barcode (and can really be used for + * any type of item). This is used to receive newly detected items, add a graphical representation + * to an overlay, update the graphics as the item changes, and remove the graphics when the item + * goes away. + */ +public class BarcodeMapTracker extends Tracker { + + private Map mBarcodes; + private Integer mId; + private BarcodeUpdateListener mBarcodeUpdateListener; + + + + BarcodeMapTracker(Map barcodes, Context context) { + this.mBarcodes = barcodes; + //this.mOverlay = mOverlay; + //this.mGraphic = mGraphic; + if (context instanceof BarcodeUpdateListener) { + this.mBarcodeUpdateListener = (BarcodeUpdateListener) context; + } else { + throw new RuntimeException("Hosting activity must implement BarcodeUpdateListener"); + } + } + + /** + * Start tracking the detected item instance within the item overlay. + */ + @Override + public void onNewItem(int id, Barcode item) { + //mGraphic.setId(id); + mId = id; + mBarcodes.put(id, item); + Log.d("BarcodeGraphicTracker", "New barcode."); + mBarcodeUpdateListener.onBarcodeDetected(item); + } + + /** + * Update the position/characteristics of the item within the overlay. + */ + @Override + public void onUpdate(Detector.Detections detectionResults, Barcode item) { + //mOverlay.add(mGraphic); + //mGraphic.updateItem(item); + if (mId != null) { + mBarcodes.put(mId, item); + } + } + + /** + * Hide the graphic when the corresponding object was not detected. This can happen for + * intermediate frames temporarily, for example if the object was momentarily blocked from + * view. + */ + @Override + public void onMissing(Detector.Detections detectionResults) { + //mOverlay.remove(mGraphic); + if (mId != null) { + mBarcodes.remove(mId); + mId = null; + } + } + + /** + * Called when the item is assumed to be gone for good. Remove the graphic annotation from + * the overlay. + */ + @Override + public void onDone() { + + //mOverlay.remove(mGraphic); + if (mId != null) { + mBarcodes.remove(mId); + mId = null; + } + } +} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java new file mode 100644 index 000000000..bed541367 --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java @@ -0,0 +1,29 @@ +package com.bitcoin.cordova.qrreader; + +import android.content.Context; +import com.google.android.gms.vision.MultiProcessor; +import com.google.android.gms.vision.Tracker; +import com.google.android.gms.vision.barcode.Barcode; + +import java.util.Map; + +/** + * Factory for creating a tracker and associated graphic to be associated with a new barcode. The + * multi-processor uses this factory to create barcode trackers as needed -- one for each barcode. + */ +class BarcodeMapTrackerFactory implements MultiProcessor.Factory { + private Map mBarcodes; + private Context mContext; + + public BarcodeMapTrackerFactory(Map mBarcodes, + Context mContext) { + this.mBarcodes = mBarcodes; + this.mContext = mContext; + } + + @Override + public Tracker create(Barcode barcode) { + return new BarcodeMapTracker(mBarcodes, mContext); + } + +} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java new file mode 100644 index 000000000..e32f618d3 --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java @@ -0,0 +1,13 @@ +package com.bitcoin.cordova.qrreader; + +import android.support.annotation.UiThread; +import com.google.android.gms.vision.barcode.Barcode; + +/** + * Consume the item instance detected from an Activity or Fragment level by implementing the + * BarcodeUpdateListener interface method onBarcodeDetected. + */ +interface BarcodeUpdateListener { + @UiThread + void onBarcodeDetected(Barcode barcode); +} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java new file mode 100644 index 000000000..b8e4edb8b --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java @@ -0,0 +1,1200 @@ +package com.bitcoin.cordova.qrreader; + + +import android.Manifest; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.os.Build; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresPermission; +import android.support.annotation.StringDef; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.WindowManager; + +import com.google.android.gms.common.images.Size; +import com.google.android.gms.vision.Detector; +import com.google.android.gms.vision.Frame; + +import java.io.IOException; +import java.lang.Thread.State; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Note: This requires Google Play Services 8.1 or higher, due to using indirect byte buffers for +// storing images. + +/** + * Manages the camera in conjunction with an underlying + * {@link com.google.android.gms.vision.Detector}. This receives preview frames from the camera at + * a specified rate, sending those frames to the detector as fast as it is able to process those + * frames. + *

+ * This camera source makes a best effort to manage processing on preview frames as fast as + * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector + * is unable to keep up with the rate of frames generated by the camera. You should use + * {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate that works well with + * the capabilities of the camera hardware and the detector options that you have selected. If CPU + * utilization is higher than you'd like, then you may want to consider reducing FPS. If the camera + * preview or detector results are too "jerky", then you may want to consider increasing FPS. + *

+ * The following Android permission is required to use the camera: + *

    + *
  • android.permissions.CAMERA
  • + *
+ */ +@SuppressWarnings("deprecation") +public class CameraSource { + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK; + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT; + + private static final String TAG = "OpenCameraSource"; + + /** + * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL + * context, we can choose any ID we want here. + */ + private static final int DUMMY_TEXTURE_NAME = 100; + + /** + * If the absolute difference between a preview size aspect ratio and a picture size aspect + * ratio is less than this tolerance, they are considered to be the same aspect ratio. + */ + private static final float ASPECT_RATIO_TOLERANCE = 0.01f; + + @StringDef({ + Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, + Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, + Camera.Parameters.FOCUS_MODE_AUTO, + Camera.Parameters.FOCUS_MODE_EDOF, + Camera.Parameters.FOCUS_MODE_FIXED, + Camera.Parameters.FOCUS_MODE_INFINITY, + Camera.Parameters.FOCUS_MODE_MACRO + }) + @Retention(RetentionPolicy.SOURCE) + private @interface FocusMode {} + + @StringDef({ + Camera.Parameters.FLASH_MODE_ON, + Camera.Parameters.FLASH_MODE_OFF, + Camera.Parameters.FLASH_MODE_AUTO, + Camera.Parameters.FLASH_MODE_RED_EYE, + Camera.Parameters.FLASH_MODE_TORCH + }) + @Retention(RetentionPolicy.SOURCE) + private @interface FlashMode {} + + private Context mContext; + + private final Object mCameraLock = new Object(); + + // Guarded by mCameraLock + private Camera mCamera; + + private int mFacing = CAMERA_FACING_BACK; + + /** + * Rotation of the device, and thus the associated preview images captured from the device. + * See {@link Frame.Metadata#getRotation()}. + */ + private int mRotation; + + private Size mPreviewSize; + + // These values may be requested by the caller. Due to hardware limitations, we may need to + // select close, but not exactly the same values for these. + private float mRequestedFps = 30.0f; + private int mRequestedPreviewWidth = 1024; + private int mRequestedPreviewHeight = 768; + + + private String mFocusMode = null; + private String mFlashMode = null; + + // These instances need to be held onto to avoid GC of their underlying resources. Even though + // these aren't used outside of the method that creates them, they still must have hard + // references maintained to them. + private SurfaceView mDummySurfaceView; + private SurfaceTexture mDummySurfaceTexture; + + /** + * Dedicated thread and associated runnable for calling into the detector with frames, as the + * frames become available from the camera. + */ + private Thread mProcessingThread; + private FrameProcessingRunnable mFrameProcessor; + + /** + * Map to convert between a byte array, received from the camera, and its associated byte + * buffer. We use byte buffers internally because this is a more efficient way to call into + * native code later (avoids a potential copy). + */ + private Map mBytesToByteBuffer = new HashMap(); + + //============================================================================================== + // Builder + //============================================================================================== + + /** + * Builder for configuring and creating an associated camera source. + */ + public static class Builder { + private final Detector mDetector; + private CameraSource mCameraSource = new CameraSource(); + + /** + * Creates a camera source builder with the supplied context and detector. Camera preview + * images will be streamed to the associated detector upon starting the camera source. + */ + public Builder(Context context, Detector detector) { + if (context == null) { + throw new IllegalArgumentException("No context supplied."); + } + if (detector == null) { + throw new IllegalArgumentException("No detector supplied."); + } + + mDetector = detector; + mCameraSource.mContext = context; + } + + /** + * Sets the requested frame rate in frames per second. If the exact requested value is not + * not available, the best matching available value is selected. Default: 30. + */ + public Builder setRequestedFps(float fps) { + if (fps <= 0) { + throw new IllegalArgumentException("Invalid fps: " + fps); + } + mCameraSource.mRequestedFps = fps; + return this; + } + + public Builder setFocusMode(@FocusMode String mode) { + mCameraSource.mFocusMode = mode; + return this; + } + + public Builder setFlashMode(@FlashMode String mode) { + mCameraSource.mFlashMode = mode; + return this; + } + + /** + * Sets the desired width and height of the camera frames in pixels. If the exact desired + * values are not available options, the best matching available options are selected. + * Also, we try to select a preview size which corresponds to the aspect ratio of an + * associated full picture size, if applicable. Default: 1024x768. + */ + public Builder setRequestedPreviewSize(int width, int height) { + // Restrict the requested range to something within the realm of possibility. The + // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that + // devices can support. We bound this to avoid int overflow in the code later. + final int MAX = 1000000; + if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) { + throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height); + } + mCameraSource.mRequestedPreviewWidth = width; + mCameraSource.mRequestedPreviewHeight = height; + return this; + } + + /** + * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or + * {@link #CAMERA_FACING_FRONT}). Default: back facing. + */ + public Builder setFacing(int facing) { + if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) { + throw new IllegalArgumentException("Invalid camera: " + facing); + } + mCameraSource.mFacing = facing; + return this; + } + + /** + * Creates an instance of the camera source. + */ + public CameraSource build() { + mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector); + return mCameraSource; + } + } + + //============================================================================================== + // Bridge Functionality for the Camera1 API + //============================================================================================== + + /** + * Callback interface used to signal the moment of actual image capture. + */ + public interface ShutterCallback { + /** + * Called as near as possible to the moment when a photo is captured from the sensor. This + * is a good opportunity to play a shutter sound or give other feedback of camera operation. + * This may be some time after the photo was triggered, but some time before the actual data + * is available. + */ + void onShutter(); + } + + /** + * Callback interface used to supply image data from a photo capture. + */ + public interface PictureCallback { + /** + * Called when image data is available after a picture is taken. The format of the data + * is a jpeg binary. + */ + void onPictureTaken(byte[] data); + } + + /** + * Callback interface used to notify on completion of camera auto focus. + */ + public interface AutoFocusCallback { + /** + * Called when the camera auto focus completes. If the camera + * does not support auto-focus and autoFocus is called, + * onAutoFocus will be called immediately with a fake value of + * success set to true. + *

+ * The auto-focus routine does not lock auto-exposure and auto-white + * balance after it completes. + * + * @param success true if focus was successful, false if otherwise + */ + void onAutoFocus(boolean success); + } + + /** + * Callback interface used to notify on auto focus start and stop. + *

+ *

This is only supported in continuous autofocus modes -- {@link + * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link + * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show + * autofocus animation based on this.

+ */ + public interface AutoFocusMoveCallback { + /** + * Called when the camera auto focus starts or stops. + * + * @param start true if focus starts to move, false if focus stops to move + */ + void onAutoFocusMoving(boolean start); + } + + //============================================================================================== + // Public + //============================================================================================== + + /** + * Stops the camera and releases the resources of the camera and underlying detector. + */ + public void release() { + synchronized (mCameraLock) { + stop(); + mFrameProcessor.release(); + } + } + + /** + * Opens the camera and starts sending preview frames to the underlying detector. The preview + * frames are not displayed. + * + * @throws IOException if the camera's preview texture or display could not be initialized + */ + @RequiresPermission(Manifest.permission.CAMERA) + public CameraSource start() throws IOException { + synchronized (mCameraLock) { + if (mCamera != null) { + return this; + } + + mCamera = createCamera(); + + // SurfaceTexture was introduced in Honeycomb (11), so if we are running and + // old version of Android. fall back to use SurfaceView. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); + mCamera.setPreviewTexture(mDummySurfaceTexture); + } else { + mDummySurfaceView = new SurfaceView(mContext); + mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); + } + mCamera.startPreview(); + + mProcessingThread = new Thread(mFrameProcessor); + mFrameProcessor.setActive(true); + mProcessingThread.start(); + } + return this; + } + + /** + * Opens the camera and starts sending preview frames to the underlying detector. The supplied + * surface holder is used for the preview so frames can be displayed to the user. + * + * @param surfaceHolder the surface holder to use for the preview frames + * @throws IOException if the supplied surface holder could not be used as the preview display + */ + @RequiresPermission(Manifest.permission.CAMERA) + public CameraSource start(SurfaceHolder surfaceHolder) throws IOException { + synchronized (mCameraLock) { + if (mCamera != null) { + return this; + } + + mCamera = createCamera(); + mCamera.setPreviewDisplay(surfaceHolder); + mCamera.startPreview(); + + mProcessingThread = new Thread(mFrameProcessor); + mFrameProcessor.setActive(true); + mProcessingThread.start(); + } + return this; + } + + /** + * Closes the camera and stops sending frames to the underlying frame detector. + *

+ * This camera source may be restarted again by calling {@link #start()} or + * {@link #start(SurfaceHolder)}. + *

+ * Call {@link #release()} instead to completely shut down this camera source and release the + * resources of the underlying detector. + */ + public void stop() { + synchronized (mCameraLock) { + mFrameProcessor.setActive(false); + if (mProcessingThread != null) { + try { + // Wait for the thread to complete to ensure that we can't have multiple threads + // executing at the same time (i.e., which would happen if we called start too + // quickly after stop). + mProcessingThread.join(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing thread interrupted on release."); + } + mProcessingThread = null; + } + + // clear the buffer to prevent oom exceptions + mBytesToByteBuffer.clear(); + + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.setPreviewCallbackWithBuffer(null); + try { + // We want to be compatible back to Gingerbread, but SurfaceTexture + // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the + // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't + // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mCamera.setPreviewTexture(null); + + } else { + mCamera.setPreviewDisplay(null); + } + } catch (Exception e) { + Log.e(TAG, "Failed to clear camera preview: " + e); + } + mCamera.release(); + mCamera = null; + } + } + } + + /** + * Returns the preview size that is currently in use by the underlying camera. + */ + public Size getPreviewSize() { + return mPreviewSize; + } + + /** + * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or + * {@link #CAMERA_FACING_FRONT}. + */ + public int getCameraFacing() { + return mFacing; + } + + public int doZoom(float scale) { + synchronized (mCameraLock) { + if (mCamera == null) { + return 0; + } + int currentZoom = 0; + int maxZoom; + Camera.Parameters parameters = mCamera.getParameters(); + if (!parameters.isZoomSupported()) { + Log.w(TAG, "Zoom is not supported on this device"); + return currentZoom; + } + maxZoom = parameters.getMaxZoom(); + + currentZoom = parameters.getZoom() + 1; + float newZoom; + if (scale > 1) { + newZoom = currentZoom + scale * (maxZoom / 10); + } else { + newZoom = currentZoom * scale; + } + currentZoom = Math.round(newZoom) - 1; + if (currentZoom < 0) { + currentZoom = 0; + } else if (currentZoom > maxZoom) { + currentZoom = maxZoom; + } + parameters.setZoom(currentZoom); + mCamera.setParameters(parameters); + return currentZoom; + } + } + + /** + * Initiates taking a picture, which happens asynchronously. The camera source should have been + * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera + * preview is suspended while the picture is being taken, but will resume once picture taking is + * done. + * + * @param shutter the callback for image capture moment, or null + * @param jpeg the callback for JPEG image data, or null + */ + public void takePicture(ShutterCallback shutter, PictureCallback jpeg) { + synchronized (mCameraLock) { + if (mCamera != null) { + PictureStartCallback startCallback = new PictureStartCallback(); + startCallback.mDelegate = shutter; + PictureDoneCallback doneCallback = new PictureDoneCallback(); + doneCallback.mDelegate = jpeg; + mCamera.takePicture(startCallback, null, null, doneCallback); + } + } + } + + /** + * Gets the current focus mode setting. + * + * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link + * #autoFocus(AutoFocusCallback)} to start the focus if focus + * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO. + * @see Camera.Parameters#FOCUS_MODE_AUTO + * @see Camera.Parameters#FOCUS_MODE_INFINITY + * @see Camera.Parameters#FOCUS_MODE_MACRO + * @see Camera.Parameters#FOCUS_MODE_FIXED + * @see Camera.Parameters#FOCUS_MODE_EDOF + * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO + * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE + */ + @Nullable + @FocusMode + public String getFocusMode() { + return mFocusMode; + } + + /** + * Sets the focus mode. + * + * @param mode the focus mode + * @return {@code true} if the focus mode is set, {@code false} otherwise + * @see #getFocusMode() + */ + public boolean setFocusMode(@FocusMode String mode) { + synchronized (mCameraLock) { + if (mCamera != null && mode != null) { + Camera.Parameters parameters = mCamera.getParameters(); + if (parameters.getSupportedFocusModes().contains(mode)) { + parameters.setFocusMode(mode); + mCamera.setParameters(parameters); + mFocusMode = mode; + return true; + } + } + + return false; + } + } + + /** + * Gets the current flash mode setting. + * + * @return current flash mode. null if flash mode setting is not + * supported or the camera is not yet created. + * @see Camera.Parameters#FLASH_MODE_OFF + * @see Camera.Parameters#FLASH_MODE_AUTO + * @see Camera.Parameters#FLASH_MODE_ON + * @see Camera.Parameters#FLASH_MODE_RED_EYE + * @see Camera.Parameters#FLASH_MODE_TORCH + */ + @Nullable + @FlashMode + public String getFlashMode() { + return mFlashMode; + } + + /** + * Sets the flash mode. + * + * @param mode flash mode. + * @return {@code true} if the flash mode is set, {@code false} otherwise + * @see #getFlashMode() + */ + public boolean setFlashMode(@FlashMode String mode) { + synchronized (mCameraLock) { + if (mCamera != null && mode != null) { + Camera.Parameters parameters = mCamera.getParameters(); + if (parameters.getSupportedFlashModes().contains(mode)) { + parameters.setFlashMode(mode); + mCamera.setParameters(parameters); + mFlashMode = mode; + return true; + } + } + + return false; + } + } + + /** + * Starts camera auto-focus and registers a callback function to run when + * the camera is focused. This method is only valid when preview is active + * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}). + *

+ *

Callers should check + * {@link #getFocusMode()} to determine if + * this method should be called. If the camera does not support auto-focus, + * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)} + * callback will be called immediately. + *

+ *

If the current flash mode is not + * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be + * fired during auto-focus, depending on the driver and camera hardware.

+ * + * @param cb the callback to run + * @see #cancelAutoFocus() + */ + public void autoFocus(@Nullable AutoFocusCallback cb) { + synchronized (mCameraLock) { + if (mCamera != null) { + CameraAutoFocusCallback autoFocusCallback = null; + if (cb != null) { + autoFocusCallback = new CameraAutoFocusCallback(); + autoFocusCallback.mDelegate = cb; + } + mCamera.autoFocus(autoFocusCallback); + } + } + } + + /** + * Cancels any auto-focus function in progress. + * Whether or not auto-focus is currently in progress, + * this function will return the focus position to the default. + * If the camera does not support auto-focus, this is a no-op. + * + * @see #autoFocus(AutoFocusCallback) + */ + public void cancelAutoFocus() { + synchronized (mCameraLock) { + if (mCamera != null) { + mCamera.cancelAutoFocus(); + } + } + } + + /** + * Sets camera auto-focus move callback. + * + * @param cb the callback to run + * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + return false; + } + + synchronized (mCameraLock) { + if (mCamera != null) { + CameraAutoFocusMoveCallback autoFocusMoveCallback = null; + if (cb != null) { + autoFocusMoveCallback = new CameraAutoFocusMoveCallback(); + autoFocusMoveCallback.mDelegate = cb; + } + mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback); + } + } + + return true; + } + + //============================================================================================== + // Private + //============================================================================================== + + /** + * Only allow creation via the builder class. + */ + private CameraSource() { + } + + /** + * Wraps the camera1 shutter callback so that the deprecated API isn't exposed. + */ + private class PictureStartCallback implements Camera.ShutterCallback { + private ShutterCallback mDelegate; + + @Override + public void onShutter() { + if (mDelegate != null) { + mDelegate.onShutter(); + } + } + } + + /** + * Wraps the final callback in the camera sequence, so that we can automatically turn the camera + * preview back on after the picture has been taken. + */ + private class PictureDoneCallback implements Camera.PictureCallback { + private PictureCallback mDelegate; + + @Override + public void onPictureTaken(byte[] data, Camera camera) { + if (mDelegate != null) { + mDelegate.onPictureTaken(data); + } + synchronized (mCameraLock) { + if (mCamera != null) { + mCamera.startPreview(); + } + } + } + } + + /** + * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed. + */ + private class CameraAutoFocusCallback implements Camera.AutoFocusCallback { + private AutoFocusCallback mDelegate; + + @Override + public void onAutoFocus(boolean success, Camera camera) { + if (mDelegate != null) { + mDelegate.onAutoFocus(success); + } + } + } + + /** + * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback { + private AutoFocusMoveCallback mDelegate; + + @Override + public void onAutoFocusMoving(boolean start, Camera camera) { + if (mDelegate != null) { + mDelegate.onAutoFocusMoving(start); + } + } + } + + /** + * Opens the camera and applies the user settings. + * + * @throws RuntimeException if the method fails + */ + @SuppressLint("InlinedApi") + private Camera createCamera() { + int requestedCameraId = getIdForRequestedCamera(mFacing); + if (requestedCameraId == -1) { + throw new RuntimeException("Could not find requested camera."); + } + Camera camera = Camera.open(requestedCameraId); + + SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight); + if (sizePair == null) { + throw new RuntimeException("Could not find suitable preview size."); + } + Size pictureSize = sizePair.pictureSize(); + mPreviewSize = sizePair.previewSize(); + + int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps); + if (previewFpsRange == null) { + throw new RuntimeException("Could not find suitable preview frames per second range."); + } + + Camera.Parameters parameters = camera.getParameters(); + + if (pictureSize != null) { + parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); + } + + parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + parameters.setPreviewFpsRange( + previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + parameters.setPreviewFormat(ImageFormat.NV21); + + setRotation(camera, parameters, requestedCameraId); + + if (mFocusMode != null) { + if (parameters.getSupportedFocusModes().contains( + mFocusMode)) { + parameters.setFocusMode(mFocusMode); + } else { + Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device."); + } + } + + // setting mFocusMode to the one set in the params + mFocusMode = parameters.getFocusMode(); + + if (mFlashMode != null) { + if (parameters.getSupportedFlashModes() != null) { + if (parameters.getSupportedFlashModes().contains( + mFlashMode)) { + parameters.setFlashMode(mFlashMode); + } else { + Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device."); + } + } + } + + // setting mFlashMode to the one set in the params + mFlashMode = parameters.getFlashMode(); + + camera.setParameters(parameters); + + // Four frame buffers are needed for working with the camera: + // + // one for the frame that is currently being executed upon in doing detection + // one for the next pending frame to process immediately upon completing detection + // two for the frames that the camera uses to populate future preview images + camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + + return camera; + } + + /** + * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such + * camera was found. + * + * @param facing the desired camera (front-facing or rear-facing) + */ + private static int getIdForRequestedCamera(int facing) { + CameraInfo cameraInfo = new CameraInfo(); + for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == facing) { + return i; + } + } + return -1; + } + + /** + * Selects the most suitable preview and picture size, given the desired width and height. + *

+ * Even though we may only need the preview size, it's necessary to find both the preview + * size and the picture size of the camera together, because these need to have the same aspect + * ratio. On some hardware, if you would only set the preview size, you will get a distorted + * image. + * + * @param camera the camera to select a preview size from + * @param desiredWidth the desired width of the camera preview frames + * @param desiredHeight the desired height of the camera preview frames + * @return the selected preview and picture size pair + */ + private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { + List validPreviewSizes = generateValidPreviewSizeList(camera); + + // The method for selecting the best size is to minimize the sum of the differences between + // the desired values and the actual values for width and height. This is certainly not the + // only way to select the best size, but it provides a decent tradeoff between using the + // closest aspect ratio vs. using the closest pixel area. + SizePair selectedPair = null; + int minDiff = Integer.MAX_VALUE; + for (SizePair sizePair : validPreviewSizes) { + Size size = sizePair.previewSize(); + int diff = Math.abs(size.getWidth() - desiredWidth) + + Math.abs(size.getHeight() - desiredHeight); + if (diff < minDiff) { + selectedPair = sizePair; + minDiff = diff; + } + } + + return selectedPair; + } + + /** + * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted + * preview images on some devices, the picture size must be set to a size that is the same + * aspect ratio as the preview size or the preview may end up being distorted. If the picture + * size is null, then there is no picture size with the same aspect ratio as the preview size. + */ + private static class SizePair { + private Size mPreview; + private Size mPicture; + + public SizePair(android.hardware.Camera.Size previewSize, + android.hardware.Camera.Size pictureSize) { + mPreview = new Size(previewSize.width, previewSize.height); + if (pictureSize != null) { + mPicture = new Size(pictureSize.width, pictureSize.height); + } + } + + public Size previewSize() { + return mPreview; + } + + @SuppressWarnings("unused") + public Size pictureSize() { + return mPicture; + } + } + + /** + * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is + * not a corresponding picture size of the same aspect ratio. If there is a corresponding + * picture size of the same aspect ratio, the picture size is paired up with the preview size. + *

+ * This is necessary because even if we don't use still pictures, the still picture size must be + * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the + * preview images may be distorted on some devices. + */ + private static List generateValidPreviewSizeList(Camera camera) { + Camera.Parameters parameters = camera.getParameters(); + List supportedPreviewSizes = + parameters.getSupportedPreviewSizes(); + List supportedPictureSizes = + parameters.getSupportedPictureSizes(); + List validPreviewSizes = new ArrayList(); + for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { + float previewAspectRatio = (float) previewSize.width / (float) previewSize.height; + + // By looping through the picture sizes in order, we favor the higher resolutions. + // We choose the highest resolution in order to support taking the full resolution + // picture later. + for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) { + float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height; + if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { + validPreviewSizes.add(new SizePair(previewSize, pictureSize)); + break; + } + } + } + + // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all + // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we + // still account for it. + if (validPreviewSizes.size() == 0) { + Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size"); + for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { + // The null picture size will let us know that we shouldn't set a picture size. + validPreviewSizes.add(new SizePair(previewSize, null)); + } + } + + return validPreviewSizes; + } + + /** + * Selects the most suitable preview frames per second range, given the desired frames per + * second. + * + * @param camera the camera to select a frames per second range from + * @param desiredPreviewFps the desired frames per second for the camera preview frames + * @return the selected preview frames per second range + */ + private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) { + // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame + // rates. + int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f); + + // The method for selecting the best range is to minimize the sum of the differences between + // the desired value and the upper and lower bounds of the range. This may select a range + // that the desired value is outside of, but this is often preferred. For example, if the + // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the + // range (15, 30). + int[] selectedFpsRange = null; + int minDiff = Integer.MAX_VALUE; + List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange(); + for (int[] range : previewFpsRangeList) { + int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; + int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; + int diff = Math.abs(deltaMin) + Math.abs(deltaMax); + if (diff < minDiff) { + selectedFpsRange = range; + minDiff = diff; + } + } + return selectedFpsRange; + } + + /** + * Calculates the correct rotation for the given camera id and sets the rotation in the + * parameters. It also sets the camera's display orientation and rotation. + * + * @param parameters the camera parameters for which to set the rotation + * @param cameraId the camera id to set rotation based on + */ + private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) { + WindowManager windowManager = + (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + int degrees = 0; + int rotation = windowManager.getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + Log.e(TAG, "Bad rotation value: " + rotation); + } + + CameraInfo cameraInfo = new CameraInfo(); + Camera.getCameraInfo(cameraId, cameraInfo); + + int angle; + int displayAngle; + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + angle = (cameraInfo.orientation + degrees) % 360; + displayAngle = (360 - angle) % 360; // compensate for it being mirrored + } else { // back-facing + angle = (cameraInfo.orientation - degrees + 360) % 360; + displayAngle = angle; + } + + // This corresponds to the rotation constants in {@link Frame}. + mRotation = angle / 90; + + camera.setDisplayOrientation(displayAngle); + parameters.setRotation(angle); + } + + /** + * Creates one buffer for the camera preview callback. The size of the buffer is based off of + * the camera preview size and the format of the camera image. + * + * @return a new preview buffer of the appropriate size for the current camera settings + */ + private byte[] createPreviewBuffer(Size previewSize) { + int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); + long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; + int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1; + + // + // NOTICE: This code only works when using play services v. 8.1 or higher. + // + + // Creating the byte array this way and wrapping it, as opposed to using .allocate(), + // should guarantee that there will be an array to work with. + byte[] byteArray = new byte[bufferSize]; + ByteBuffer buffer = ByteBuffer.wrap(byteArray); + if (!buffer.hasArray() || (buffer.array() != byteArray)) { + // I don't think that this will ever happen. But if it does, then we wouldn't be + // passing the preview content to the underlying detector later. + throw new IllegalStateException("Failed to create valid buffer for camera source."); + } + + mBytesToByteBuffer.put(byteArray, buffer); + return byteArray; + } + + //============================================================================================== + // Frame processing + //============================================================================================== + + /** + * Called when the camera has a new preview frame. + */ + private class CameraPreviewCallback implements Camera.PreviewCallback { + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + mFrameProcessor.setNextFrame(data, camera); + } + } + + /** + * This runnable controls access to the underlying receiver, calling it to process frames when + * available from the camera. This is designed to run detection on frames as fast as possible + * (i.e., without unnecessary context switching or waiting on the next frame). + *

+ * While detection is running on a frame, new frames may be received from the camera. As these + * frames come in, the most recent frame is held onto as pending. As soon as detection and its + * associated processing are done for the previous frame, detection on the mostly recently + * received frame will immediately start on the same thread. + */ + private class FrameProcessingRunnable implements Runnable { + private Detector mDetector; + private long mStartTimeMillis = SystemClock.elapsedRealtime(); + + // This lock guards all of the member variables below. + private final Object mLock = new Object(); + private boolean mActive = true; + + // These pending variables hold the state associated with the new frame awaiting processing. + private long mPendingTimeMillis; + private int mPendingFrameId = 0; + private ByteBuffer mPendingFrameData; + + FrameProcessingRunnable(Detector detector) { + mDetector = detector; + } + + /** + * Releases the underlying receiver. This is only safe to do after the associated thread + * has completed, which is managed in camera source's release method above. + */ + @SuppressLint("Assert") + void release() { + assert (mProcessingThread.getState() == State.TERMINATED); + mDetector.release(); + mDetector = null; + } + + /** + * Marks the runnable as active/not active. Signals any blocked threads to continue. + */ + void setActive(boolean active) { + synchronized (mLock) { + mActive = active; + mLock.notifyAll(); + } + } + + /** + * Sets the frame data received from the camera. This adds the previous unused frame buffer + * (if present) back to the camera, and keeps a pending reference to the frame data for + * future use. + */ + void setNextFrame(byte[] data, Camera camera) { + synchronized (mLock) { + if (mPendingFrameData != null) { + camera.addCallbackBuffer(mPendingFrameData.array()); + mPendingFrameData = null; + } + + if (!mBytesToByteBuffer.containsKey(data)) { + Log.d(TAG, + "Skipping frame. Could not find ByteBuffer associated with the image " + + "data from the camera."); + return; + } + + // Timestamp and frame ID are maintained here, which will give downstream code some + // idea of the timing of frames received and when frames were dropped along the way. + mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis; + mPendingFrameId++; + mPendingFrameData = mBytesToByteBuffer.get(data); + + // Notify the processor thread if it is waiting on the next frame (see below). + mLock.notifyAll(); + } + } + + /** + * As long as the processing thread is active, this executes detection on frames + * continuously. The next pending frame is either immediately available or hasn't been + * received yet. Once it is available, we transfer the frame info to local variables and + * run detection on that frame. It immediately loops back for the next frame without + * pausing. + *

+ * If detection takes longer than the time in between new frames from the camera, this will + * mean that this loop will run without ever waiting on a frame, avoiding any context + * switching or frame acquisition time latency. + *

+ * If you find that this is using more CPU than you'd like, you should probably decrease the + * FPS setting above to allow for some idle time in between frames. + */ + @Override + public void run() { + Frame outputFrame; + ByteBuffer data; + + while (true) { + synchronized (mLock) { + while (mActive && (mPendingFrameData == null)) { + try { + // Wait for the next frame to be received from the camera, since we + // don't have it yet. + mLock.wait(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing loop terminated.", e); + return; + } + } + + if (!mActive) { + // Exit the loop once this camera source is stopped or released. We check + // this here, immediately after the wait() above, to handle the case where + // setActive(false) had been called, triggering the termination of this + // loop. + return; + } + + outputFrame = new Frame.Builder() + .setImageData(mPendingFrameData, mPreviewSize.getWidth(), + mPreviewSize.getHeight(), ImageFormat.NV21) + .setId(mPendingFrameId) + .setTimestampMillis(mPendingTimeMillis) + .setRotation(mRotation) + .build(); + + // Hold onto the frame data locally, so that we can use this for detection + // below. We need to clear mPendingFrameData to ensure that this buffer isn't + // recycled back to the camera before we are done using that data. + data = mPendingFrameData; + mPendingFrameData = null; + } + + // The code below needs to run outside of synchronization, because this will allow + // the camera to add pending frame(s) while we are running detection on the current + // frame. + + try { + mDetector.receiveFrame(outputFrame); + } catch (Throwable t) { + Log.e(TAG, "Exception thrown from receiver.", t); + } finally { + mCamera.addCallbackBuffer(data.array()); + } + } + } + } +} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java new file mode 100644 index 000000000..de7b49d27 --- /dev/null +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java @@ -0,0 +1,180 @@ +package com.bitcoin.cordova.qrreader; + + +import android.Manifest; +import android.content.Context; +import android.content.res.Configuration; +import android.support.annotation.RequiresPermission; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.google.android.gms.common.images.Size; + +import java.io.IOException; + +public class CameraSourcePreview extends ViewGroup { + private static final String TAG = "CameraSourcePreview"; + + private Context mContext; + private SurfaceView mSurfaceView; + private boolean mStartRequested; + private boolean mSurfaceAvailable; + private CameraSource mCameraSource; + + //private GraphicOverlay mOverlay; + + public CameraSourcePreview(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mStartRequested = false; + mSurfaceAvailable = false; + + mSurfaceView = new SurfaceView(context); + mSurfaceView.getHolder().addCallback(new SurfaceCallback()); + addView(mSurfaceView); + } + + @RequiresPermission(Manifest.permission.CAMERA) + public void start(CameraSource cameraSource) throws IOException, SecurityException { + if (cameraSource == null) { + stop(); + } + + mCameraSource = cameraSource; + + if (mCameraSource != null) { + mStartRequested = true; + startIfReady(); + } + } + + /* + @RequiresPermission(Manifest.permission.CAMERA) + public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException { + mOverlay = overlay; + start(cameraSource); + } + */ + + public void stop() { + if (mCameraSource != null) { + mCameraSource.stop(); + } + } + + public void release() { + if (mCameraSource != null) { + mCameraSource.release(); + mCameraSource = null; + } + } + + @RequiresPermission(Manifest.permission.CAMERA) + private void startIfReady() throws IOException, SecurityException { + if (mStartRequested && mSurfaceAvailable) { + mCameraSource.start(mSurfaceView.getHolder()); + /* + if (mOverlay != null) { + Size size = mCameraSource.getPreviewSize(); + int min = Math.min(size.getWidth(), size.getHeight()); + int max = Math.max(size.getWidth(), size.getHeight()); + if (isPortraitMode()) { + // Swap width and height sizes when in portrait, since it will be rotated by + // 90 degrees + mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing()); + } else { + mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing()); + } + mOverlay.clear(); + } + */ + mStartRequested = false; + } + } + + private class SurfaceCallback implements SurfaceHolder.Callback { + @Override + public void surfaceCreated(SurfaceHolder surface) { + mSurfaceAvailable = true; + try { + startIfReady(); + } catch (SecurityException se) { + Log.e(TAG,"Do not have permission to start the camera", se); + } catch (IOException e) { + Log.e(TAG, "Could not start camera source.", e); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder surface) { + mSurfaceAvailable = false; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = 320; + int height = 240; + if (mCameraSource != null) { + Size size = mCameraSource.getPreviewSize(); + if (size != null) { + width = size.getWidth(); + height = size.getHeight(); + } + } + + // Swap width and height sizes when in portrait, since it will be rotated 90 degrees + if (isPortraitMode()) { + int tmp = width; + //noinspection SuspiciousNameCombination + width = height; + height = tmp; + } + + final int layoutWidth = right - left; + final int layoutHeight = bottom - top; + + // Computes height and width for potentially doing fit width. + int childWidth = layoutWidth; + int childHeight = (int)(((float) layoutWidth / (float) width) * height); + + // If height is too tall using fit width, does fit height instead. + if (childHeight > layoutHeight) { + childHeight = layoutHeight; + childWidth = (int)(((float) layoutHeight / (float) height) * width); + } + + for (int i = 0; i < getChildCount(); ++i) { + getChildAt(i).layout(0, 0, childWidth, childHeight); + } + + try { + startIfReady(); + } catch (SecurityException se) { + Log.e(TAG,"Do not have permission to start the camera", se); + } catch (IOException e) { + Log.e(TAG, "Could not start camera source.", e); + } + } + + private boolean isPortraitMode() { + int orientation = mContext.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return false; + } + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + return true; + } + + Log.d(TAG, "isPortraitMode returning false by default"); + return false; + } +} + From 2abdd36d84336ebdcb2d8ddee98677f8cb55856e Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 12:30:24 +1300 Subject: [PATCH 08/65] Created camera source. --- .../bitcoin/cordova/qrreader/QRReader.java | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 3aefb5e8e..653399a1b 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -26,10 +26,20 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.Map; import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.hardware.Camera; +import android.os.Build; import android.util.Log; import android.view.ViewGroup; + +import com.google.android.gms.vision.MultiProcessor; import com.google.android.gms.vision.barcode.Barcode; +import com.google.android.gms.vision.barcode.BarcodeDetector; + import org.apache.cordova.CordovaWebView; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; @@ -39,6 +49,7 @@ Licensed to the Apache Software Foundation (ASF) under one import org.json.JSONObject; import android.provider.Settings; +import android.widget.Toast; public class QRReader extends CordovaPlugin { public static final String TAG = "QRReader"; @@ -55,6 +66,7 @@ public class QRReader extends CordovaPlugin { private Map mBarcodes = new HashMap(); + private CameraSource mCameraSource; private CallbackContext mPermissionCallbackContext; public QRReader() { @@ -89,6 +101,76 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo // LOCAL METHODS //-------------------------------------------------------------------------- + /** + * Creates and starts the camera. Note that this uses a higher resolution in comparison + * to other detection examples to enable the barcode detector to detect small barcodes + * at long distances. + * + * Suppressing InlinedApi since there is a check that the minimum version is met before using + * the constant. + */ + @SuppressLint("InlinedApi") + private Boolean createCameraSource(Context context, boolean useFlash, CallbackContext callbackContext) { + + boolean autoFocus = true; + // A barcode detector is created to track barcodes. An associated multi-processor instance + // is set to receive the barcode detection results, track the barcodes, and maintain + // graphics for each barcode on screen. The factory is used by the multi-processor to + // create a separate tracker instance for each barcode. + BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); + BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, context); + barcodeDetector.setProcessor( + new MultiProcessor.Builder(barcodeFactory).build()); + + if (!barcodeDetector.isOperational()) { + // Note: The first time that an app using the barcode or face API is installed on a + // device, GMS will download a native libraries to the device in order to do detection. + // Usually this completes before the app is run for the first time. But if that + // download has not yet completed, then the above call will not detect any barcodes + // and/or faces. + // + // isOperational() can be used to check if the required native libraries are currently + // available. The detectors will automatically become operational once the library + // downloads complete on device. + Log.w(TAG, "Detector dependencies are not yet available."); + callbackContext.error("Detector dependencies are not yet available."); + return false; + + + /* TODO: Handle this better later? + // Check for low storage. If there is low storage, the native library will not be + // downloaded, so detection will not become operational. + IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); + boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null; + + if (hasLowStorage) { + Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show(); + Log.w(TAG, "Low storage error."); + } + */ + } + + // Creates and starts the camera. Note that this uses a higher resolution in comparison + // to other detection examples to enable the barcode detector to detect small barcodes + // at long distances. + CameraSource.Builder builder = new CameraSource.Builder(context, barcodeDetector) + .setFacing(CameraSource.CAMERA_FACING_BACK) + .setRequestedPreviewSize(1600, 1024) + .setRequestedFps(15.0f); + + // make sure that auto focus is an available option + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + builder = builder.setFocusMode( + autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); + } + + mCameraSource = builder + .setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null) + .build(); + + return true; + } + private void getCameraPermission(CallbackContext callbackContext) { mPermissionCallbackContext = callbackContext; cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); @@ -140,7 +222,20 @@ private void startReadingWithPermission(CallbackContext callbackContext) { callbackContext.error("Failed to get view group."); return; } - callbackContext.success("Got view group."); + + + Context context = cordova.getActivity().getApplicationContext(); + if (mCameraSource == null) { + if (!createCameraSource(context, false, callbackContext)) { + return; + } else { + callbackContext.success("Created camera source."); + } + } else { + callbackContext.success("Have existing camera source."); + } + + } } From f2045b6097dee8a37dbb40a7640c28c3871a1f9f Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 15:20:24 +1300 Subject: [PATCH 09/65] Showing the preview in the Scan tab. --- .../cordova/qrreader/CameraSourcePreview.java | 11 ++ .../bitcoin/cordova/qrreader/QRReader.java | 105 +++++++++++++++++- src/js/controllers/tab-scan.controller.js | 38 ++++++- src/js/routes.js | 5 +- www/views/tab-scan.html | 4 +- 5 files changed, 153 insertions(+), 10 deletions(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java index de7b49d27..be82f3176 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java @@ -26,6 +26,17 @@ public class CameraSourcePreview extends ViewGroup { //private GraphicOverlay mOverlay; + public CameraSourcePreview(Context context) { + super(context); + mContext = context; + mStartRequested = false; + mSurfaceAvailable = false; + + mSurfaceView = new SurfaceView(context); + mSurfaceView.getHolder().addCallback(new SurfaceCallback()); + addView(mSurfaceView); + } + public CameraSourcePreview(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 653399a1b..de90240b8 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -22,20 +22,28 @@ Licensed to the Apache Software Foundation (ASF) under one +import java.io.IOException; import java.util.HashMap; import java.util.Map; import android.Manifest; import android.annotation.SuppressLint; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.graphics.Color; import android.hardware.Camera; import android.os.Build; +import android.util.AttributeSet; import android.util.Log; +import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.vision.MultiProcessor; import com.google.android.gms.vision.barcode.Barcode; import com.google.android.gms.vision.barcode.BarcodeDetector; @@ -49,6 +57,8 @@ Licensed to the Apache Software Foundation (ASF) under one import org.json.JSONObject; import android.provider.Settings; +import android.widget.Button; +import android.widget.FrameLayout; import android.widget.Toast; public class QRReader extends CordovaPlugin { @@ -64,9 +74,13 @@ public class QRReader extends CordovaPlugin { public static final String CAMERA = Manifest.permission.CAMERA; public static final int CAMERA_REQ_CODE = 774980; + // intent request code to handle updating play services if needed. + private static final int RC_HANDLE_GMS = 9001; + private Map mBarcodes = new HashMap(); private CameraSource mCameraSource; + private CameraSourcePreview mCameraSourcePreview; private CallbackContext mPermissionCallbackContext; public QRReader() { @@ -201,6 +215,40 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } + /** + * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet + * (e.g., because onResume was called before the camera source was created), this will be called + * again when the camera source is created. + */ + private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { + // check that the device has play services available. + int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); + if (code != ConnectionResult.SUCCESS) { + Dialog dlg = + GoogleApiAvailability.getInstance().getErrorDialog(cordova.getActivity(), code, RC_HANDLE_GMS); + dlg.show(); + callbackContext.error("Google Play services is unavailable."); + return false; + } + + if (mCameraSource != null) { + try { + mCameraSourcePreview.start(mCameraSource); + } catch (IOException e) { + Log.e(TAG, "Unable to start camera source.", e); + mCameraSource.release(); + mCameraSource = null; + callbackContext.error("Unable to start camera source. " + e.getMessage()); + return false; + } + } else { + Log.e(TAG, "No camera source to start."); + callbackContext.error("No camera source to start."); + return false; + } + return true; + } + private void startReading(CallbackContext callbackContext) { Log.d(TAG, "startReading()"); @@ -214,27 +262,74 @@ private void startReading(CallbackContext callbackContext) { } } - private void startReadingWithPermission(CallbackContext callbackContext) { + private void startReadingWithPermission(final CallbackContext callbackContext) { Log.d(TAG, "startReadingWithPermission()"); - ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); + final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); if (viewGroup == null) { callbackContext.error("Failed to get view group."); return; } - Context context = cordova.getActivity().getApplicationContext(); + final Context context = cordova.getActivity().getApplicationContext(); if (mCameraSource == null) { if (!createCameraSource(context, false, callbackContext)) { return; } else { - callbackContext.success("Created camera source."); + //callbackContext.success("Created camera source."); } } else { - callbackContext.success("Have existing camera source."); + //callbackContext.success("Have existing camera source."); } + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + + // What about coming through here subsequently? + mCameraSourcePreview = new CameraSourcePreview(context); + + //FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); + + //viewGroup.addView(mCameraSourcePreview, layoutParams); + + FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(400, 400, Gravity.CENTER); + FrameLayout.LayoutParams matchParentLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); + View testView; + //View testView = new View(context); + //testView = new Button(context); + //((Button) testView).setText("The Button"); + //testView.setBackgroundColor(Color.argb(1, 1, 0, 0)); + testView = mCameraSourcePreview; + + + viewGroup.addView(testView, sizedLayout); + + //webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); + //cameraPreviewing = true; + //webView.getView().bringToFront(); + //mCameraSourcePreview.bringToFront(); + //testView.bringToFront(); + webView.getView().bringToFront(); + viewGroup.bringChildToFront(testView); + + Boolean cameraStarted = false; + try { + cameraStarted = startCameraSource(context, callbackContext); + } catch (SecurityException e) { + Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); + callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); + return; + } + + //testView.bringToFront(); + if (cameraStarted) { + callbackContext.success("Added view."); + } + } + }); + } diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 4e152ca57..c0e3cd3d7 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -27,6 +27,8 @@ angular loading: 'loading', visible: 'visible' }; + + $scope.onStart = onStart; $scope.scannerStates = scannerStates; function _updateCapabilities(){ @@ -42,6 +44,7 @@ angular function _handleCapabilities(){ // always update the view + /* $timeout(function(){ if(!scannerService.isInitialized()){ $scope.currentState = scannerStates.loading; @@ -56,6 +59,7 @@ angular } $log.debug('Scan view state set to: ' + $scope.currentState); }); + */ } function _refreshScanView(){ @@ -68,16 +72,28 @@ angular // This could be much cleaner with a Promise API // (needs a polyfill for some platforms) + /* $rootScope.$on('scannerServiceInitialized', function(){ $log.debug('Scanner initialization finished, reinitializing scan view...'); _refreshScanView(); }); + */ $scope.$on("$ionicView.enter", function(event, data) { $ionicNavBarDelegate.showBar(true); }); $scope.$on("$ionicView.afterEnter", function() { + $scope.currentState = scannerStates.hasPermission; + console.log('Starting qrreader.'); + window.qrreader.startReading( + function onSuccess(result) { + console.log('qrreader startReading() result:', result); + }, + function onError(error) { + console.error('qrreader startReading() error:', error); + }); + /* var capabilities = scannerService.getCapabilities(); if (capabilities.hasPermission) { // try initializing and refreshing status any time the view is entered @@ -87,9 +103,11 @@ angular activate(); } } + */ }); function activate(){ + /* scannerService.activate(function(){ _updateCapabilities(); _handleCapabilities(); @@ -111,20 +129,25 @@ angular scannerService.resumePreview(); }); }); + */ } $scope.activate = activate; $scope.authorize = function(){ + /* scannerService.initialize(function(){ _refreshScanView(); }); + */ }; $scope.$on("$ionicView.beforeLeave", function() { - scannerService.deactivate(); + //scannerService.deactivate(); + window.qrreader.stopReading(); }); function handleSuccessfulScan(contents){ + /* $log.debug('Scan returned: "' + contents + '"'); scannerService.pausePreview(); // Sometimes (testing in Chrome, when reading QR Code) data is an object @@ -141,31 +164,41 @@ angular scannerService.resumePreview(); } }); + */ } $rootScope.$on('incomingDataMenu.menuHidden', function() { activate(); }); + function onStart() { + $scope.currentState = scannerStates.hasPermission; + } + $scope.openSettings = function(){ - scannerService.openSettings(); + //scannerService.openSettings(); }; $scope.reactivationCount = 0; $scope.attemptToReactivate = function(){ + /* scannerService.reinitialize(function(){ $scope.reactivationCount++; }); + */ }; $scope.toggleLight = function(){ + /* scannerService.toggleLight(function(lightEnabled){ $scope.lightActive = lightEnabled; $scope.$apply(); }); + */ }; $scope.toggleCamera = function(){ + /* $scope.cameraToggleActive = true; scannerService.toggleCamera(function(status){ // (a short delay for the user to see the visual feedback) @@ -175,6 +208,7 @@ angular $log.debug('Camera toggle control deactivated.'); }, 200); }); + */ }; $scope.canGoBack = function(){ diff --git a/src/js/routes.js b/src/js/routes.js index 0575bcc00..35a6df5c9 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1433,6 +1433,8 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr //console.log('calling getInfo()...'); //window.Device.getInfo(); if (window.qrreader) { + console.log('qrreader found.'); + /* console.log('qrreader present with testString:', window.qrreader.testString); console.log('qrreader get test info.'); window.qrreader.getTestInfo( @@ -1458,10 +1460,11 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr function onError(error) { console.error('qrreader stopReading() error:', error); }); - + */ } else { console.log('qrreader missing.'); } + // Send a log to test var log = new window.BitAnalytics.LogEvent("wallet_opened", [{}, {}, {}], [channel, 'leanplum']); diff --git a/www/views/tab-scan.html b/www/views/tab-scan.html index 10a20799c..dad49537b 100644 --- a/www/views/tab-scan.html +++ b/www/views/tab-scan.html @@ -18,9 +18,9 @@

Enable the camera to get started.
Enable camera access in your device settings to get started.
Please connect a camera to get started.
- + - +
From 8a0e51a6aaf6fac4d918ae4dcfdda2f8aac8ac43 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 15:57:03 +1300 Subject: [PATCH 10/65] Scanning now works. --- .../cordova/qrreader/BarcodeMapTracker.java | 12 +++---- .../qrreader/BarcodeMapTrackerFactory.java | 8 ++--- .../bitcoin/cordova/qrreader/QRReader.java | 35 ++++++++++++------- src/js/controllers/tab-scan.controller.js | 12 ++++--- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java index 811c835de..cefdc54f4 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java @@ -26,15 +26,11 @@ public class BarcodeMapTracker extends Tracker { - BarcodeMapTracker(Map barcodes, Context context) { + BarcodeMapTracker(Map barcodes, BarcodeUpdateListener listener) { this.mBarcodes = barcodes; //this.mOverlay = mOverlay; //this.mGraphic = mGraphic; - if (context instanceof BarcodeUpdateListener) { - this.mBarcodeUpdateListener = (BarcodeUpdateListener) context; - } else { - throw new RuntimeException("Hosting activity must implement BarcodeUpdateListener"); - } + this.mBarcodeUpdateListener = listener; } /** @@ -46,7 +42,9 @@ public void onNewItem(int id, Barcode item) { mId = id; mBarcodes.put(id, item); Log.d("BarcodeGraphicTracker", "New barcode."); - mBarcodeUpdateListener.onBarcodeDetected(item); + if (mBarcodeUpdateListener != null) { + mBarcodeUpdateListener.onBarcodeDetected(item); + } } /** diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java index bed541367..e1626d6e3 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java @@ -13,17 +13,17 @@ */ class BarcodeMapTrackerFactory implements MultiProcessor.Factory { private Map mBarcodes; - private Context mContext; + private BarcodeUpdateListener mListener; public BarcodeMapTrackerFactory(Map mBarcodes, - Context mContext) { + BarcodeUpdateListener listener) { this.mBarcodes = mBarcodes; - this.mContext = mContext; + this.mListener = listener; } @Override public Tracker create(Barcode barcode) { - return new BarcodeMapTracker(mBarcodes, mContext); + return new BarcodeMapTracker(mBarcodes, mListener); } } diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index de90240b8..c1d92778f 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -61,7 +61,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.widget.FrameLayout; import android.widget.Toast; -public class QRReader extends CordovaPlugin { +public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static final String TAG = "QRReader"; public static String platform; // Device OS @@ -81,7 +81,7 @@ public class QRReader extends CordovaPlugin { private Map mBarcodes = new HashMap(); private CameraSource mCameraSource; private CameraSourcePreview mCameraSourcePreview; - private CallbackContext mPermissionCallbackContext; + private CallbackContext mStartCallbackContext; public QRReader() { } @@ -111,9 +111,17 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo return true; } - //-------------------------------------------------------------------------- - // LOCAL METHODS - //-------------------------------------------------------------------------- + @Override + public void onBarcodeDetected(Barcode barcode) { + String contents = barcode.rawValue; + Log.d(TAG, "Detected new barcode."); + if (mStartCallbackContext != null) { + mStartCallbackContext.success(contents); + } else { + Log.e(TAG, "No callback context when detecting new barcode."); + } + } + /** * Creates and starts the camera. Note that this uses a higher resolution in comparison @@ -132,7 +140,7 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo // graphics for each barcode on screen. The factory is used by the multi-processor to // create a separate tracker instance for each barcode. BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); - BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, context); + BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, this); barcodeDetector.setProcessor( new MultiProcessor.Builder(barcodeFactory).build()); @@ -186,7 +194,7 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo } private void getCameraPermission(CallbackContext callbackContext) { - mPermissionCallbackContext = callbackContext; + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); } @@ -202,14 +210,14 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, if (requestCode == CAMERA_REQ_CODE) { for (int r : grantResults) { if (r == PackageManager.PERMISSION_DENIED) { - if (this.mPermissionCallbackContext != null) { - this.mPermissionCallbackContext.error("Camera permission denied."); + if (this.mStartCallbackContext != null) { + this.mStartCallbackContext.error("Camera permission denied."); } return; } } - if (this.mPermissionCallbackContext != null) { - startReadingWithPermission(mPermissionCallbackContext); + if (this.mStartCallbackContext != null) { + startReadingWithPermission(mStartCallbackContext); } } @@ -251,6 +259,7 @@ private Boolean startCameraSource(Context context, CallbackContext callbackConte private void startReading(CallbackContext callbackContext) { Log.d(TAG, "startReading()"); + mStartCallbackContext = callbackContext; if(cordova.hasPermission(CAMERA)) { @@ -294,7 +303,7 @@ public void run() { //viewGroup.addView(mCameraSourcePreview, layoutParams); - FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(400, 400, Gravity.CENTER); + FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(800, 800, Gravity.CENTER); FrameLayout.LayoutParams matchParentLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); View testView; //View testView = new View(context); @@ -324,9 +333,11 @@ public void run() { } //testView.bringToFront(); + /* // Send barcode instead if (cameraStarted) { callbackContext.success("Added view."); } + */ } }); diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index c0e3cd3d7..74e6e3c3b 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -89,6 +89,8 @@ angular window.qrreader.startReading( function onSuccess(result) { console.log('qrreader startReading() result:', result); + + handleSuccessfulScan(result); }, function onError(error) { console.error('qrreader startReading() error:', error); @@ -147,9 +149,9 @@ angular }); function handleSuccessfulScan(contents){ - /* + $log.debug('Scan returned: "' + contents + '"'); - scannerService.pausePreview(); + //scannerService.pausePreview(); // Sometimes (testing in Chrome, when reading QR Code) data is an object // that has a string data.result. contents = contents.result || contents; @@ -158,13 +160,13 @@ angular var title = gettextCatalog.getString('Scan Failed'); popupService.showAlert(title, err.message, function onAlertShown() { // Enable another scan since we won't receive incomingDataMenu.menuHidden - activate(); + //activate(); }); } else { - scannerService.resumePreview(); + //scannerService.resumePreview(); } }); - */ + } $rootScope.$on('incomingDataMenu.menuHidden', function() { From 0cd93c2dc5d3679e75dc6446ec5cf1ec2970a61f Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 19:16:29 +1300 Subject: [PATCH 11/65] Added source code for cordova-plugin-qrreader. --- .../cordova-plugin-qrreader/LICENSE | 202 +++ .../cordova-plugin-qrreader/plugin.xml | 47 + .../src/android/BarcodeMapTracker.java | 89 ++ .../src/android/BarcodeMapTrackerFactory.java | 29 + .../src/android/BarcodeUpdateListener.java | 13 + .../src/android/CameraSource.java | 1200 +++++++++++++++++ .../src/android/CameraSourcePreview.java | 191 +++ .../src/android/QRReader.java | 348 +++++ .../src/android/qrreader.gradle | 4 + .../cordova-plugin-qrreader/www/qrreader.js | 106 ++ 10 files changed, 2229 insertions(+) create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/LICENSE create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE b/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml new file mode 100644 index 000000000..1d35c7ce7 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -0,0 +1,47 @@ + + + + + QR Reader + Cordova QR Code Reader Plugin + Apache 2.0 + cordova,qrcode + + + + + + + + + + + + + + + + + + diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java new file mode 100644 index 000000000..cefdc54f4 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java @@ -0,0 +1,89 @@ +package com.bitcoin.cordova.qrreader; + + +import android.content.Context; +import android.util.Log; + + +import com.google.android.gms.vision.Detector; +import com.google.android.gms.vision.Tracker; +import com.google.android.gms.vision.barcode.Barcode; + +import java.util.HashMap; +import java.util.Map; + +/** + * Generic tracker which is used for tracking or reading a barcode (and can really be used for + * any type of item). This is used to receive newly detected items, add a graphical representation + * to an overlay, update the graphics as the item changes, and remove the graphics when the item + * goes away. + */ +public class BarcodeMapTracker extends Tracker { + + private Map mBarcodes; + private Integer mId; + private BarcodeUpdateListener mBarcodeUpdateListener; + + + + BarcodeMapTracker(Map barcodes, BarcodeUpdateListener listener) { + this.mBarcodes = barcodes; + //this.mOverlay = mOverlay; + //this.mGraphic = mGraphic; + this.mBarcodeUpdateListener = listener; + } + + /** + * Start tracking the detected item instance within the item overlay. + */ + @Override + public void onNewItem(int id, Barcode item) { + //mGraphic.setId(id); + mId = id; + mBarcodes.put(id, item); + Log.d("BarcodeGraphicTracker", "New barcode."); + if (mBarcodeUpdateListener != null) { + mBarcodeUpdateListener.onBarcodeDetected(item); + } + } + + /** + * Update the position/characteristics of the item within the overlay. + */ + @Override + public void onUpdate(Detector.Detections detectionResults, Barcode item) { + //mOverlay.add(mGraphic); + //mGraphic.updateItem(item); + if (mId != null) { + mBarcodes.put(mId, item); + } + } + + /** + * Hide the graphic when the corresponding object was not detected. This can happen for + * intermediate frames temporarily, for example if the object was momentarily blocked from + * view. + */ + @Override + public void onMissing(Detector.Detections detectionResults) { + //mOverlay.remove(mGraphic); + if (mId != null) { + mBarcodes.remove(mId); + mId = null; + } + } + + /** + * Called when the item is assumed to be gone for good. Remove the graphic annotation from + * the overlay. + */ + @Override + public void onDone() { + + //mOverlay.remove(mGraphic); + if (mId != null) { + mBarcodes.remove(mId); + mId = null; + } + } +} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java new file mode 100644 index 000000000..e1626d6e3 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java @@ -0,0 +1,29 @@ +package com.bitcoin.cordova.qrreader; + +import android.content.Context; +import com.google.android.gms.vision.MultiProcessor; +import com.google.android.gms.vision.Tracker; +import com.google.android.gms.vision.barcode.Barcode; + +import java.util.Map; + +/** + * Factory for creating a tracker and associated graphic to be associated with a new barcode. The + * multi-processor uses this factory to create barcode trackers as needed -- one for each barcode. + */ +class BarcodeMapTrackerFactory implements MultiProcessor.Factory { + private Map mBarcodes; + private BarcodeUpdateListener mListener; + + public BarcodeMapTrackerFactory(Map mBarcodes, + BarcodeUpdateListener listener) { + this.mBarcodes = mBarcodes; + this.mListener = listener; + } + + @Override + public Tracker create(Barcode barcode) { + return new BarcodeMapTracker(mBarcodes, mListener); + } + +} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java new file mode 100644 index 000000000..e32f618d3 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java @@ -0,0 +1,13 @@ +package com.bitcoin.cordova.qrreader; + +import android.support.annotation.UiThread; +import com.google.android.gms.vision.barcode.Barcode; + +/** + * Consume the item instance detected from an Activity or Fragment level by implementing the + * BarcodeUpdateListener interface method onBarcodeDetected. + */ +interface BarcodeUpdateListener { + @UiThread + void onBarcodeDetected(Barcode barcode); +} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java new file mode 100644 index 000000000..b8e4edb8b --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java @@ -0,0 +1,1200 @@ +package com.bitcoin.cordova.qrreader; + + +import android.Manifest; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.os.Build; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresPermission; +import android.support.annotation.StringDef; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.WindowManager; + +import com.google.android.gms.common.images.Size; +import com.google.android.gms.vision.Detector; +import com.google.android.gms.vision.Frame; + +import java.io.IOException; +import java.lang.Thread.State; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Note: This requires Google Play Services 8.1 or higher, due to using indirect byte buffers for +// storing images. + +/** + * Manages the camera in conjunction with an underlying + * {@link com.google.android.gms.vision.Detector}. This receives preview frames from the camera at + * a specified rate, sending those frames to the detector as fast as it is able to process those + * frames. + *

+ * This camera source makes a best effort to manage processing on preview frames as fast as + * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector + * is unable to keep up with the rate of frames generated by the camera. You should use + * {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate that works well with + * the capabilities of the camera hardware and the detector options that you have selected. If CPU + * utilization is higher than you'd like, then you may want to consider reducing FPS. If the camera + * preview or detector results are too "jerky", then you may want to consider increasing FPS. + *

+ * The following Android permission is required to use the camera: + *

    + *
  • android.permissions.CAMERA
  • + *
+ */ +@SuppressWarnings("deprecation") +public class CameraSource { + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK; + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT; + + private static final String TAG = "OpenCameraSource"; + + /** + * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL + * context, we can choose any ID we want here. + */ + private static final int DUMMY_TEXTURE_NAME = 100; + + /** + * If the absolute difference between a preview size aspect ratio and a picture size aspect + * ratio is less than this tolerance, they are considered to be the same aspect ratio. + */ + private static final float ASPECT_RATIO_TOLERANCE = 0.01f; + + @StringDef({ + Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, + Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, + Camera.Parameters.FOCUS_MODE_AUTO, + Camera.Parameters.FOCUS_MODE_EDOF, + Camera.Parameters.FOCUS_MODE_FIXED, + Camera.Parameters.FOCUS_MODE_INFINITY, + Camera.Parameters.FOCUS_MODE_MACRO + }) + @Retention(RetentionPolicy.SOURCE) + private @interface FocusMode {} + + @StringDef({ + Camera.Parameters.FLASH_MODE_ON, + Camera.Parameters.FLASH_MODE_OFF, + Camera.Parameters.FLASH_MODE_AUTO, + Camera.Parameters.FLASH_MODE_RED_EYE, + Camera.Parameters.FLASH_MODE_TORCH + }) + @Retention(RetentionPolicy.SOURCE) + private @interface FlashMode {} + + private Context mContext; + + private final Object mCameraLock = new Object(); + + // Guarded by mCameraLock + private Camera mCamera; + + private int mFacing = CAMERA_FACING_BACK; + + /** + * Rotation of the device, and thus the associated preview images captured from the device. + * See {@link Frame.Metadata#getRotation()}. + */ + private int mRotation; + + private Size mPreviewSize; + + // These values may be requested by the caller. Due to hardware limitations, we may need to + // select close, but not exactly the same values for these. + private float mRequestedFps = 30.0f; + private int mRequestedPreviewWidth = 1024; + private int mRequestedPreviewHeight = 768; + + + private String mFocusMode = null; + private String mFlashMode = null; + + // These instances need to be held onto to avoid GC of their underlying resources. Even though + // these aren't used outside of the method that creates them, they still must have hard + // references maintained to them. + private SurfaceView mDummySurfaceView; + private SurfaceTexture mDummySurfaceTexture; + + /** + * Dedicated thread and associated runnable for calling into the detector with frames, as the + * frames become available from the camera. + */ + private Thread mProcessingThread; + private FrameProcessingRunnable mFrameProcessor; + + /** + * Map to convert between a byte array, received from the camera, and its associated byte + * buffer. We use byte buffers internally because this is a more efficient way to call into + * native code later (avoids a potential copy). + */ + private Map mBytesToByteBuffer = new HashMap(); + + //============================================================================================== + // Builder + //============================================================================================== + + /** + * Builder for configuring and creating an associated camera source. + */ + public static class Builder { + private final Detector mDetector; + private CameraSource mCameraSource = new CameraSource(); + + /** + * Creates a camera source builder with the supplied context and detector. Camera preview + * images will be streamed to the associated detector upon starting the camera source. + */ + public Builder(Context context, Detector detector) { + if (context == null) { + throw new IllegalArgumentException("No context supplied."); + } + if (detector == null) { + throw new IllegalArgumentException("No detector supplied."); + } + + mDetector = detector; + mCameraSource.mContext = context; + } + + /** + * Sets the requested frame rate in frames per second. If the exact requested value is not + * not available, the best matching available value is selected. Default: 30. + */ + public Builder setRequestedFps(float fps) { + if (fps <= 0) { + throw new IllegalArgumentException("Invalid fps: " + fps); + } + mCameraSource.mRequestedFps = fps; + return this; + } + + public Builder setFocusMode(@FocusMode String mode) { + mCameraSource.mFocusMode = mode; + return this; + } + + public Builder setFlashMode(@FlashMode String mode) { + mCameraSource.mFlashMode = mode; + return this; + } + + /** + * Sets the desired width and height of the camera frames in pixels. If the exact desired + * values are not available options, the best matching available options are selected. + * Also, we try to select a preview size which corresponds to the aspect ratio of an + * associated full picture size, if applicable. Default: 1024x768. + */ + public Builder setRequestedPreviewSize(int width, int height) { + // Restrict the requested range to something within the realm of possibility. The + // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that + // devices can support. We bound this to avoid int overflow in the code later. + final int MAX = 1000000; + if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) { + throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height); + } + mCameraSource.mRequestedPreviewWidth = width; + mCameraSource.mRequestedPreviewHeight = height; + return this; + } + + /** + * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or + * {@link #CAMERA_FACING_FRONT}). Default: back facing. + */ + public Builder setFacing(int facing) { + if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) { + throw new IllegalArgumentException("Invalid camera: " + facing); + } + mCameraSource.mFacing = facing; + return this; + } + + /** + * Creates an instance of the camera source. + */ + public CameraSource build() { + mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector); + return mCameraSource; + } + } + + //============================================================================================== + // Bridge Functionality for the Camera1 API + //============================================================================================== + + /** + * Callback interface used to signal the moment of actual image capture. + */ + public interface ShutterCallback { + /** + * Called as near as possible to the moment when a photo is captured from the sensor. This + * is a good opportunity to play a shutter sound or give other feedback of camera operation. + * This may be some time after the photo was triggered, but some time before the actual data + * is available. + */ + void onShutter(); + } + + /** + * Callback interface used to supply image data from a photo capture. + */ + public interface PictureCallback { + /** + * Called when image data is available after a picture is taken. The format of the data + * is a jpeg binary. + */ + void onPictureTaken(byte[] data); + } + + /** + * Callback interface used to notify on completion of camera auto focus. + */ + public interface AutoFocusCallback { + /** + * Called when the camera auto focus completes. If the camera + * does not support auto-focus and autoFocus is called, + * onAutoFocus will be called immediately with a fake value of + * success set to true. + *

+ * The auto-focus routine does not lock auto-exposure and auto-white + * balance after it completes. + * + * @param success true if focus was successful, false if otherwise + */ + void onAutoFocus(boolean success); + } + + /** + * Callback interface used to notify on auto focus start and stop. + *

+ *

This is only supported in continuous autofocus modes -- {@link + * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link + * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show + * autofocus animation based on this.

+ */ + public interface AutoFocusMoveCallback { + /** + * Called when the camera auto focus starts or stops. + * + * @param start true if focus starts to move, false if focus stops to move + */ + void onAutoFocusMoving(boolean start); + } + + //============================================================================================== + // Public + //============================================================================================== + + /** + * Stops the camera and releases the resources of the camera and underlying detector. + */ + public void release() { + synchronized (mCameraLock) { + stop(); + mFrameProcessor.release(); + } + } + + /** + * Opens the camera and starts sending preview frames to the underlying detector. The preview + * frames are not displayed. + * + * @throws IOException if the camera's preview texture or display could not be initialized + */ + @RequiresPermission(Manifest.permission.CAMERA) + public CameraSource start() throws IOException { + synchronized (mCameraLock) { + if (mCamera != null) { + return this; + } + + mCamera = createCamera(); + + // SurfaceTexture was introduced in Honeycomb (11), so if we are running and + // old version of Android. fall back to use SurfaceView. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); + mCamera.setPreviewTexture(mDummySurfaceTexture); + } else { + mDummySurfaceView = new SurfaceView(mContext); + mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); + } + mCamera.startPreview(); + + mProcessingThread = new Thread(mFrameProcessor); + mFrameProcessor.setActive(true); + mProcessingThread.start(); + } + return this; + } + + /** + * Opens the camera and starts sending preview frames to the underlying detector. The supplied + * surface holder is used for the preview so frames can be displayed to the user. + * + * @param surfaceHolder the surface holder to use for the preview frames + * @throws IOException if the supplied surface holder could not be used as the preview display + */ + @RequiresPermission(Manifest.permission.CAMERA) + public CameraSource start(SurfaceHolder surfaceHolder) throws IOException { + synchronized (mCameraLock) { + if (mCamera != null) { + return this; + } + + mCamera = createCamera(); + mCamera.setPreviewDisplay(surfaceHolder); + mCamera.startPreview(); + + mProcessingThread = new Thread(mFrameProcessor); + mFrameProcessor.setActive(true); + mProcessingThread.start(); + } + return this; + } + + /** + * Closes the camera and stops sending frames to the underlying frame detector. + *

+ * This camera source may be restarted again by calling {@link #start()} or + * {@link #start(SurfaceHolder)}. + *

+ * Call {@link #release()} instead to completely shut down this camera source and release the + * resources of the underlying detector. + */ + public void stop() { + synchronized (mCameraLock) { + mFrameProcessor.setActive(false); + if (mProcessingThread != null) { + try { + // Wait for the thread to complete to ensure that we can't have multiple threads + // executing at the same time (i.e., which would happen if we called start too + // quickly after stop). + mProcessingThread.join(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing thread interrupted on release."); + } + mProcessingThread = null; + } + + // clear the buffer to prevent oom exceptions + mBytesToByteBuffer.clear(); + + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.setPreviewCallbackWithBuffer(null); + try { + // We want to be compatible back to Gingerbread, but SurfaceTexture + // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the + // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't + // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mCamera.setPreviewTexture(null); + + } else { + mCamera.setPreviewDisplay(null); + } + } catch (Exception e) { + Log.e(TAG, "Failed to clear camera preview: " + e); + } + mCamera.release(); + mCamera = null; + } + } + } + + /** + * Returns the preview size that is currently in use by the underlying camera. + */ + public Size getPreviewSize() { + return mPreviewSize; + } + + /** + * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or + * {@link #CAMERA_FACING_FRONT}. + */ + public int getCameraFacing() { + return mFacing; + } + + public int doZoom(float scale) { + synchronized (mCameraLock) { + if (mCamera == null) { + return 0; + } + int currentZoom = 0; + int maxZoom; + Camera.Parameters parameters = mCamera.getParameters(); + if (!parameters.isZoomSupported()) { + Log.w(TAG, "Zoom is not supported on this device"); + return currentZoom; + } + maxZoom = parameters.getMaxZoom(); + + currentZoom = parameters.getZoom() + 1; + float newZoom; + if (scale > 1) { + newZoom = currentZoom + scale * (maxZoom / 10); + } else { + newZoom = currentZoom * scale; + } + currentZoom = Math.round(newZoom) - 1; + if (currentZoom < 0) { + currentZoom = 0; + } else if (currentZoom > maxZoom) { + currentZoom = maxZoom; + } + parameters.setZoom(currentZoom); + mCamera.setParameters(parameters); + return currentZoom; + } + } + + /** + * Initiates taking a picture, which happens asynchronously. The camera source should have been + * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera + * preview is suspended while the picture is being taken, but will resume once picture taking is + * done. + * + * @param shutter the callback for image capture moment, or null + * @param jpeg the callback for JPEG image data, or null + */ + public void takePicture(ShutterCallback shutter, PictureCallback jpeg) { + synchronized (mCameraLock) { + if (mCamera != null) { + PictureStartCallback startCallback = new PictureStartCallback(); + startCallback.mDelegate = shutter; + PictureDoneCallback doneCallback = new PictureDoneCallback(); + doneCallback.mDelegate = jpeg; + mCamera.takePicture(startCallback, null, null, doneCallback); + } + } + } + + /** + * Gets the current focus mode setting. + * + * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link + * #autoFocus(AutoFocusCallback)} to start the focus if focus + * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO. + * @see Camera.Parameters#FOCUS_MODE_AUTO + * @see Camera.Parameters#FOCUS_MODE_INFINITY + * @see Camera.Parameters#FOCUS_MODE_MACRO + * @see Camera.Parameters#FOCUS_MODE_FIXED + * @see Camera.Parameters#FOCUS_MODE_EDOF + * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO + * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE + */ + @Nullable + @FocusMode + public String getFocusMode() { + return mFocusMode; + } + + /** + * Sets the focus mode. + * + * @param mode the focus mode + * @return {@code true} if the focus mode is set, {@code false} otherwise + * @see #getFocusMode() + */ + public boolean setFocusMode(@FocusMode String mode) { + synchronized (mCameraLock) { + if (mCamera != null && mode != null) { + Camera.Parameters parameters = mCamera.getParameters(); + if (parameters.getSupportedFocusModes().contains(mode)) { + parameters.setFocusMode(mode); + mCamera.setParameters(parameters); + mFocusMode = mode; + return true; + } + } + + return false; + } + } + + /** + * Gets the current flash mode setting. + * + * @return current flash mode. null if flash mode setting is not + * supported or the camera is not yet created. + * @see Camera.Parameters#FLASH_MODE_OFF + * @see Camera.Parameters#FLASH_MODE_AUTO + * @see Camera.Parameters#FLASH_MODE_ON + * @see Camera.Parameters#FLASH_MODE_RED_EYE + * @see Camera.Parameters#FLASH_MODE_TORCH + */ + @Nullable + @FlashMode + public String getFlashMode() { + return mFlashMode; + } + + /** + * Sets the flash mode. + * + * @param mode flash mode. + * @return {@code true} if the flash mode is set, {@code false} otherwise + * @see #getFlashMode() + */ + public boolean setFlashMode(@FlashMode String mode) { + synchronized (mCameraLock) { + if (mCamera != null && mode != null) { + Camera.Parameters parameters = mCamera.getParameters(); + if (parameters.getSupportedFlashModes().contains(mode)) { + parameters.setFlashMode(mode); + mCamera.setParameters(parameters); + mFlashMode = mode; + return true; + } + } + + return false; + } + } + + /** + * Starts camera auto-focus and registers a callback function to run when + * the camera is focused. This method is only valid when preview is active + * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}). + *

+ *

Callers should check + * {@link #getFocusMode()} to determine if + * this method should be called. If the camera does not support auto-focus, + * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)} + * callback will be called immediately. + *

+ *

If the current flash mode is not + * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be + * fired during auto-focus, depending on the driver and camera hardware.

+ * + * @param cb the callback to run + * @see #cancelAutoFocus() + */ + public void autoFocus(@Nullable AutoFocusCallback cb) { + synchronized (mCameraLock) { + if (mCamera != null) { + CameraAutoFocusCallback autoFocusCallback = null; + if (cb != null) { + autoFocusCallback = new CameraAutoFocusCallback(); + autoFocusCallback.mDelegate = cb; + } + mCamera.autoFocus(autoFocusCallback); + } + } + } + + /** + * Cancels any auto-focus function in progress. + * Whether or not auto-focus is currently in progress, + * this function will return the focus position to the default. + * If the camera does not support auto-focus, this is a no-op. + * + * @see #autoFocus(AutoFocusCallback) + */ + public void cancelAutoFocus() { + synchronized (mCameraLock) { + if (mCamera != null) { + mCamera.cancelAutoFocus(); + } + } + } + + /** + * Sets camera auto-focus move callback. + * + * @param cb the callback to run + * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + return false; + } + + synchronized (mCameraLock) { + if (mCamera != null) { + CameraAutoFocusMoveCallback autoFocusMoveCallback = null; + if (cb != null) { + autoFocusMoveCallback = new CameraAutoFocusMoveCallback(); + autoFocusMoveCallback.mDelegate = cb; + } + mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback); + } + } + + return true; + } + + //============================================================================================== + // Private + //============================================================================================== + + /** + * Only allow creation via the builder class. + */ + private CameraSource() { + } + + /** + * Wraps the camera1 shutter callback so that the deprecated API isn't exposed. + */ + private class PictureStartCallback implements Camera.ShutterCallback { + private ShutterCallback mDelegate; + + @Override + public void onShutter() { + if (mDelegate != null) { + mDelegate.onShutter(); + } + } + } + + /** + * Wraps the final callback in the camera sequence, so that we can automatically turn the camera + * preview back on after the picture has been taken. + */ + private class PictureDoneCallback implements Camera.PictureCallback { + private PictureCallback mDelegate; + + @Override + public void onPictureTaken(byte[] data, Camera camera) { + if (mDelegate != null) { + mDelegate.onPictureTaken(data); + } + synchronized (mCameraLock) { + if (mCamera != null) { + mCamera.startPreview(); + } + } + } + } + + /** + * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed. + */ + private class CameraAutoFocusCallback implements Camera.AutoFocusCallback { + private AutoFocusCallback mDelegate; + + @Override + public void onAutoFocus(boolean success, Camera camera) { + if (mDelegate != null) { + mDelegate.onAutoFocus(success); + } + } + } + + /** + * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback { + private AutoFocusMoveCallback mDelegate; + + @Override + public void onAutoFocusMoving(boolean start, Camera camera) { + if (mDelegate != null) { + mDelegate.onAutoFocusMoving(start); + } + } + } + + /** + * Opens the camera and applies the user settings. + * + * @throws RuntimeException if the method fails + */ + @SuppressLint("InlinedApi") + private Camera createCamera() { + int requestedCameraId = getIdForRequestedCamera(mFacing); + if (requestedCameraId == -1) { + throw new RuntimeException("Could not find requested camera."); + } + Camera camera = Camera.open(requestedCameraId); + + SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight); + if (sizePair == null) { + throw new RuntimeException("Could not find suitable preview size."); + } + Size pictureSize = sizePair.pictureSize(); + mPreviewSize = sizePair.previewSize(); + + int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps); + if (previewFpsRange == null) { + throw new RuntimeException("Could not find suitable preview frames per second range."); + } + + Camera.Parameters parameters = camera.getParameters(); + + if (pictureSize != null) { + parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); + } + + parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + parameters.setPreviewFpsRange( + previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + parameters.setPreviewFormat(ImageFormat.NV21); + + setRotation(camera, parameters, requestedCameraId); + + if (mFocusMode != null) { + if (parameters.getSupportedFocusModes().contains( + mFocusMode)) { + parameters.setFocusMode(mFocusMode); + } else { + Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device."); + } + } + + // setting mFocusMode to the one set in the params + mFocusMode = parameters.getFocusMode(); + + if (mFlashMode != null) { + if (parameters.getSupportedFlashModes() != null) { + if (parameters.getSupportedFlashModes().contains( + mFlashMode)) { + parameters.setFlashMode(mFlashMode); + } else { + Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device."); + } + } + } + + // setting mFlashMode to the one set in the params + mFlashMode = parameters.getFlashMode(); + + camera.setParameters(parameters); + + // Four frame buffers are needed for working with the camera: + // + // one for the frame that is currently being executed upon in doing detection + // one for the next pending frame to process immediately upon completing detection + // two for the frames that the camera uses to populate future preview images + camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); + + return camera; + } + + /** + * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such + * camera was found. + * + * @param facing the desired camera (front-facing or rear-facing) + */ + private static int getIdForRequestedCamera(int facing) { + CameraInfo cameraInfo = new CameraInfo(); + for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == facing) { + return i; + } + } + return -1; + } + + /** + * Selects the most suitable preview and picture size, given the desired width and height. + *

+ * Even though we may only need the preview size, it's necessary to find both the preview + * size and the picture size of the camera together, because these need to have the same aspect + * ratio. On some hardware, if you would only set the preview size, you will get a distorted + * image. + * + * @param camera the camera to select a preview size from + * @param desiredWidth the desired width of the camera preview frames + * @param desiredHeight the desired height of the camera preview frames + * @return the selected preview and picture size pair + */ + private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { + List validPreviewSizes = generateValidPreviewSizeList(camera); + + // The method for selecting the best size is to minimize the sum of the differences between + // the desired values and the actual values for width and height. This is certainly not the + // only way to select the best size, but it provides a decent tradeoff between using the + // closest aspect ratio vs. using the closest pixel area. + SizePair selectedPair = null; + int minDiff = Integer.MAX_VALUE; + for (SizePair sizePair : validPreviewSizes) { + Size size = sizePair.previewSize(); + int diff = Math.abs(size.getWidth() - desiredWidth) + + Math.abs(size.getHeight() - desiredHeight); + if (diff < minDiff) { + selectedPair = sizePair; + minDiff = diff; + } + } + + return selectedPair; + } + + /** + * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted + * preview images on some devices, the picture size must be set to a size that is the same + * aspect ratio as the preview size or the preview may end up being distorted. If the picture + * size is null, then there is no picture size with the same aspect ratio as the preview size. + */ + private static class SizePair { + private Size mPreview; + private Size mPicture; + + public SizePair(android.hardware.Camera.Size previewSize, + android.hardware.Camera.Size pictureSize) { + mPreview = new Size(previewSize.width, previewSize.height); + if (pictureSize != null) { + mPicture = new Size(pictureSize.width, pictureSize.height); + } + } + + public Size previewSize() { + return mPreview; + } + + @SuppressWarnings("unused") + public Size pictureSize() { + return mPicture; + } + } + + /** + * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is + * not a corresponding picture size of the same aspect ratio. If there is a corresponding + * picture size of the same aspect ratio, the picture size is paired up with the preview size. + *

+ * This is necessary because even if we don't use still pictures, the still picture size must be + * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the + * preview images may be distorted on some devices. + */ + private static List generateValidPreviewSizeList(Camera camera) { + Camera.Parameters parameters = camera.getParameters(); + List supportedPreviewSizes = + parameters.getSupportedPreviewSizes(); + List supportedPictureSizes = + parameters.getSupportedPictureSizes(); + List validPreviewSizes = new ArrayList(); + for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { + float previewAspectRatio = (float) previewSize.width / (float) previewSize.height; + + // By looping through the picture sizes in order, we favor the higher resolutions. + // We choose the highest resolution in order to support taking the full resolution + // picture later. + for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) { + float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height; + if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { + validPreviewSizes.add(new SizePair(previewSize, pictureSize)); + break; + } + } + } + + // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all + // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we + // still account for it. + if (validPreviewSizes.size() == 0) { + Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size"); + for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { + // The null picture size will let us know that we shouldn't set a picture size. + validPreviewSizes.add(new SizePair(previewSize, null)); + } + } + + return validPreviewSizes; + } + + /** + * Selects the most suitable preview frames per second range, given the desired frames per + * second. + * + * @param camera the camera to select a frames per second range from + * @param desiredPreviewFps the desired frames per second for the camera preview frames + * @return the selected preview frames per second range + */ + private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) { + // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame + // rates. + int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f); + + // The method for selecting the best range is to minimize the sum of the differences between + // the desired value and the upper and lower bounds of the range. This may select a range + // that the desired value is outside of, but this is often preferred. For example, if the + // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the + // range (15, 30). + int[] selectedFpsRange = null; + int minDiff = Integer.MAX_VALUE; + List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange(); + for (int[] range : previewFpsRangeList) { + int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; + int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; + int diff = Math.abs(deltaMin) + Math.abs(deltaMax); + if (diff < minDiff) { + selectedFpsRange = range; + minDiff = diff; + } + } + return selectedFpsRange; + } + + /** + * Calculates the correct rotation for the given camera id and sets the rotation in the + * parameters. It also sets the camera's display orientation and rotation. + * + * @param parameters the camera parameters for which to set the rotation + * @param cameraId the camera id to set rotation based on + */ + private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) { + WindowManager windowManager = + (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + int degrees = 0; + int rotation = windowManager.getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + Log.e(TAG, "Bad rotation value: " + rotation); + } + + CameraInfo cameraInfo = new CameraInfo(); + Camera.getCameraInfo(cameraId, cameraInfo); + + int angle; + int displayAngle; + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + angle = (cameraInfo.orientation + degrees) % 360; + displayAngle = (360 - angle) % 360; // compensate for it being mirrored + } else { // back-facing + angle = (cameraInfo.orientation - degrees + 360) % 360; + displayAngle = angle; + } + + // This corresponds to the rotation constants in {@link Frame}. + mRotation = angle / 90; + + camera.setDisplayOrientation(displayAngle); + parameters.setRotation(angle); + } + + /** + * Creates one buffer for the camera preview callback. The size of the buffer is based off of + * the camera preview size and the format of the camera image. + * + * @return a new preview buffer of the appropriate size for the current camera settings + */ + private byte[] createPreviewBuffer(Size previewSize) { + int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); + long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; + int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1; + + // + // NOTICE: This code only works when using play services v. 8.1 or higher. + // + + // Creating the byte array this way and wrapping it, as opposed to using .allocate(), + // should guarantee that there will be an array to work with. + byte[] byteArray = new byte[bufferSize]; + ByteBuffer buffer = ByteBuffer.wrap(byteArray); + if (!buffer.hasArray() || (buffer.array() != byteArray)) { + // I don't think that this will ever happen. But if it does, then we wouldn't be + // passing the preview content to the underlying detector later. + throw new IllegalStateException("Failed to create valid buffer for camera source."); + } + + mBytesToByteBuffer.put(byteArray, buffer); + return byteArray; + } + + //============================================================================================== + // Frame processing + //============================================================================================== + + /** + * Called when the camera has a new preview frame. + */ + private class CameraPreviewCallback implements Camera.PreviewCallback { + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + mFrameProcessor.setNextFrame(data, camera); + } + } + + /** + * This runnable controls access to the underlying receiver, calling it to process frames when + * available from the camera. This is designed to run detection on frames as fast as possible + * (i.e., without unnecessary context switching or waiting on the next frame). + *

+ * While detection is running on a frame, new frames may be received from the camera. As these + * frames come in, the most recent frame is held onto as pending. As soon as detection and its + * associated processing are done for the previous frame, detection on the mostly recently + * received frame will immediately start on the same thread. + */ + private class FrameProcessingRunnable implements Runnable { + private Detector mDetector; + private long mStartTimeMillis = SystemClock.elapsedRealtime(); + + // This lock guards all of the member variables below. + private final Object mLock = new Object(); + private boolean mActive = true; + + // These pending variables hold the state associated with the new frame awaiting processing. + private long mPendingTimeMillis; + private int mPendingFrameId = 0; + private ByteBuffer mPendingFrameData; + + FrameProcessingRunnable(Detector detector) { + mDetector = detector; + } + + /** + * Releases the underlying receiver. This is only safe to do after the associated thread + * has completed, which is managed in camera source's release method above. + */ + @SuppressLint("Assert") + void release() { + assert (mProcessingThread.getState() == State.TERMINATED); + mDetector.release(); + mDetector = null; + } + + /** + * Marks the runnable as active/not active. Signals any blocked threads to continue. + */ + void setActive(boolean active) { + synchronized (mLock) { + mActive = active; + mLock.notifyAll(); + } + } + + /** + * Sets the frame data received from the camera. This adds the previous unused frame buffer + * (if present) back to the camera, and keeps a pending reference to the frame data for + * future use. + */ + void setNextFrame(byte[] data, Camera camera) { + synchronized (mLock) { + if (mPendingFrameData != null) { + camera.addCallbackBuffer(mPendingFrameData.array()); + mPendingFrameData = null; + } + + if (!mBytesToByteBuffer.containsKey(data)) { + Log.d(TAG, + "Skipping frame. Could not find ByteBuffer associated with the image " + + "data from the camera."); + return; + } + + // Timestamp and frame ID are maintained here, which will give downstream code some + // idea of the timing of frames received and when frames were dropped along the way. + mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis; + mPendingFrameId++; + mPendingFrameData = mBytesToByteBuffer.get(data); + + // Notify the processor thread if it is waiting on the next frame (see below). + mLock.notifyAll(); + } + } + + /** + * As long as the processing thread is active, this executes detection on frames + * continuously. The next pending frame is either immediately available or hasn't been + * received yet. Once it is available, we transfer the frame info to local variables and + * run detection on that frame. It immediately loops back for the next frame without + * pausing. + *

+ * If detection takes longer than the time in between new frames from the camera, this will + * mean that this loop will run without ever waiting on a frame, avoiding any context + * switching or frame acquisition time latency. + *

+ * If you find that this is using more CPU than you'd like, you should probably decrease the + * FPS setting above to allow for some idle time in between frames. + */ + @Override + public void run() { + Frame outputFrame; + ByteBuffer data; + + while (true) { + synchronized (mLock) { + while (mActive && (mPendingFrameData == null)) { + try { + // Wait for the next frame to be received from the camera, since we + // don't have it yet. + mLock.wait(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing loop terminated.", e); + return; + } + } + + if (!mActive) { + // Exit the loop once this camera source is stopped or released. We check + // this here, immediately after the wait() above, to handle the case where + // setActive(false) had been called, triggering the termination of this + // loop. + return; + } + + outputFrame = new Frame.Builder() + .setImageData(mPendingFrameData, mPreviewSize.getWidth(), + mPreviewSize.getHeight(), ImageFormat.NV21) + .setId(mPendingFrameId) + .setTimestampMillis(mPendingTimeMillis) + .setRotation(mRotation) + .build(); + + // Hold onto the frame data locally, so that we can use this for detection + // below. We need to clear mPendingFrameData to ensure that this buffer isn't + // recycled back to the camera before we are done using that data. + data = mPendingFrameData; + mPendingFrameData = null; + } + + // The code below needs to run outside of synchronization, because this will allow + // the camera to add pending frame(s) while we are running detection on the current + // frame. + + try { + mDetector.receiveFrame(outputFrame); + } catch (Throwable t) { + Log.e(TAG, "Exception thrown from receiver.", t); + } finally { + mCamera.addCallbackBuffer(data.array()); + } + } + } + } +} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java new file mode 100644 index 000000000..be82f3176 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java @@ -0,0 +1,191 @@ +package com.bitcoin.cordova.qrreader; + + +import android.Manifest; +import android.content.Context; +import android.content.res.Configuration; +import android.support.annotation.RequiresPermission; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.google.android.gms.common.images.Size; + +import java.io.IOException; + +public class CameraSourcePreview extends ViewGroup { + private static final String TAG = "CameraSourcePreview"; + + private Context mContext; + private SurfaceView mSurfaceView; + private boolean mStartRequested; + private boolean mSurfaceAvailable; + private CameraSource mCameraSource; + + //private GraphicOverlay mOverlay; + + public CameraSourcePreview(Context context) { + super(context); + mContext = context; + mStartRequested = false; + mSurfaceAvailable = false; + + mSurfaceView = new SurfaceView(context); + mSurfaceView.getHolder().addCallback(new SurfaceCallback()); + addView(mSurfaceView); + } + + public CameraSourcePreview(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mStartRequested = false; + mSurfaceAvailable = false; + + mSurfaceView = new SurfaceView(context); + mSurfaceView.getHolder().addCallback(new SurfaceCallback()); + addView(mSurfaceView); + } + + @RequiresPermission(Manifest.permission.CAMERA) + public void start(CameraSource cameraSource) throws IOException, SecurityException { + if (cameraSource == null) { + stop(); + } + + mCameraSource = cameraSource; + + if (mCameraSource != null) { + mStartRequested = true; + startIfReady(); + } + } + + /* + @RequiresPermission(Manifest.permission.CAMERA) + public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException { + mOverlay = overlay; + start(cameraSource); + } + */ + + public void stop() { + if (mCameraSource != null) { + mCameraSource.stop(); + } + } + + public void release() { + if (mCameraSource != null) { + mCameraSource.release(); + mCameraSource = null; + } + } + + @RequiresPermission(Manifest.permission.CAMERA) + private void startIfReady() throws IOException, SecurityException { + if (mStartRequested && mSurfaceAvailable) { + mCameraSource.start(mSurfaceView.getHolder()); + /* + if (mOverlay != null) { + Size size = mCameraSource.getPreviewSize(); + int min = Math.min(size.getWidth(), size.getHeight()); + int max = Math.max(size.getWidth(), size.getHeight()); + if (isPortraitMode()) { + // Swap width and height sizes when in portrait, since it will be rotated by + // 90 degrees + mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing()); + } else { + mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing()); + } + mOverlay.clear(); + } + */ + mStartRequested = false; + } + } + + private class SurfaceCallback implements SurfaceHolder.Callback { + @Override + public void surfaceCreated(SurfaceHolder surface) { + mSurfaceAvailable = true; + try { + startIfReady(); + } catch (SecurityException se) { + Log.e(TAG,"Do not have permission to start the camera", se); + } catch (IOException e) { + Log.e(TAG, "Could not start camera source.", e); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder surface) { + mSurfaceAvailable = false; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = 320; + int height = 240; + if (mCameraSource != null) { + Size size = mCameraSource.getPreviewSize(); + if (size != null) { + width = size.getWidth(); + height = size.getHeight(); + } + } + + // Swap width and height sizes when in portrait, since it will be rotated 90 degrees + if (isPortraitMode()) { + int tmp = width; + //noinspection SuspiciousNameCombination + width = height; + height = tmp; + } + + final int layoutWidth = right - left; + final int layoutHeight = bottom - top; + + // Computes height and width for potentially doing fit width. + int childWidth = layoutWidth; + int childHeight = (int)(((float) layoutWidth / (float) width) * height); + + // If height is too tall using fit width, does fit height instead. + if (childHeight > layoutHeight) { + childHeight = layoutHeight; + childWidth = (int)(((float) layoutHeight / (float) height) * width); + } + + for (int i = 0; i < getChildCount(); ++i) { + getChildAt(i).layout(0, 0, childWidth, childHeight); + } + + try { + startIfReady(); + } catch (SecurityException se) { + Log.e(TAG,"Do not have permission to start the camera", se); + } catch (IOException e) { + Log.e(TAG, "Could not start camera source.", e); + } + } + + private boolean isPortraitMode() { + int orientation = mContext.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return false; + } + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + return true; + } + + Log.d(TAG, "isPortraitMode returning false by default"); + return false; + } +} + diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java new file mode 100644 index 000000000..c1d92778f --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -0,0 +1,348 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 com.bitcoin.cordova.qrreader; + + + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.hardware.Camera; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.vision.MultiProcessor; +import com.google.android.gms.vision.barcode.Barcode; +import com.google.android.gms.vision.barcode.BarcodeDetector; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaInterface; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.provider.Settings; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.Toast; + +public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { + public static final String TAG = "QRReader"; + + public static String platform; // Device OS + public static String uuid; // Device UUID + + private static final String ANDROID_PLATFORM = "Android"; + private static final String AMAZON_PLATFORM = "amazon-fireos"; + private static final String AMAZON_DEVICE = "Amazon"; + + public static final String CAMERA = Manifest.permission.CAMERA; + public static final int CAMERA_REQ_CODE = 774980; + + // intent request code to handle updating play services if needed. + private static final int RC_HANDLE_GMS = 9001; + + + private Map mBarcodes = new HashMap(); + private CameraSource mCameraSource; + private CameraSourcePreview mCameraSourcePreview; + private CallbackContext mStartCallbackContext; + + public QRReader() { + } + + + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + //QRReader.uuid = getUuid(); + } + + + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + if ("getTestInfo".equals(action)) { + + JSONObject r = new JSONObject(); + r.put("something", this.getTestInfo()); + callbackContext.success(r); + + } else if ("startReading".equals(action)) { + startReading(callbackContext); + + } else if ("stopReading".equals(action)) { + callbackContext.success("stopped"); + } else { + return false; + } + return true; + } + + @Override + public void onBarcodeDetected(Barcode barcode) { + String contents = barcode.rawValue; + Log.d(TAG, "Detected new barcode."); + if (mStartCallbackContext != null) { + mStartCallbackContext.success(contents); + } else { + Log.e(TAG, "No callback context when detecting new barcode."); + } + } + + + /** + * Creates and starts the camera. Note that this uses a higher resolution in comparison + * to other detection examples to enable the barcode detector to detect small barcodes + * at long distances. + * + * Suppressing InlinedApi since there is a check that the minimum version is met before using + * the constant. + */ + @SuppressLint("InlinedApi") + private Boolean createCameraSource(Context context, boolean useFlash, CallbackContext callbackContext) { + + boolean autoFocus = true; + // A barcode detector is created to track barcodes. An associated multi-processor instance + // is set to receive the barcode detection results, track the barcodes, and maintain + // graphics for each barcode on screen. The factory is used by the multi-processor to + // create a separate tracker instance for each barcode. + BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); + BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, this); + barcodeDetector.setProcessor( + new MultiProcessor.Builder(barcodeFactory).build()); + + if (!barcodeDetector.isOperational()) { + // Note: The first time that an app using the barcode or face API is installed on a + // device, GMS will download a native libraries to the device in order to do detection. + // Usually this completes before the app is run for the first time. But if that + // download has not yet completed, then the above call will not detect any barcodes + // and/or faces. + // + // isOperational() can be used to check if the required native libraries are currently + // available. The detectors will automatically become operational once the library + // downloads complete on device. + Log.w(TAG, "Detector dependencies are not yet available."); + callbackContext.error("Detector dependencies are not yet available."); + return false; + + + /* TODO: Handle this better later? + // Check for low storage. If there is low storage, the native library will not be + // downloaded, so detection will not become operational. + IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); + boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null; + + if (hasLowStorage) { + Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show(); + Log.w(TAG, "Low storage error."); + } + */ + } + + // Creates and starts the camera. Note that this uses a higher resolution in comparison + // to other detection examples to enable the barcode detector to detect small barcodes + // at long distances. + CameraSource.Builder builder = new CameraSource.Builder(context, barcodeDetector) + .setFacing(CameraSource.CAMERA_FACING_BACK) + .setRequestedPreviewSize(1600, 1024) + .setRequestedFps(15.0f); + + // make sure that auto focus is an available option + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + builder = builder.setFocusMode( + autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); + } + + mCameraSource = builder + .setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null) + .build(); + + return true; + } + + private void getCameraPermission(CallbackContext callbackContext) { + + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + } + + public String getTestInfo() { + return "Hello Java World 1"; + } + + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException + { + Log.d(TAG, "onRequestPermissionResult()"); + + if (requestCode == CAMERA_REQ_CODE) { + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + if (this.mStartCallbackContext != null) { + this.mStartCallbackContext.error("Camera permission denied."); + } + return; + } + } + if (this.mStartCallbackContext != null) { + startReadingWithPermission(mStartCallbackContext); + } + } + + } + + /** + * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet + * (e.g., because onResume was called before the camera source was created), this will be called + * again when the camera source is created. + */ + private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { + // check that the device has play services available. + int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); + if (code != ConnectionResult.SUCCESS) { + Dialog dlg = + GoogleApiAvailability.getInstance().getErrorDialog(cordova.getActivity(), code, RC_HANDLE_GMS); + dlg.show(); + callbackContext.error("Google Play services is unavailable."); + return false; + } + + if (mCameraSource != null) { + try { + mCameraSourcePreview.start(mCameraSource); + } catch (IOException e) { + Log.e(TAG, "Unable to start camera source.", e); + mCameraSource.release(); + mCameraSource = null; + callbackContext.error("Unable to start camera source. " + e.getMessage()); + return false; + } + } else { + Log.e(TAG, "No camera source to start."); + callbackContext.error("No camera source to start."); + return false; + } + return true; + } + + private void startReading(CallbackContext callbackContext) { + Log.d(TAG, "startReading()"); + mStartCallbackContext = callbackContext; + + if(cordova.hasPermission(CAMERA)) + { + startReadingWithPermission(callbackContext); + } + else + { + getCameraPermission(callbackContext); + } + } + + private void startReadingWithPermission(final CallbackContext callbackContext) { + Log.d(TAG, "startReadingWithPermission()"); + + final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); + if (viewGroup == null) { + callbackContext.error("Failed to get view group."); + return; + } + + + final Context context = cordova.getActivity().getApplicationContext(); + if (mCameraSource == null) { + if (!createCameraSource(context, false, callbackContext)) { + return; + } else { + //callbackContext.success("Created camera source."); + } + } else { + //callbackContext.success("Have existing camera source."); + } + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + + // What about coming through here subsequently? + mCameraSourcePreview = new CameraSourcePreview(context); + + //FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); + + //viewGroup.addView(mCameraSourcePreview, layoutParams); + + FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(800, 800, Gravity.CENTER); + FrameLayout.LayoutParams matchParentLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); + View testView; + //View testView = new View(context); + //testView = new Button(context); + //((Button) testView).setText("The Button"); + //testView.setBackgroundColor(Color.argb(1, 1, 0, 0)); + testView = mCameraSourcePreview; + + + viewGroup.addView(testView, sizedLayout); + + //webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); + //cameraPreviewing = true; + //webView.getView().bringToFront(); + //mCameraSourcePreview.bringToFront(); + //testView.bringToFront(); + webView.getView().bringToFront(); + viewGroup.bringChildToFront(testView); + + Boolean cameraStarted = false; + try { + cameraStarted = startCameraSource(context, callbackContext); + } catch (SecurityException e) { + Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); + callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); + return; + } + + //testView.bringToFront(); + /* // Send barcode instead + if (cameraStarted) { + callbackContext.success("Added view."); + } + */ + } + }); + + + } + +} + diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle new file mode 100644 index 000000000..82556217b --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle @@ -0,0 +1,4 @@ +dependencies { + // Important - the CameraSource implementation in this project requires version 8.1 or higher. + compile 'com.google.android.gms:play-services-vision:11.8.0' +} \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js new file mode 100644 index 000000000..f2679a6f4 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js @@ -0,0 +1,106 @@ +cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * +*/ + + +var argscheck = require('cordova/argscheck'); +var channel = require('cordova/channel'); +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var cordova = require('cordova'); +/* +channel.createSticky('onCordovaInfoReady'); +// Tell cordova channel to wait on the CordovaInfoReady event +channel.waitForInitialization('onCordovaInfoReady'); +*/ + +function QRReader() { + this.testString = 'hello1'; + /* + this.available = false; + this.platform = null; + this.version = null; + this.uuid = null; + this.cordova = null; + this.model = null; + this.manufacturer = null; + this.isVirtual = null; + this.serial = null; + + var me = this; + + channel.onCordovaReady.subscribe(function () { + me.getInfo(function (info) { + // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js + // TODO: CB-5105 native implementations should not return info.cordova + var buildLabel = cordova.version; + me.available = true; + me.platform = info.platform; + me.version = info.version; + me.uuid = info.uuid; + me.cordova = buildLabel; + me.model = info.model; + me.isVirtual = info.isVirtual; + me.manufacturer = info.manufacturer || 'unknown'; + me.serial = info.serial || 'unknown'; + channel.onCordovaInfoReady.fire(); + }, function (e) { + me.available = false; + utils.alert('[ERROR] Error initializing Cordova: ' + e); + }); + }); + */ +} + + +/** + * Get device info + * + * @param {Function} successCallback The function to call when the heading data is available + * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) + */ +/* +QRReader.prototype.getInfo = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.getInfo', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); +}; +*/ + +QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); +}; + +QRReader.prototype.startReading = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.startReading', arguments); + exec(successCallback, errorCallback, 'QRReader', 'startReading', []); +}; + +QRReader.prototype.stopReading = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.stopReading', arguments); + exec(successCallback, errorCallback, 'QRReader', 'stopReading', []); +}; + +module.exports = new QRReader(); + +//module.exports = {}; + +}); From 57fd31d0840604ee94af16f891a1eda1fe18f62b Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 19:30:30 +1300 Subject: [PATCH 12/65] Added more of the source files into plugin.xml. --- plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml index 1d35c7ce7..99e3c97a3 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -40,7 +40,13 @@ + + + + + + v From e0a216cd15f9b9b41fa8583b6c30592b392b3165 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 19:46:41 +1300 Subject: [PATCH 13/65] Corrected the path for the gradle file in the plugin. --- plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml index 99e3c97a3..ebdea4b09 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -46,8 +46,8 @@ - v - + + From 9062132944f974b321d706f94a85075c9ef3cf62 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 19:47:55 +1300 Subject: [PATCH 14/65] Added package.json for plugin. --- .../cordova-plugin-qrreader/package.json | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/package.json diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/package.json b/plugins-bitcoincom/cordova-plugin-qrreader/package.json new file mode 100644 index 000000000..3b9b910c9 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/package.json @@ -0,0 +1,32 @@ +{ + "name": "cordova-plugin-qrreader", + "version": "0.1.0", + "description": "Cordova QR Code Reader Plugin", + "cordova": { + "id": "cordova-plugin-qrreader", + "platforms": [ + "android" + ] + }, + "keywords": [ + "cordova", + "qrcode", + "ecosystem:cordova", + "cordova-android", + "cordova-ios", + "cordova-windows", + "cordova-browser", + "cordova-osx" + ], + "author": "Bitcoin.com", + "license": "Apache-2.0", + "engines": { + "cordovaDependencies": { + "0.1.0": { + "cordova-androd": ">6.0.0" + } + } + }, + "devDependencies": { + } +} From b734cf26bed5c614883b3cd0fed0c02d8f2bebe1 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 20:01:04 +1300 Subject: [PATCH 15/65] Added qrreader into config-template.xml. --- app-template/config-template.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app-template/config-template.xml b/app-template/config-template.xml index db6acff36..0fa2fcba7 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -79,6 +79,7 @@ + From d5a39b6a7296ef20039d0f8a5473be9397bbde2d Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 20:12:22 +1300 Subject: [PATCH 16/65] Updated the readme. --- plugins-bitcoincom/cordova-plugin-qrreader/readme.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/readme.md diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md new file mode 100644 index 000000000..5ddb74e9a --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md @@ -0,0 +1,6 @@ +After cloning, delete the platforms/android folder before running `npm run apply:bitcoincom`. +It's convenient to use that folder for now when working on the plugin. We will remove it from the repo after the plugin dev is complete. + +To add the plugin (if not found during `npm run apply:bitcoincom`) + +`node_modules/cordova/bin/cordova plugin add ./plugins-bitcoincom/cordova-plugin-qrreader` \ No newline at end of file From fcda6aae1aab74ef9c34086bf045707743581cfb Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 20:55:25 +1300 Subject: [PATCH 17/65] Removed module definition from qrreader.js. --- plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js index f2679a6f4..7ed6179ac 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js +++ b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js @@ -1,4 +1,4 @@ -cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { + /* * * Licensed to the Apache Software Foundation (ASF) under one @@ -103,4 +103,4 @@ module.exports = new QRReader(); //module.exports = {}; -}); + From 1a582353160724d815de2964fd0faa00d532d87b Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 21:00:02 +1300 Subject: [PATCH 18/65] Removed qrreader from config-template.xml since it wasn't working. --- app-template/config-template.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app-template/config-template.xml b/app-template/config-template.xml index 0fa2fcba7..db6acff36 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -79,7 +79,6 @@ - From 3b7f99af4ddb5aa4505fc42133abc3f1877be84d Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 20 Feb 2019 21:02:54 +1300 Subject: [PATCH 19/65] Updated the readme. --- plugins-bitcoincom/cordova-plugin-qrreader/readme.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md index 5ddb74e9a..2af6e782b 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md +++ b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md @@ -1,6 +1,7 @@ -After cloning, delete the platforms/android folder before running `npm run apply:bitcoincom`. -It's convenient to use that folder for now when working on the plugin. We will remove it from the repo after the plugin dev is complete. +1. After cloning, delete the platforms/android folder. It's convenient to use that folder for now when working on the plugin. We will remove it from the repo after the plugin dev is complete. -To add the plugin (if not found during `npm run apply:bitcoincom`) +2. `npm run apply:bitcoincom` -`node_modules/cordova/bin/cordova plugin add ./plugins-bitcoincom/cordova-plugin-qrreader` \ No newline at end of file +3. `node_modules/cordova/bin/cordova plugin add ./plugins-bitcoincom/cordova-plugin-qrreader` + +4. `npm run start:android` \ No newline at end of file From a12fbf207f4439056f7d66bc877bbb7aa1e5ca58 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Thu, 21 Feb 2019 00:10:48 +0900 Subject: [PATCH 20/65] iOS QRReader --- .../src/ios/QRReader.swift | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift new file mode 100644 index 000000000..8fab72216 --- /dev/null +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -0,0 +1,127 @@ +// +// QRReader.swift +// Bitcoin.com +// +// Copyright © 2019 Jean-Baptiste Dominguez +// Copyright © 2019 Bitcoin Cash Supporters developers +// + +// Migration Native to Cordova +// In progress + +import UIKit +import AVKit + +class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { + + fileprivate var captureSession: AVCaptureSession! + fileprivate var previewLayer: AVCaptureVideoPreviewLayer! + + fileprivate var readingCommand: CDVInvokedUrlCommand? + + override func pluginInitialize() { + super.pluginInitialize() + } + + func startReading(_ command: CDVInvokedUrlCommand){ + + // Keep the callback + readingCommand = command + + // If it is already initialized, return + guard let _ = self.previewLayer else { + return + } + + backgroundColor = UIColor.black + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { + // TODO: Handle the case without permission "Permission" + return + } + + let videoInput: AVCaptureDeviceInput + + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + // TODO: Handle this case "Retry" + return + } + + if (captureSession.canAddInput(videoInput)) { + captureSession.addInput(videoInput) + } else { + failed() + return + } + + let metadataOutput = AVCaptureMetadataOutput() + + if (captureSession.canAddOutput(metadataOutput)) { + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + } else { + failed() + return + } + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = self.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + self.layer.addSublayer(previewLayer) + + captureSession.startRunning() + } + + func failed() { + print("Scanning unsupported") + captureSession = nil + } + + // override func viewWillAppear(_ animated: Bool) { + // super.viewWillAppear(animated) + + // if (captureSession?.isRunning == false) { + // captureSession.startRunning() + // } + // } + + // override func viewWillDisappear(_ animated: Bool) { + // super.viewWillDisappear(animated) + + // if (captureSession?.isRunning == true) { + // captureSession.stopRunning() + // } + // } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + captureSession.stopRunning() + + if let metadataObject = metadataObjects.first { + guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + guard let result = readableObject.stringValue + , let readingCommand = self.readingCommand + , let callbackId = readingCommand.callbackId + , let commandDelegate = self.commandDelegate else { + return + } + + // Vigration to test + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + + // Callback + let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result.stringValue) + commandDelegate.send(pluginResult, callbackId: callbackId) + readingCommand = nil + } + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + +} From 85dcf595adf963172dbae9b17a711f474d319010 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Thu, 21 Feb 2019 00:14:03 +0900 Subject: [PATCH 21/65] iOS scanner plugin StopReading --- .../cordova-plugin-qrreader/src/ios/QRReader.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index 8fab72216..677b8bc7a 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -19,6 +19,10 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { fileprivate var readingCommand: CDVInvokedUrlCommand? + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + override func pluginInitialize() { super.pluginInitialize() } @@ -76,6 +80,10 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { captureSession.startRunning() } + + func stopReading(_ command: CDVInvokedUrlCommand){ + captureSession.stopRunning() + } func failed() { print("Scanning unsupported") @@ -119,9 +127,4 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { readingCommand = nil } } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait - } - } From 8ec23c39fae35a0fef4305af6fffc12161d72770 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Thu, 21 Feb 2019 00:27:30 +0900 Subject: [PATCH 22/65] Cleaning --- .../src/ios/QRReader.swift | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index 677b8bc7a..7b909b87b 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -12,6 +12,11 @@ import UIKit import AVKit +enum QRReaderError { + case NO_PERMISSION + case SCANNING_UNSUPPORTED +} + class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { fileprivate var captureSession: AVCaptureSession! @@ -42,6 +47,7 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { // TODO: Handle the case without permission "Permission" + failed(QRReaderError.NO_PERMISSION) return } @@ -54,28 +60,27 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { return } - if (captureSession.canAddInput(videoInput)) { - captureSession.addInput(videoInput) - } else { - failed() + guard captureSession.canAddInput(videoInput) else { + failed(QRReaderError.SCANNING_UNSUPPORTED) return } - + captureSession.addInput(videoInput) + let metadataOutput = AVCaptureMetadataOutput() - if (captureSession.canAddOutput(metadataOutput)) { - captureSession.addOutput(metadataOutput) - - metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = [.qr] - } else { - failed() + guard captureSession.canAddOutput(metadataOutput) else { + failed(QRReaderError.SCANNING_UNSUPPORTED) return } + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = self.layer.bounds previewLayer.videoGravity = .resizeAspectFill + self.layer.addSublayer(previewLayer) captureSession.startRunning() @@ -85,27 +90,18 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { captureSession.stopRunning() } - func failed() { + func failed(error: QRReaderError) { print("Scanning unsupported") - captureSession = nil + guard let readingCommand = self.readingCommand + , let callbackId = readingCommand.callbackId + , let commandDelegate = self.commandDelegate else { + return + } + + let pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error.rawValue) + commandDelegate.send(pluginResult, callbackId:callbackId) } - // override func viewWillAppear(_ animated: Bool) { - // super.viewWillAppear(animated) - - // if (captureSession?.isRunning == false) { - // captureSession.startRunning() - // } - // } - - // override func viewWillDisappear(_ animated: Bool) { - // super.viewWillDisappear(animated) - - // if (captureSession?.isRunning == true) { - // captureSession.stopRunning() - // } - // } - func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { captureSession.stopRunning() From 650e7258d3e8a19fa6ee493a796f3d02356d3dff Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 13:17:27 +1300 Subject: [PATCH 23/65] Stops reading when leaving scan page. Better layout of scan preview. Initialization of QR code reader separated out, ready to use during app initialization. --- .../bitcoin/cordova/qrreader/QRReader.java | 141 ++++++++++-------- src/js/controllers/tab-scan.controller.js | 17 ++- src/js/services/qr-reader.service.js | 86 +++++++++++ 3 files changed, 179 insertions(+), 65 deletions(-) create mode 100644 src/js/services/qr-reader.service.js diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index c1d92778f..e32db135c 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -36,6 +36,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.graphics.Color; import android.hardware.Camera; import android.os.Build; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; @@ -61,15 +62,23 @@ Licensed to the Apache Software Foundation (ASF) under one import android.widget.FrameLayout; import android.widget.Toast; + + public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static final String TAG = "QRReader"; public static String platform; // Device OS public static String uuid; // Device UUID - private static final String ANDROID_PLATFORM = "Android"; - private static final String AMAZON_PLATFORM = "amazon-fireos"; - private static final String AMAZON_DEVICE = "Amazon"; + public enum eReaderError { + ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, + ERROR_FAILED_TO_GET_VIEW_GROUP, + ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, + ERROR_NO_CAMERA_SOURCE, + ERROR_PERMISSION_DENIED, + ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, + ERROR_UNABLE_TO_START_CAMERA_SOURCE + } public static final String CAMERA = Manifest.permission.CAMERA; public static final int CAMERA_REQ_CODE = 774980; @@ -90,6 +99,13 @@ public QRReader() { public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); //QRReader.uuid = getUuid(); + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + } + + }); } @@ -104,7 +120,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo startReading(callbackContext); } else if ("stopReading".equals(action)) { - callbackContext.success("stopped"); + stopReading(callbackContext); } else { return false; } @@ -223,12 +239,31 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } + private void initPreview(@Nullable CallbackContext callbackContext) { + final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); + if (viewGroup == null) { + Log.e(TAG, "Failed to get view group"); + if (callbackContext != null) { + callbackContext.error("Failed to get view group."); + } + return; + } + + final Context context = cordova.getActivity().getApplicationContext(); + mCameraSourcePreview = new CameraSourcePreview(context); + + FrameLayout.LayoutParams childCenterLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER); + viewGroup.addView(mCameraSourcePreview, childCenterLayout); + } + /** * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet * (e.g., because onResume was called before the camera source was created), this will be called * again when the camera source is created. */ private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { + + // check that the device has play services available. int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); if (code != ConnectionResult.SUCCESS) { @@ -239,6 +274,7 @@ private Boolean startCameraSource(Context context, CallbackContext callbackConte return false; } + // TODO: Check for valid mCameraSourcePreview if (mCameraSource != null) { try { mCameraSourcePreview.start(mCameraSource); @@ -274,75 +310,54 @@ private void startReading(CallbackContext callbackContext) { private void startReadingWithPermission(final CallbackContext callbackContext) { Log.d(TAG, "startReadingWithPermission()"); - final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); - if (viewGroup == null) { - callbackContext.error("Failed to get view group."); - return; - } - + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { - final Context context = cordova.getActivity().getApplicationContext(); - if (mCameraSource == null) { - if (!createCameraSource(context, false, callbackContext)) { - return; - } else { - //callbackContext.success("Created camera source."); + if (mCameraSourcePreview == null) { + initPreview(callbackContext); } - } else { - //callbackContext.success("Have existing camera source."); - } - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - - // What about coming through here subsequently? - mCameraSourcePreview = new CameraSourcePreview(context); - - //FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); - - //viewGroup.addView(mCameraSourcePreview, layoutParams); - - FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(800, 800, Gravity.CENTER); - FrameLayout.LayoutParams matchParentLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); - View testView; - //View testView = new View(context); - //testView = new Button(context); - //((Button) testView).setText("The Button"); - //testView.setBackgroundColor(Color.argb(1, 1, 0, 0)); - testView = mCameraSourcePreview; - - - viewGroup.addView(testView, sizedLayout); - - //webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); - //cameraPreviewing = true; - //webView.getView().bringToFront(); - //mCameraSourcePreview.bringToFront(); - //testView.bringToFront(); - webView.getView().bringToFront(); - viewGroup.bringChildToFront(testView); - - Boolean cameraStarted = false; - try { - cameraStarted = startCameraSource(context, callbackContext); - } catch (SecurityException e) { - Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); - callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); - return; - } + if (mCameraSourcePreview != null) { - //testView.bringToFront(); - /* // Send barcode instead - if (cameraStarted) { - callbackContext.success("Added view."); + final Context context = cordova.getActivity().getApplicationContext(); + if (mCameraSource == null) { + if (!createCameraSource(context, false, callbackContext)) { + return; } - */ + } + + webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); + + webView.getView().bringToFront(); + //viewGroup.bringChildToFront(preview); + //viewGroup.bringChildToFront(webView.getView()); + + try { + startCameraSource(context, callbackContext); + } catch (SecurityException e) { + Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); + callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); + return; + } + } + } }); } + private void stopReading(CallbackContext callbackContext) { + Log.d(TAG, "stopReading()"); + + if (mCameraSource != null) { + mCameraSource.stop(); + } + webView.getView().setBackgroundColor(Color.WHITE); + + callbackContext.success("stopped"); + } + } diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 74e6e3c3b..7ad1d628d 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -9,6 +9,7 @@ angular function tabScanController( gettextCatalog , popupService + , qrReaderService , $scope , $log , $timeout @@ -84,8 +85,17 @@ angular }); $scope.$on("$ionicView.afterEnter", function() { - $scope.currentState = scannerStates.hasPermission; + $scope.currentState = scannerStates.visible; console.log('Starting qrreader.'); + + qrReaderService.startReading().then( + function onStartReadingResolved(contents) { + handleSuccessfulScan(contents); + }, + function onStartReadingRejected(reason) { + $log.error('Failed to start reading QR code. ' + reason); + }); + /* window.qrreader.startReading( function onSuccess(result) { console.log('qrreader startReading() result:', result); @@ -95,6 +105,8 @@ angular function onError(error) { console.error('qrreader startReading() error:', error); }); + */ + /* var capabilities = scannerService.getCapabilities(); if (capabilities.hasPermission) { @@ -145,7 +157,8 @@ angular $scope.$on("$ionicView.beforeLeave", function() { //scannerService.deactivate(); - window.qrreader.stopReading(); + //window.qrreader.stopReading(); + qrReaderService.stopReading(); }); function handleSuccessfulScan(contents){ diff --git a/src/js/services/qr-reader.service.js b/src/js/services/qr-reader.service.js new file mode 100644 index 000000000..a7f5ca311 --- /dev/null +++ b/src/js/services/qr-reader.service.js @@ -0,0 +1,86 @@ +'use strict'; + +(function() { + + angular + .module('bitcoincom.services') + .factory('qrReaderService', qrReaderService); + + function qrReaderService( + gettextCatalog + , $log + , $timeout + , platformInfo + , $q + , $rootScope + , $window + ) { + + var errors = { + // Common + + + // Android + + + // Desktop + + + // iOS + }; + var isDesktop = !platformInfo.isCordova; + var qrReader = $window.qrreader; + + var service = { + // Functions + startReading: startReading, + stopReading: stopReading + }; + + return service; + + function startReading() { + var deferred = $q.defer(); + + qrReader.startReading( + function onSuccess(result) { + console.log('qrreader startReading() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrreader startReading() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred; + } + + // No need to wait on this promise unless you want to start again + // immediately after + function stopReading() { + var deferred = $q.defer(); + + qrReader.stopReading( + function onSuccess(result) { + console.log('qrreader stopReading() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrreader stopReading() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred; + } + + } + +})(); From efdb98b315943111dde2b0df7a0117b5f7f59aa8 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 13:37:05 +1300 Subject: [PATCH 24/65] Handling the case where initial permission is denied. --- .../bitcoin/cordova/qrreader/QRReader.java | 19 +++++++---- src/js/controllers/tab-scan.controller.js | 32 +++++++++++++------ src/js/services/qr-reader.service.js | 8 ++--- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index e32db135c..2ce433727 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -71,13 +71,14 @@ public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static String uuid; // Device UUID public enum eReaderError { - ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, - ERROR_FAILED_TO_GET_VIEW_GROUP, - ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, - ERROR_NO_CAMERA_SOURCE, - ERROR_PERMISSION_DENIED, - ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, - ERROR_UNABLE_TO_START_CAMERA_SOURCE + ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, + ERROR_FAILED_TO_GET_VIEW_GROUP, + ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, + ERROR_NO_CAMERA_SOURCE, + ERROR_PERMISSION_DENIED, + ERROR_READER_ALREADY_STARTED, + ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, + ERROR_UNABLE_TO_START_CAMERA_SOURCE } public static final String CAMERA = Manifest.permission.CAMERA; @@ -325,6 +326,9 @@ public void run() { if (!createCameraSource(context, false, callbackContext)) { return; } + } else { + callbackContext.error("Reader already started."); + return; } webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); @@ -353,6 +357,7 @@ private void stopReading(CallbackContext callbackContext) { if (mCameraSource != null) { mCameraSource.stop(); + mCameraSource = null; } webView.getView().setBackgroundColor(Color.WHITE); diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 7ad1d628d..6d36a9ef8 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -85,16 +85,7 @@ angular }); $scope.$on("$ionicView.afterEnter", function() { - $scope.currentState = scannerStates.visible; - console.log('Starting qrreader.'); - - qrReaderService.startReading().then( - function onStartReadingResolved(contents) { - handleSuccessfulScan(contents); - }, - function onStartReadingRejected(reason) { - $log.error('Failed to start reading QR code. ' + reason); - }); + startReading(); /* window.qrreader.startReading( function onSuccess(result) { @@ -229,6 +220,8 @@ angular $scope.canGoBack = function(){ return $state.params.passthroughMode; }; + + function goBack(){ $ionicHistory.nextViewOptions({ disableAnimate: true @@ -236,5 +229,24 @@ angular $ionicHistory.backView().go(); } $scope.goBack = goBack; + + function startReading() { + $scope.currentState = scannerStates.visible; + console.log('Starting qrreader.'); + + qrReaderService.startReading().then( + function onStartReadingResolved(contents) { + handleSuccessfulScan(contents); + }, + function onStartReadingRejected(reason) { + $log.error('Failed to start reading QR code. ' + reason); + + var newScannerState = scannerStates.unauthorized; + // TODO: Handle all the different types of errors + //$scope.$apply(function onApply() { + $scope.currentState = newScannerState; + //}); + }); + } } })(); diff --git a/src/js/services/qr-reader.service.js b/src/js/services/qr-reader.service.js index a7f5ca311..971030779 100644 --- a/src/js/services/qr-reader.service.js +++ b/src/js/services/qr-reader.service.js @@ -52,11 +52,11 @@ console.error('qrreader startReading() error:', error); var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog(errorMessage); + var translatedErrorMessage = gettextCatalog.getString(errorMessage); deferred.reject(translatedErrorMessage); }); - return deferred; + return deferred.promise; } // No need to wait on this promise unless you want to start again @@ -74,11 +74,11 @@ console.error('qrreader stopReading() error:', error); var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog(errorMessage); + var translatedErrorMessage = gettextCatalog.getString(errorMessage); deferred.reject(translatedErrorMessage); }); - return deferred; + return deferred.promise; } } From 17f59babbfb372a22893fb5e28d4fee988893005 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 13:54:30 +1300 Subject: [PATCH 25/65] Migrated the changes back to the plugin source. --- .../src/android/QRReader.java | 147 ++++++++++-------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java index c1d92778f..984b6f3a0 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -36,6 +36,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.graphics.Color; import android.hardware.Camera; import android.os.Build; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; @@ -61,15 +62,24 @@ Licensed to the Apache Software Foundation (ASF) under one import android.widget.FrameLayout; import android.widget.Toast; + + public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static final String TAG = "QRReader"; public static String platform; // Device OS public static String uuid; // Device UUID - private static final String ANDROID_PLATFORM = "Android"; - private static final String AMAZON_PLATFORM = "amazon-fireos"; - private static final String AMAZON_DEVICE = "Amazon"; + public enum eReaderError { + ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, + ERROR_FAILED_TO_GET_VIEW_GROUP, + ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, + ERROR_NO_CAMERA_SOURCE, + ERROR_PERMISSION_DENIED, + ERROR_READER_ALREADY_STARTED, + ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, + ERROR_UNABLE_TO_START_CAMERA_SOURCE + } public static final String CAMERA = Manifest.permission.CAMERA; public static final int CAMERA_REQ_CODE = 774980; @@ -90,6 +100,13 @@ public QRReader() { public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); //QRReader.uuid = getUuid(); + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + } + + }); } @@ -104,7 +121,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo startReading(callbackContext); } else if ("stopReading".equals(action)) { - callbackContext.success("stopped"); + stopReading(callbackContext); } else { return false; } @@ -223,12 +240,31 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } + private void initPreview(@Nullable CallbackContext callbackContext) { + final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); + if (viewGroup == null) { + Log.e(TAG, "Failed to get view group"); + if (callbackContext != null) { + callbackContext.error("Failed to get view group."); + } + return; + } + + final Context context = cordova.getActivity().getApplicationContext(); + mCameraSourcePreview = new CameraSourcePreview(context); + + FrameLayout.LayoutParams childCenterLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER); + viewGroup.addView(mCameraSourcePreview, childCenterLayout); + } + /** * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet * (e.g., because onResume was called before the camera source was created), this will be called * again when the camera source is created. */ private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { + + // check that the device has play services available. int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); if (code != ConnectionResult.SUCCESS) { @@ -239,6 +275,7 @@ private Boolean startCameraSource(Context context, CallbackContext callbackConte return false; } + // TODO: Check for valid mCameraSourcePreview if (mCameraSource != null) { try { mCameraSourcePreview.start(mCameraSource); @@ -274,75 +311,57 @@ private void startReading(CallbackContext callbackContext) { private void startReadingWithPermission(final CallbackContext callbackContext) { Log.d(TAG, "startReadingWithPermission()"); - final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); - if (viewGroup == null) { - callbackContext.error("Failed to get view group."); - return; - } - + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { - final Context context = cordova.getActivity().getApplicationContext(); - if (mCameraSource == null) { - if (!createCameraSource(context, false, callbackContext)) { - return; - } else { - //callbackContext.success("Created camera source."); + if (mCameraSourcePreview == null) { + initPreview(callbackContext); } - } else { - //callbackContext.success("Have existing camera source."); - } - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - - // What about coming through here subsequently? - mCameraSourcePreview = new CameraSourcePreview(context); - - //FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); - - //viewGroup.addView(mCameraSourcePreview, layoutParams); - - FrameLayout.LayoutParams sizedLayout = new FrameLayout.LayoutParams(800, 800, Gravity.CENTER); - FrameLayout.LayoutParams matchParentLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); - View testView; - //View testView = new View(context); - //testView = new Button(context); - //((Button) testView).setText("The Button"); - //testView.setBackgroundColor(Color.argb(1, 1, 0, 0)); - testView = mCameraSourcePreview; - - - viewGroup.addView(testView, sizedLayout); - - //webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); - //cameraPreviewing = true; - //webView.getView().bringToFront(); - //mCameraSourcePreview.bringToFront(); - //testView.bringToFront(); - webView.getView().bringToFront(); - viewGroup.bringChildToFront(testView); - - Boolean cameraStarted = false; - try { - cameraStarted = startCameraSource(context, callbackContext); - } catch (SecurityException e) { - Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); - callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); - return; - } + if (mCameraSourcePreview != null) { - //testView.bringToFront(); - /* // Send barcode instead - if (cameraStarted) { - callbackContext.success("Added view."); + final Context context = cordova.getActivity().getApplicationContext(); + if (mCameraSource == null) { + if (!createCameraSource(context, false, callbackContext)) { + return; } - */ + } else { + callbackContext.error("Reader already started."); + return; + } + + webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); + + webView.getView().bringToFront(); + //viewGroup.bringChildToFront(preview); + //viewGroup.bringChildToFront(webView.getView()); + + try { + startCameraSource(context, callbackContext); + } catch (SecurityException e) { + Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); + callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); + return; + } + } + } }); } -} + private void stopReading(CallbackContext callbackContext) { + Log.d(TAG, "stopReading()"); + + if (mCameraSource != null) { + mCameraSource.stop(); + mCameraSource = null; + } + webView.getView().setBackgroundColor(Color.WHITE); + + callbackContext.success("stopped"); + } +} \ No newline at end of file From 8119cec4d46cd08873055c98f23688f63d632f14 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 16:21:26 +1300 Subject: [PATCH 26/65] Open settings. --- .../cordova-plugin-qrreader/www/qrreader.js | 8 +++++++ .../bitcoin/cordova/qrreader/QRReader.java | 23 +++++++++++++++++++ src/js/services/qr-reader.service.js | 21 +++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index f2679a6f4..85bc462c0 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -1,4 +1,5 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { + /* * * Licensed to the Apache Software Foundation (ASF) under one @@ -89,6 +90,11 @@ QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); }; +QRReader.prototype.openSettings = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); +}; + QRReader.prototype.startReading = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.startReading', arguments); exec(successCallback, errorCallback, 'QRReader', 'startReading', []); @@ -103,4 +109,6 @@ module.exports = new QRReader(); //module.exports = {}; + + }); diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 2ce433727..5cd3c0156 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -35,6 +35,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.content.pm.PackageManager; import android.graphics.Color; import android.hardware.Camera; +import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; import android.util.AttributeSet; @@ -75,6 +76,7 @@ public enum eReaderError { ERROR_FAILED_TO_GET_VIEW_GROUP, ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, ERROR_NO_CAMERA_SOURCE, + ERROR_OPEN_SETTINGS_UNAVAILABLE, ERROR_PERMISSION_DENIED, ERROR_READER_ALREADY_STARTED, ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, @@ -117,6 +119,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo r.put("something", this.getTestInfo()); callbackContext.success(r); + } else if ("openSettings".equals(action)) { + openSettings(callbackContext); + } else if ("startReading".equals(action)) { startReading(callbackContext); @@ -257,6 +262,24 @@ private void initPreview(@Nullable CallbackContext callbackContext) { viewGroup.addView(mCameraSourcePreview, childCenterLayout); } + private void openSettings(CallbackContext callbackContext) { + try { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); + intent.setData(uri); + this.cordova.getActivity().getApplicationContext().startActivity(intent); + + startReading(callbackContext); + + } catch (Exception e) { + Log.e(TAG, "Error opening settings. " + e.getMessage()); + callbackContext.error(eReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); + } + + } + /** * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet * (e.g., because onResume was called before the camera source was created), this will be called diff --git a/src/js/services/qr-reader.service.js b/src/js/services/qr-reader.service.js index 971030779..3a83daa89 100644 --- a/src/js/services/qr-reader.service.js +++ b/src/js/services/qr-reader.service.js @@ -33,12 +33,33 @@ var service = { // Functions + openSettings: openSettings, startReading: startReading, stopReading: stopReading }; return service; + function openSettings() { + var deferred = $q.defer(); + + qrReader.openSettings( + function onSuccess(result) { + console.log('qrreader openSettings() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrreader openSettings() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + function startReading() { var deferred = $q.defer(); From a39e5e2fdd929c5f8feccc1d4b2f42b65d649a5b Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 16:29:23 +1300 Subject: [PATCH 27/65] Migrating changes to the plugin source. --- .../src/android/QRReader.java | 42 ++++++++++--------- .../cordova-plugin-qrreader/www/qrreader.js | 33 +++------------ 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java index 984b6f3a0..822a81a48 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -1,22 +1,3 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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 com.bitcoin.cordova.qrreader; @@ -35,6 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.content.pm.PackageManager; import android.graphics.Color; import android.hardware.Camera; +import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; import android.util.AttributeSet; @@ -75,6 +57,7 @@ public enum eReaderError { ERROR_FAILED_TO_GET_VIEW_GROUP, ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, ERROR_NO_CAMERA_SOURCE, + ERROR_OPEN_SETTINGS_UNAVAILABLE, ERROR_PERMISSION_DENIED, ERROR_READER_ALREADY_STARTED, ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, @@ -117,6 +100,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo r.put("something", this.getTestInfo()); callbackContext.success(r); + } else if ("openSettings".equals(action)) { + openSettings(callbackContext); + } else if ("startReading".equals(action)) { startReading(callbackContext); @@ -257,6 +243,24 @@ private void initPreview(@Nullable CallbackContext callbackContext) { viewGroup.addView(mCameraSourcePreview, childCenterLayout); } + private void openSettings(CallbackContext callbackContext) { + try { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); + intent.setData(uri); + this.cordova.getActivity().getApplicationContext().startActivity(intent); + + startReading(callbackContext); + + } catch (Exception e) { + Log.e(TAG, "Error opening settings. " + e.getMessage()); + callbackContext.error(eReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); + } + + } + /** * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet * (e.g., because onResume was called before the camera source was created), this will be called diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js index 7ed6179ac..8060a6732 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js +++ b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js @@ -1,24 +1,4 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * -*/ var argscheck = require('cordova/argscheck'); @@ -77,18 +57,18 @@ function QRReader() { * @param {Function} successCallback The function to call when the heading data is available * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) */ -/* -QRReader.prototype.getInfo = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.getInfo', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); -}; -*/ + QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); }; +QRReader.prototype.openSettings = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); + exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); +}; + QRReader.prototype.startReading = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.startReading', arguments); exec(successCallback, errorCallback, 'QRReader', 'startReading', []); @@ -101,6 +81,5 @@ QRReader.prototype.stopReading = function (successCallback, errorCallback) { module.exports = new QRReader(); -//module.exports = {}; From da8bfcb2d687ab4fe8b9935acc93d75225b740dd Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 17:13:43 +1300 Subject: [PATCH 28/65] Some fixes for opening settings. --- .../cordova-plugin-qrreader/www/qrreader.js | 30 ++----------------- .../bitcoin/cordova/qrreader/QRReader.java | 25 +++------------- .../cordova-plugin-qrreader/www/qrreader.js | 2 +- src/js/controllers/tab-scan.controller.js | 17 ++++++++++- 4 files changed, 23 insertions(+), 51 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index 85bc462c0..d4d85bb8f 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -1,25 +1,5 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * -*/ var argscheck = require('cordova/argscheck'); @@ -78,12 +58,7 @@ function QRReader() { * @param {Function} successCallback The function to call when the heading data is available * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) */ -/* -QRReader.prototype.getInfo = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.getInfo', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getDeviceInfo', []); -}; -*/ + QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); @@ -92,7 +67,7 @@ QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { QRReader.prototype.openSettings = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); + exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); }; QRReader.prototype.startReading = function (successCallback, errorCallback) { @@ -107,7 +82,6 @@ QRReader.prototype.stopReading = function (successCallback, errorCallback) { module.exports = new QRReader(); -//module.exports = {}; diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 5cd3c0156..776f3c9c2 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -1,22 +1,3 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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 com.bitcoin.cordova.qrreader; @@ -113,6 +94,7 @@ public void run() { public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + Log.d(TAG, "execute() with \"" + action + "\""); if ("getTestInfo".equals(action)) { JSONObject r = new JSONObject(); @@ -263,6 +245,7 @@ private void initPreview(@Nullable CallbackContext callbackContext) { } private void openSettings(CallbackContext callbackContext) { + Log.d(TAG, "openSettings()"); try { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); @@ -271,6 +254,7 @@ private void openSettings(CallbackContext callbackContext) { intent.setData(uri); this.cordova.getActivity().getApplicationContext().startActivity(intent); + Log.d(TAG, "About to start reading."); startReading(callbackContext); } catch (Exception e) { @@ -387,5 +371,4 @@ private void stopReading(CallbackContext callbackContext) { callbackContext.success("stopped"); } -} - +} \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js index 8060a6732..d7265e7ed 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js +++ b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js @@ -66,7 +66,7 @@ QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { QRReader.prototype.openSettings = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); + exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); }; QRReader.prototype.startReading = function (successCallback, errorCallback) { diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 6d36a9ef8..7cce74774 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -183,6 +183,20 @@ angular $scope.openSettings = function(){ //scannerService.openSettings(); + qrReaderService.openSettings().then( + function onOpenSettingsResolved(contents) { + handleSuccessfulScan(contents); + }, + function onOpenSettingsRejected(reason) { + $log.error('Failed to open settings. ' + reason); + + var newScannerState = scannerStates.unavailable; + $scope.canOpenSettings = false; + // TODO: Handle all the different types of errors + //$scope.$apply(function onApply() { + $scope.currentState = newScannerState; + //}); + }); }; $scope.reactivationCount = 0; @@ -241,7 +255,8 @@ angular function onStartReadingRejected(reason) { $log.error('Failed to start reading QR code. ' + reason); - var newScannerState = scannerStates.unauthorized; + var newScannerState = scannerStates.denied; + $scope.canOpenSettings = true; // TODO: Handle all the different types of errors //$scope.$apply(function onApply() { $scope.currentState = newScannerState; From 7dd92394c77c0d5e1212c73099d10f51ad29d2b6 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 17:30:22 +1300 Subject: [PATCH 29/65] Successfully opening settings. --- .../src/com/bitcoin/cordova/qrreader/QRReader.java | 6 ++++-- .../cordova-plugin-qrreader/src/android/QRReader.java | 8 ++++++-- src/js/controllers/tab-scan.controller.js | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 776f3c9c2..000b6204e 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -252,10 +252,12 @@ private void openSettings(CallbackContext callbackContext) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); intent.setData(uri); + Log.d(TAG, "Starting settings activity..."); this.cordova.getActivity().getApplicationContext().startActivity(intent); - Log.d(TAG, "About to start reading."); - startReading(callbackContext); + callbackContext.success(); + //Log.d(TAG, "About to start reading."); + //startReading(callbackContext); } catch (Exception e) { Log.e(TAG, "Error opening settings. " + e.getMessage()); diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java index 822a81a48..5e6731997 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -1,4 +1,3 @@ - package com.bitcoin.cordova.qrreader; @@ -94,6 +93,7 @@ public void run() { public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + Log.d(TAG, "execute() with \"" + action + "\""); if ("getTestInfo".equals(action)) { JSONObject r = new JSONObject(); @@ -244,15 +244,19 @@ private void initPreview(@Nullable CallbackContext callbackContext) { } private void openSettings(CallbackContext callbackContext) { + Log.d(TAG, "openSettings()"); try { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); intent.setData(uri); + Log.d(TAG, "Starting settings activity..."); this.cordova.getActivity().getApplicationContext().startActivity(intent); - startReading(callbackContext); + callbackContext.success(); + //Log.d(TAG, "About to start reading."); + //startReading(callbackContext); } catch (Exception e) { Log.e(TAG, "Error opening settings. " + e.getMessage()); diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 7cce74774..ec543ffd0 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -184,8 +184,8 @@ angular $scope.openSettings = function(){ //scannerService.openSettings(); qrReaderService.openSettings().then( - function onOpenSettingsResolved(contents) { - handleSuccessfulScan(contents); + function onOpenSettingsResolved(result) { + console.log('Open settings resolved with:', result); }, function onOpenSettingsRejected(reason) { $log.error('Failed to open settings. ' + reason); From 92c78c52592063f5e141fa8c6f73c2509f034709 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 21 Feb 2019 17:36:24 +1300 Subject: [PATCH 30/65] Updated readme for gradle issues. --- .../cordova-plugin-qrreader/readme.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md index 2af6e782b..c6951f3e9 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md +++ b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md @@ -4,4 +4,20 @@ 3. `node_modules/cordova/bin/cordova plugin add ./plugins-bitcoincom/cordova-plugin-qrreader` -4. `npm run start:android` \ No newline at end of file +4. `npm run start:android` it will run, but the scanning may not work. + +5. Add the contents of + + plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle + +to the end of + +platforms/android/build-extras.gradle + +Contents presented here for your convenience: + + dependencies { + // Important - the CameraSource implementation in this project requires version 8.1 or higher. + compile 'com.google.android.gms:play-services-vision:11.8.0' + } + From ee48f3b56f4a0ebcc7d752f5095c67be1df1fc41 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 10:32:41 +0900 Subject: [PATCH 31/65] Fix Swift code + add config in plugin.xml --- .../cordova-plugin-qrreader/www/qrreader.js | 3 ++ .../cordova-plugin-qrreader/plugin.xml | 10 ++++++- .../src/ios/QRReader.swift | 29 ++++++++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index f2679a6f4..2631874bc 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -1,4 +1,5 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { + /* * * Licensed to the Apache Software Foundation (ASF) under one @@ -103,4 +104,6 @@ module.exports = new QRReader(); //module.exports = {}; + + }); diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml index ebdea4b09..09e4a6a3b 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -36,7 +36,7 @@ - + @@ -50,4 +50,12 @@ + + + + + + + + diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index 7b909b87b..7b13c4e40 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -12,17 +12,19 @@ import UIKit import AVKit -enum QRReaderError { - case NO_PERMISSION - case SCANNING_UNSUPPORTED -} +@objc(QRReader) class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { - + + fileprivate var readingCommand: CDVInvokedUrlCommand? fileprivate var captureSession: AVCaptureSession! fileprivate var previewLayer: AVCaptureVideoPreviewLayer! - - fileprivate var readingCommand: CDVInvokedUrlCommand? + fileprivate var cameraView: UIView! + + enum QRReaderError { + case NO_PERMISSION + case SCANNING_UNSUPPORTED + } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait @@ -30,15 +32,19 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { override func pluginInitialize() { super.pluginInitialize() + cameraView = CameraView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) + cameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]; } - + func startReading(_ command: CDVInvokedUrlCommand){ // Keep the callback readingCommand = command - // If it is already initialized, return - guard let _ = self.previewLayer else { + // If it is already initialized or webview missing, return + guard let _ = self.previewLayer + , let webView = self.webView + , let superView = webView.superview else { return } @@ -81,7 +87,8 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { previewLayer.frame = self.layer.bounds previewLayer.videoGravity = .resizeAspectFill - self.layer.addSublayer(previewLayer) + cameraView.addSublayer(previewLayer) + superView.insertSubview(cameraView, belowSubview: webView) captureSession.startRunning() } From cbb21038b424d11665362c32a6e98d7b8f39a7c5 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 11:21:42 +0900 Subject: [PATCH 32/65] Fix QRReader config + Clean up --- .../cordova-plugin-qrreader/plugin.xml | 1 + .../src/ios/QRReader.swift | 61 ++++++++++--------- src/js/controllers/tab-scan.controller.js | 16 ++--- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml index 09e4a6a3b..ce0637bcf 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -56,6 +56,7 @@ + diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index 7b13c4e40..edecb73b7 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -12,46 +12,51 @@ import UIKit import AVKit - @objc(QRReader) class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { - + fileprivate var readingCommand: CDVInvokedUrlCommand? fileprivate var captureSession: AVCaptureSession! fileprivate var previewLayer: AVCaptureVideoPreviewLayer! fileprivate var cameraView: UIView! - - enum QRReaderError { - case NO_PERMISSION - case SCANNING_UNSUPPORTED - } - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait + enum QRReaderError: Int { + case NO_PERMISSION = 1 + case SCANNING_UNSUPPORTED = 2 } - + override func pluginInitialize() { super.pluginInitialize() - cameraView = CameraView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) + cameraView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) cameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]; } - + func startReading(_ command: CDVInvokedUrlCommand){ // Keep the callback readingCommand = command // If it is already initialized or webview missing, return - guard let _ = self.previewLayer + + guard let _ = self.cameraView , let webView = self.webView , let superView = webView.superview else { + return + } + + guard self.previewLayer == nil + , self.captureSession == nil else { + + if !self.captureSession.isRunning { + self.captureSession.startRunning() + } return } - backgroundColor = UIColor.black + cameraView.backgroundColor = UIColor.yellow captureSession = AVCaptureSession() - guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { + guard let videoCaptureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else { // TODO: Handle the case without permission "Permission" failed(QRReaderError.NO_PERMISSION) return @@ -71,7 +76,7 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { return } captureSession.addInput(videoInput) - + let metadataOutput = AVCaptureMetadataOutput() guard captureSession.canAddOutput(metadataOutput) else { @@ -79,32 +84,32 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { return } captureSession.addOutput(metadataOutput) - + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = [.qr] + metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code] previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - previewLayer.frame = self.layer.bounds - previewLayer.videoGravity = .resizeAspectFill - - cameraView.addSublayer(previewLayer) + previewLayer.frame = cameraView.layer.bounds + previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill + + cameraView.layer.addSublayer(previewLayer) superView.insertSubview(cameraView, belowSubview: webView) captureSession.startRunning() } - + func stopReading(_ command: CDVInvokedUrlCommand){ captureSession.stopRunning() } - func failed(error: QRReaderError) { + func failed(_ error: QRReaderError) { print("Scanning unsupported") guard let readingCommand = self.readingCommand , let callbackId = readingCommand.callbackId , let commandDelegate = self.commandDelegate else { - return + return } - + let pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error.rawValue) commandDelegate.send(pluginResult, callbackId:callbackId) } @@ -125,9 +130,9 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) // Callback - let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result.stringValue) + let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result) commandDelegate.send(pluginResult, callbackId: callbackId) - readingCommand = nil + self.readingCommand = nil } } } diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index ec543ffd0..c0d2fd52c 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -33,14 +33,14 @@ angular $scope.scannerStates = scannerStates; function _updateCapabilities(){ - var capabilities = scannerService.getCapabilities(); - $scope.scannerIsAvailable = capabilities.isAvailable; - $scope.scannerHasPermission = capabilities.hasPermission; - $scope.scannerIsDenied = capabilities.isDenied; - $scope.scannerIsRestricted = capabilities.isRestricted; - $scope.canEnableLight = capabilities.canEnableLight; - $scope.canChangeCamera = capabilities.canChangeCamera; - $scope.canOpenSettings = capabilities.canOpenSettings; + // var capabilities = scannerService.getCapabilities(); + // $scope.scannerIsAvailable = capabilities.isAvailable; + // $scope.scannerHasPermission = capabilities.hasPermission; + // $scope.scannerIsDenied = capabilities.isDenied; + // $scope.scannerIsRestricted = capabilities.isRestricted; + // $scope.canEnableLight = capabilities.canEnableLight; + // $scope.canChangeCamera = capabilities.canChangeCamera; + // $scope.canOpenSettings = capabilities.canOpenSettings; } function _handleCapabilities(){ From e7b91340240c1a45340131e6d7af979889bb28f7 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 11:33:09 +0900 Subject: [PATCH 33/65] Fix scanning QRCode --- .../cordova-plugin-qrreader/src/ios/QRReader.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index edecb73b7..dc5bbbe10 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -86,7 +86,7 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code] + metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode] previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = cameraView.layer.bounds @@ -114,11 +114,12 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { commandDelegate.send(pluginResult, callbackId:callbackId) } - func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + func captureOutput(_ output: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { captureSession.stopRunning() if let metadataObject = metadataObjects.first { guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + guard let result = readableObject.stringValue , let readingCommand = self.readingCommand , let callbackId = readingCommand.callbackId From 754d624973c703d456be64ce0faa332db811ce70 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 12:21:05 +0900 Subject: [PATCH 34/65] Refactoring + clean up + add open settings --- .../src/ios/QRReader.swift | 126 +++++++++++++----- 1 file changed, 90 insertions(+), 36 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index dc5bbbe10..d4dfdf20f 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -20,17 +20,60 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { fileprivate var previewLayer: AVCaptureVideoPreviewLayer! fileprivate var cameraView: UIView! - enum QRReaderError: Int { - case NO_PERMISSION = 1 - case SCANNING_UNSUPPORTED = 2 + enum QRReaderError: String { + case ERROR_NO_PERMISSION + case ERROR_SCANNING_UNSUPPORTED + case ERROR_OPEN_SETTINGS_UNAVAILABLE } +} + + +// Initialization +// +extension QRReader { + override func pluginInitialize() { super.pluginInitialize() cameraView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) cameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]; } + func captureOutput(_ output: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + captureSession.stopRunning() + + if let metadataObject = metadataObjects.first { + guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + + guard let result = readableObject.stringValue else { + return + } + + // Vigration to test + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + + // Callback + onSuccess(result) + } + } +} + +// Expose methods +// +extension QRReader { + + func openSettings(_ command: CDVInvokedUrlCommand) { + guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else { + return + } + + if UIApplication.shared.canOpenURL(settingsUrl) { + UIApplication.shared.open(settingsUrl, completionHandler: { [weak self] (success) in + self?.callback(command, status: CDVCommandStatus_NO_RESULT) + }) + } + } + func startReading(_ command: CDVInvokedUrlCommand){ // Keep the callback @@ -47,18 +90,18 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { guard self.previewLayer == nil , self.captureSession == nil else { - if !self.captureSession.isRunning { - self.captureSession.startRunning() - } - return + if !self.captureSession.isRunning { + self.captureSession.startRunning() + } + return } - cameraView.backgroundColor = UIColor.yellow + cameraView.backgroundColor = UIColor.white captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else { // TODO: Handle the case without permission "Permission" - failed(QRReaderError.NO_PERMISSION) + onFailed(QRReaderError.ERROR_NO_PERMISSION) return } @@ -68,11 +111,12 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { // TODO: Handle this case "Retry" + onFailed(QRReaderError.ERROR_NO_PERMISSION) return } guard captureSession.canAddInput(videoInput) else { - failed(QRReaderError.SCANNING_UNSUPPORTED) + onFailed(QRReaderError.ERROR_SCANNING_UNSUPPORTED) return } captureSession.addInput(videoInput) @@ -80,7 +124,7 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { let metadataOutput = AVCaptureMetadataOutput() guard captureSession.canAddOutput(metadataOutput) else { - failed(QRReaderError.SCANNING_UNSUPPORTED) + onFailed(QRReaderError.ERROR_SCANNING_UNSUPPORTED) return } captureSession.addOutput(metadataOutput) @@ -101,39 +145,49 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { func stopReading(_ command: CDVInvokedUrlCommand){ captureSession.stopRunning() } +} + +// Private methods +// +extension QRReader { - func failed(_ error: QRReaderError) { - print("Scanning unsupported") - guard let readingCommand = self.readingCommand - , let callbackId = readingCommand.callbackId + fileprivate func callback(_ command: CDVInvokedUrlCommand, status: CDVCommandStatus) { + callback(command, status: status, message: nil) + } + + fileprivate func callback(_ command: CDVInvokedUrlCommand, status: CDVCommandStatus, message: String?) { + guard let callbackId = command.callbackId , let commandDelegate = self.commandDelegate else { return } - let pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error.rawValue) - commandDelegate.send(pluginResult, callbackId:callbackId) + var pluginResult: CDVPluginResult + + // Callback + if let _ = message { + pluginResult = CDVPluginResult(status: status, messageAs: message) + } else { + pluginResult = CDVPluginResult(status: status) + } + + commandDelegate.send(pluginResult, callbackId: callbackId) } - func captureOutput(_ output: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { - captureSession.stopRunning() + func onSuccess(_ result: String) { + guard let readingCommand = self.readingCommand else { + return + } - if let metadataObject = metadataObjects.first { - guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } - - guard let result = readableObject.stringValue - , let readingCommand = self.readingCommand - , let callbackId = readingCommand.callbackId - , let commandDelegate = self.commandDelegate else { - return - } - - // Vigration to test - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - - // Callback - let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result) - commandDelegate.send(pluginResult, callbackId: callbackId) - self.readingCommand = nil + callback(readingCommand, status: CDVCommandStatus_OK, message: result) + self.readingCommand = nil + } + + func onFailed(_ error: QRReaderError) { + guard let readingCommand = self.readingCommand else { + return } + + callback(readingCommand, status: CDVCommandStatus_ERROR, message: error.rawValue) + self.readingCommand = nil } } From 699321738467d326d7ba1365ea3a2e1c77f52f52 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 12:21:15 +0900 Subject: [PATCH 35/65] Update tab-scan --- src/js/controllers/tab-scan.controller.js | 36 +++-------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index c0d2fd52c..fe8964a6e 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -111,34 +111,8 @@ angular */ }); - function activate(){ - /* - scannerService.activate(function(){ - _updateCapabilities(); - _handleCapabilities(); - $log.debug('Scanner activated, setting to visible...'); - $scope.currentState = scannerStates.visible; - // pause to update the view - $timeout(function(){ - scannerService.scan(function(err, contents){ - if(err){ - $log.debug('Scan canceled.'); - } else if ($state.params.passthroughMode) { - $rootScope.scanResult = contents.result || contents; - goBack(); - } else { - handleSuccessfulScan(contents); - } - }); - // resume preview if paused - scannerService.resumePreview(); - }); - }); - */ - } - $scope.activate = activate; - $scope.authorize = function(){ + // TODO: Manage the authorization /* scannerService.initialize(function(){ _refreshScanView(); @@ -147,8 +121,6 @@ angular }; $scope.$on("$ionicView.beforeLeave", function() { - //scannerService.deactivate(); - //window.qrreader.stopReading(); qrReaderService.stopReading(); }); @@ -164,17 +136,17 @@ angular var title = gettextCatalog.getString('Scan Failed'); popupService.showAlert(title, err.message, function onAlertShown() { // Enable another scan since we won't receive incomingDataMenu.menuHidden - //activate(); + startReading(); }); } else { - //scannerService.resumePreview(); + startReading(); } }); } $rootScope.$on('incomingDataMenu.menuHidden', function() { - activate(); + startReading(); }); function onStart() { From fdb2b58accd77b2c16a85867f1409ca11b6840ac Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 12:27:40 +0900 Subject: [PATCH 36/65] Scan clean up controller + html --- src/js/controllers/tab-scan.controller.js | 106 ++-------------------- www/views/tab-scan.html | 10 +- 2 files changed, 10 insertions(+), 106 deletions(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index fe8964a6e..37b9fdd8b 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -29,56 +29,9 @@ angular visible: 'visible' }; - $scope.onStart = onStart; + $scope.onRetry = onRetry; $scope.scannerStates = scannerStates; - - function _updateCapabilities(){ - // var capabilities = scannerService.getCapabilities(); - // $scope.scannerIsAvailable = capabilities.isAvailable; - // $scope.scannerHasPermission = capabilities.hasPermission; - // $scope.scannerIsDenied = capabilities.isDenied; - // $scope.scannerIsRestricted = capabilities.isRestricted; - // $scope.canEnableLight = capabilities.canEnableLight; - // $scope.canChangeCamera = capabilities.canChangeCamera; - // $scope.canOpenSettings = capabilities.canOpenSettings; - } - - function _handleCapabilities(){ - // always update the view - /* - $timeout(function(){ - if(!scannerService.isInitialized()){ - $scope.currentState = scannerStates.loading; - } else if(!$scope.scannerIsAvailable){ - $scope.currentState = scannerStates.unavailable; - } else if($scope.scannerIsDenied){ - $scope.currentState = scannerStates.denied; - } else if($scope.scannerIsRestricted){ - $scope.currentState = scannerStates.denied; - } else if(!$scope.scannerHasPermission){ - $scope.currentState = scannerStates.unauthorized; - } - $log.debug('Scan view state set to: ' + $scope.currentState); - }); - */ - } - - function _refreshScanView(){ - _updateCapabilities(); - _handleCapabilities(); - if($scope.scannerHasPermission){ - activate(); - } - } - - // This could be much cleaner with a Promise API - // (needs a polyfill for some platforms) - /* - $rootScope.$on('scannerServiceInitialized', function(){ - $log.debug('Scanner initialization finished, reinitializing scan view...'); - _refreshScanView(); - }); - */ + $scope.currentState = scannerStates.visible; $scope.$on("$ionicView.enter", function(event, data) { $ionicNavBarDelegate.showBar(true); @@ -86,40 +39,8 @@ angular $scope.$on("$ionicView.afterEnter", function() { startReading(); - /* - window.qrreader.startReading( - function onSuccess(result) { - console.log('qrreader startReading() result:', result); - - handleSuccessfulScan(result); - }, - function onError(error) { - console.error('qrreader startReading() error:', error); - }); - */ - - /* - var capabilities = scannerService.getCapabilities(); - if (capabilities.hasPermission) { - // try initializing and refreshing status any time the view is entered - if(!scannerService.isInitialized()) { - scannerService.gentleInitialize(); - } else { - activate(); - } - } - */ }); - $scope.authorize = function(){ - // TODO: Manage the authorization - /* - scannerService.initialize(function(){ - _refreshScanView(); - }); - */ - }; - $scope.$on("$ionicView.beforeLeave", function() { qrReaderService.stopReading(); }); @@ -149,11 +70,11 @@ angular startReading(); }); - function onStart() { - $scope.currentState = scannerStates.hasPermission; + function onRetry() { + startReading(); } - $scope.openSettings = function(){ + $scope.onOpenSettings = function(){ //scannerService.openSettings(); qrReaderService.openSettings().then( function onOpenSettingsResolved(result) { @@ -165,21 +86,10 @@ angular var newScannerState = scannerStates.unavailable; $scope.canOpenSettings = false; // TODO: Handle all the different types of errors - //$scope.$apply(function onApply() { - $scope.currentState = newScannerState; - //}); + $scope.currentState = newScannerState; }); }; - $scope.reactivationCount = 0; - $scope.attemptToReactivate = function(){ - /* - scannerService.reinitialize(function(){ - $scope.reactivationCount++; - }); - */ - }; - $scope.toggleLight = function(){ /* scannerService.toggleLight(function(lightEnabled){ @@ -230,9 +140,7 @@ angular var newScannerState = scannerStates.denied; $scope.canOpenSettings = true; // TODO: Handle all the different types of errors - //$scope.$apply(function onApply() { - $scope.currentState = newScannerState; - //}); + $scope.currentState = newScannerState; }); } } diff --git a/www/views/tab-scan.html b/www/views/tab-scan.html index dad49537b..cfc043661 100644 --- a/www/views/tab-scan.html +++ b/www/views/tab-scan.html @@ -15,15 +15,11 @@

Scan QR Codes
You can scan bitcoin addresses, payment requests, paper wallets, and more.
-
Enable the camera to get started.
-
Enable camera access in your device settings to get started.
-
Please connect a camera to get started.
- - - + +
-
+
From efdaf513c554c3e34ed3e3cc20f9bc29ec12e11f Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 12:45:23 +0900 Subject: [PATCH 37/65] activate the camera if the user comes back on the app --- src/js/controllers/tab-scan.controller.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 37b9fdd8b..ffc71edd3 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -39,12 +39,20 @@ angular $scope.$on("$ionicView.afterEnter", function() { startReading(); + document.addEventListener("resume", onResume, true); }); $scope.$on("$ionicView.beforeLeave", function() { qrReaderService.stopReading(); + document.removeEventListener("resume", onResume, true); }); + function onResume() { + $scope.$apply(function () { + startReading(); + }); + } + function handleSuccessfulScan(contents){ $log.debug('Scan returned: "' + contents + '"'); From 1148d89b278b46d82f282c08c944d9c453fafca7 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 17:24:08 +1300 Subject: [PATCH 38/65] Removed unused scanner state. --- src/js/controllers/tab-scan.controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index ffc71edd3..f3755a2e2 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -25,7 +25,6 @@ angular unauthorized: 'unauthorized', denied: 'denied', unavailable: 'unavailable', - loading: 'loading', visible: 'visible' }; From 61c5839020921c14e43144ea4d714d95c7dad07b Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 18:22:06 +1300 Subject: [PATCH 39/65] Centering the QR reader preview. --- .../bitcoin/cordova/qrreader/CameraSourcePreview.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java index be82f3176..9d2ae12a4 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java @@ -162,8 +162,15 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto childWidth = (int)(((float) layoutHeight / (float) height) * width); } + // Centre the child in the preview bounds + int childTop = (layoutHeight - childHeight) / 2; + int childBottom = childTop + childHeight; + + int childLeft = (layoutWidth - childWidth) / 2; + int childRight = childLeft + childWidth; + for (int i = 0; i < getChildCount(); ++i) { - getChildAt(i).layout(0, 0, childWidth, childHeight); + getChildAt(i).layout(childLeft, childTop, childRight, childBottom); } try { From 4d1acebb912beef1070b4d8a7ef399a9ee6d022e Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 18:42:39 +1300 Subject: [PATCH 40/65] Updated readme from a while ago. --- plugins-bitcoincom/cordova-plugin-qrreader/readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md index c6951f3e9..4cc102721 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md +++ b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md @@ -21,3 +21,6 @@ Contents presented here for your convenience: compile 'com.google.android.gms:play-services-vision:11.8.0' } + + 6. `npm run start:android` Scanning should work now. + From d78cbb546cc102ca65028e9bb079b6c23e4a4368 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 18:51:52 +1300 Subject: [PATCH 41/65] Migrated changes to the plugin source. --- .../src/android/CameraSourcePreview.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java index be82f3176..9d2ae12a4 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java @@ -162,8 +162,15 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto childWidth = (int)(((float) layoutHeight / (float) height) * width); } + // Centre the child in the preview bounds + int childTop = (layoutHeight - childHeight) / 2; + int childBottom = childTop + childHeight; + + int childLeft = (layoutWidth - childWidth) / 2; + int childRight = childLeft + childWidth; + for (int i = 0; i < getChildCount(); ++i) { - getChildAt(i).layout(0, 0, childWidth, childHeight); + getChildAt(i).layout(childLeft, childTop, childRight, childBottom); } try { From 30a868987e22cf73752dddc16610b0219e963a19 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 19:19:45 +1300 Subject: [PATCH 42/65] Black background outside the QR preview. --- .../src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java | 3 +++ .../src/android/CameraSourcePreview.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java index 9d2ae12a4..258628ef8 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java @@ -4,6 +4,7 @@ import android.Manifest; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.support.annotation.RequiresPermission; import android.util.AttributeSet; import android.util.Log; @@ -32,6 +33,8 @@ public CameraSourcePreview(Context context) { mStartRequested = false; mSurfaceAvailable = false; + setBackgroundColor(Color.BLACK); + mSurfaceView = new SurfaceView(context); mSurfaceView.getHolder().addCallback(new SurfaceCallback()); addView(mSurfaceView); diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java index 9d2ae12a4..258628ef8 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java @@ -4,6 +4,7 @@ import android.Manifest; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.support.annotation.RequiresPermission; import android.util.AttributeSet; import android.util.Log; @@ -32,6 +33,8 @@ public CameraSourcePreview(Context context) { mStartRequested = false; mSurfaceAvailable = false; + setBackgroundColor(Color.BLACK); + mSurfaceView = new SurfaceView(context); mSurfaceView.getHolder().addCallback(new SurfaceCallback()); addView(mSurfaceView); From b080adc00c81c2b98409d5f97480272c4b1fb2af Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 22 Feb 2019 19:37:26 +1300 Subject: [PATCH 43/65] Removed camera controls. --- www/views/tab-scan.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/www/views/tab-scan.html b/www/views/tab-scan.html index cfc043661..59aa3b177 100644 --- a/www/views/tab-scan.html +++ b/www/views/tab-scan.html @@ -24,18 +24,6 @@
-
From 77b8f50470441c526b23d037f992e432415d6c8b Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 16:34:54 +0900 Subject: [PATCH 44/65] Uniform error code with iOS and Android --- .../cordova-plugin-qrreader/www/qrreader.js | 6 ---- .../bitcoin/cordova/qrreader/QRReader.java | 28 ++++++------------ .../src/android/QRReader.java | 29 +++++++------------ .../src/ios/QRReader.swift | 6 ++-- 4 files changed, 22 insertions(+), 47 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index 2cfafc34c..aab362307 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -79,10 +79,4 @@ QRReader.prototype.stopReading = function (successCallback, errorCallback) { }; module.exports = new QRReader(); - - - - - - }); diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 000b6204e..99cfcf154 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -52,17 +52,11 @@ public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static String platform; // Device OS public static String uuid; // Device UUID - public enum eReaderError { - ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, - ERROR_FAILED_TO_GET_VIEW_GROUP, - ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, - ERROR_NO_CAMERA_SOURCE, - ERROR_OPEN_SETTINGS_UNAVAILABLE, - ERROR_PERMISSION_DENIED, - ERROR_READER_ALREADY_STARTED, - ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, - ERROR_UNABLE_TO_START_CAMERA_SOURCE - } + enum QRReaderError { + ERROR_PERMISSION_DENIED, + ERROR_SCANNING_UNSUPPORTED, + ERROR_OPEN_SETTINGS_UNAVAILABLE + } public static final String CAMERA = Manifest.permission.CAMERA; public static final int CAMERA_REQ_CODE = 774980; @@ -108,7 +102,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo startReading(callbackContext); } else if ("stopReading".equals(action)) { - stopReading(callbackContext); + stopReading(callbackContext); } else { return false; } @@ -198,7 +192,6 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo } private void getCameraPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); } @@ -261,7 +254,7 @@ private void openSettings(CallbackContext callbackContext) { } catch (Exception e) { Log.e(TAG, "Error opening settings. " + e.getMessage()); - callbackContext.error(eReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); + callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); } } @@ -307,12 +300,9 @@ private void startReading(CallbackContext callbackContext) { Log.d(TAG, "startReading()"); mStartCallbackContext = callbackContext; - if(cordova.hasPermission(CAMERA)) - { + if(cordova.hasPermission(CAMERA)) { startReadingWithPermission(callbackContext); - } - else - { + } else { getCameraPermission(callbackContext); } } diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java index 5e6731997..99cfcf154 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -1,3 +1,4 @@ + package com.bitcoin.cordova.qrreader; @@ -51,17 +52,11 @@ public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static String platform; // Device OS public static String uuid; // Device UUID - public enum eReaderError { - ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, - ERROR_FAILED_TO_GET_VIEW_GROUP, - ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, - ERROR_NO_CAMERA_SOURCE, - ERROR_OPEN_SETTINGS_UNAVAILABLE, - ERROR_PERMISSION_DENIED, - ERROR_READER_ALREADY_STARTED, - ERROR_SECURITY_EXCEPTION_WHEN_STARTING_CAMERA_SOURCE, - ERROR_UNABLE_TO_START_CAMERA_SOURCE - } + enum QRReaderError { + ERROR_PERMISSION_DENIED, + ERROR_SCANNING_UNSUPPORTED, + ERROR_OPEN_SETTINGS_UNAVAILABLE + } public static final String CAMERA = Manifest.permission.CAMERA; public static final int CAMERA_REQ_CODE = 774980; @@ -107,7 +102,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo startReading(callbackContext); } else if ("stopReading".equals(action)) { - stopReading(callbackContext); + stopReading(callbackContext); } else { return false; } @@ -197,7 +192,6 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo } private void getCameraPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); } @@ -260,7 +254,7 @@ private void openSettings(CallbackContext callbackContext) { } catch (Exception e) { Log.e(TAG, "Error opening settings. " + e.getMessage()); - callbackContext.error(eReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); + callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); } } @@ -306,12 +300,9 @@ private void startReading(CallbackContext callbackContext) { Log.d(TAG, "startReading()"); mStartCallbackContext = callbackContext; - if(cordova.hasPermission(CAMERA)) - { + if(cordova.hasPermission(CAMERA)) { startReadingWithPermission(callbackContext); - } - else - { + } else { getCameraPermission(callbackContext); } } diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index d4dfdf20f..5fa67a129 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -21,7 +21,7 @@ class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { fileprivate var cameraView: UIView! enum QRReaderError: String { - case ERROR_NO_PERMISSION + case ERROR_PERMISSION_DENIED case ERROR_SCANNING_UNSUPPORTED case ERROR_OPEN_SETTINGS_UNAVAILABLE } @@ -101,7 +101,7 @@ extension QRReader { guard let videoCaptureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else { // TODO: Handle the case without permission "Permission" - onFailed(QRReaderError.ERROR_NO_PERMISSION) + onFailed(QRReaderError.ERROR_PERMISSION_DENIED) return } @@ -111,7 +111,7 @@ extension QRReader { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { // TODO: Handle this case "Retry" - onFailed(QRReaderError.ERROR_NO_PERMISSION) + onFailed(QRReaderError.ERROR_PERMISSION_DENIED) return } From 83b113aa4d9ed589a6b79c2756ff320e3394e069 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 16:35:23 +0900 Subject: [PATCH 45/65] Tab --- .../cordova-plugin-qrreader/plugin.xml | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml index ce0637bcf..61d131fe1 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml @@ -19,36 +19,36 @@ --> - QR Reader - Cordova QR Code Reader Plugin - Apache 2.0 - cordova,qrcode - - - - + xmlns:rim="http://www.blackberry.com/ns/widgets" + xmlns:android="http://schemas.android.com/apk/res/android" + id="cordova-plugin-qrreader" + version="0.1.0"> + QR Reader + Cordova QR Code Reader Plugin + Apache 2.0 + cordova,qrcode + + + + - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + From 48651fc59bd3ab748cfc5a64185d67afd18b8655 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 16:35:47 +0900 Subject: [PATCH 46/65] Fix the recurcive loop for the permission --- src/js/controllers/tab-scan.controller.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index f3755a2e2..115d17e46 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -27,6 +27,7 @@ angular unavailable: 'unavailable', visible: 'visible' }; + var isHandlerEnable = true; $scope.onRetry = onRetry; $scope.scannerStates = scannerStates; @@ -47,9 +48,13 @@ angular }); function onResume() { - $scope.$apply(function () { - startReading(); - }); + if (isHandlerEnable) { + $scope.$apply(function () { + startReading(); + }); + } else { + isHandlerEnable = true; + } } function handleSuccessfulScan(contents){ @@ -137,6 +142,7 @@ angular $scope.currentState = scannerStates.visible; console.log('Starting qrreader.'); + isHandlerEnable = false; qrReaderService.startReading().then( function onStartReadingResolved(contents) { handleSuccessfulScan(contents); From 7a3725bec228be2d01d7714e90209bd2c1610c53 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 16:52:05 +0900 Subject: [PATCH 47/65] Add one method to request permission --- .../bitcoin/cordova/qrreader/QRReader.java | 26 ++++++------------- .../src/android/QRReader.java | 26 ++++++------------- .../src/ios/QRReader.swift | 4 +++ .../cordova-plugin-qrreader/www/qrreader.js | 11 ++++---- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 99cfcf154..296e6a7c6 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -89,20 +89,14 @@ public void run() { public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { Log.d(TAG, "execute() with \"" + action + "\""); - if ("getTestInfo".equals(action)) { - - JSONObject r = new JSONObject(); - r.put("something", this.getTestInfo()); - callbackContext.success(r); - - } else if ("openSettings".equals(action)) { + if ("openSettings".equals(action)) { openSettings(callbackContext); - } else if ("startReading".equals(action)) { startReading(callbackContext); - } else if ("stopReading".equals(action)) { stopReading(callbackContext); + } else if ("checkPermission".equals(action)) { + checkPermission(callbackContext); } else { return false; } @@ -191,14 +185,6 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo return true; } - private void getCameraPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); - } - - public String getTestInfo() { - return "Hello Java World 1"; - } - public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { @@ -237,6 +223,10 @@ private void initPreview(@Nullable CallbackContext callbackContext) { viewGroup.addView(mCameraSourcePreview, childCenterLayout); } + private void checkPermission(CallbackContext callbackContext) { + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + } + private void openSettings(CallbackContext callbackContext) { Log.d(TAG, "openSettings()"); try { @@ -303,7 +293,7 @@ private void startReading(CallbackContext callbackContext) { if(cordova.hasPermission(CAMERA)) { startReadingWithPermission(callbackContext); } else { - getCameraPermission(callbackContext); + callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.toString()); } } diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java index 99cfcf154..296e6a7c6 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java @@ -89,20 +89,14 @@ public void run() { public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { Log.d(TAG, "execute() with \"" + action + "\""); - if ("getTestInfo".equals(action)) { - - JSONObject r = new JSONObject(); - r.put("something", this.getTestInfo()); - callbackContext.success(r); - - } else if ("openSettings".equals(action)) { + if ("openSettings".equals(action)) { openSettings(callbackContext); - } else if ("startReading".equals(action)) { startReading(callbackContext); - } else if ("stopReading".equals(action)) { stopReading(callbackContext); + } else if ("checkPermission".equals(action)) { + checkPermission(callbackContext); } else { return false; } @@ -191,14 +185,6 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo return true; } - private void getCameraPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); - } - - public String getTestInfo() { - return "Hello Java World 1"; - } - public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { @@ -237,6 +223,10 @@ private void initPreview(@Nullable CallbackContext callbackContext) { viewGroup.addView(mCameraSourcePreview, childCenterLayout); } + private void checkPermission(CallbackContext callbackContext) { + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + } + private void openSettings(CallbackContext callbackContext) { Log.d(TAG, "openSettings()"); try { @@ -303,7 +293,7 @@ private void startReading(CallbackContext callbackContext) { if(cordova.hasPermission(CAMERA)) { startReadingWithPermission(callbackContext); } else { - getCameraPermission(callbackContext); + callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.toString()); } } diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index 5fa67a129..abb8abeb6 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -62,6 +62,10 @@ extension QRReader { // extension QRReader { + func checkPermission(_ command: CDVInvokedUrlCommand) { + self.callback(command, status: CDVCommandStatus_NO_RESULT) + } + func openSettings(_ command: CDVInvokedUrlCommand) { guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else { return diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js index d7265e7ed..cbfa96cc3 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js +++ b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js @@ -58,17 +58,16 @@ function QRReader() { * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) */ - -QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); -}; - QRReader.prototype.openSettings = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); }; +QRReader.prototype.checkPermission = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.checkPermission', arguments); + exec(successCallback, errorCallback, 'QRReader', 'checkPermission', []); +}; + QRReader.prototype.startReading = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.startReading', arguments); exec(successCallback, errorCallback, 'QRReader', 'startReading', []); From d35104b5a958868cd37ca675f7d3be5809902494 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 17:00:31 +0900 Subject: [PATCH 48/65] add checkPermission service + swift + plugin --- .../src/ios/QRReader.swift | 13 ++--- src/js/controllers/tab-scan.controller.js | 58 +++++++++---------- src/js/services/qr-reader.service.js | 29 +++++++++- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift index abb8abeb6..8a3d919b5 100644 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift @@ -63,19 +63,18 @@ extension QRReader { extension QRReader { func checkPermission(_ command: CDVInvokedUrlCommand) { - self.callback(command, status: CDVCommandStatus_NO_RESULT) + self.callback(command, status: CDVCommandStatus_OK) } func openSettings(_ command: CDVInvokedUrlCommand) { - guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else { + guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString), UIApplication.shared.canOpenURL(settingsUrl) else { + self.callback(command, status: CDVCommandStatus_ERROR, message: QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.rawValue) return } - if UIApplication.shared.canOpenURL(settingsUrl) { - UIApplication.shared.open(settingsUrl, completionHandler: { [weak self] (success) in - self?.callback(command, status: CDVCommandStatus_NO_RESULT) - }) - } + UIApplication.shared.open(settingsUrl, completionHandler: { [weak self] (success) in + self?.callback(command, status: CDVCommandStatus_OK) + }) } func startReading(_ command: CDVInvokedUrlCommand){ diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 115d17e46..2c867fc69 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -27,8 +27,8 @@ angular unavailable: 'unavailable', visible: 'visible' }; - var isHandlerEnable = true; + $scope.onOpenSettings = onOpenSettings; $scope.onRetry = onRetry; $scope.scannerStates = scannerStates; $scope.currentState = scannerStates.visible; @@ -38,7 +38,7 @@ angular }); $scope.$on("$ionicView.afterEnter", function() { - startReading(); + startReadingWithPermission(); document.addEventListener("resume", onResume, true); }); @@ -48,13 +48,9 @@ angular }); function onResume() { - if (isHandlerEnable) { - $scope.$apply(function () { - startReading(); - }); - } else { - isHandlerEnable = true; - } + $scope.$apply(function () { + startReading(); + }); } function handleSuccessfulScan(contents){ @@ -86,7 +82,7 @@ angular startReading(); } - $scope.onOpenSettings = function(){ + function onOpenSettings(){ //scannerService.openSettings(); qrReaderService.openSettings().then( function onOpenSettingsResolved(result) { @@ -102,6 +98,29 @@ angular }); }; + function startReadingWithPermission() { + qrReaderService.checkPermission().then(function () { + startReading(); + }); + } + + function startReading() { + $scope.currentState = scannerStates.visible; + console.log('Starting qrreader.'); + qrReaderService.startReading().then( + function onStartReadingResolved(contents) { + handleSuccessfulScan(contents); + }, + function onStartReadingRejected(reason) { + $log.error('Failed to start reading QR code. ' + reason); + + var newScannerState = scannerStates.denied; + $scope.canOpenSettings = true; + // TODO: Handle all the different types of errors + $scope.currentState = newScannerState; + }); + } + $scope.toggleLight = function(){ /* scannerService.toggleLight(function(lightEnabled){ @@ -137,24 +156,5 @@ angular $ionicHistory.backView().go(); } $scope.goBack = goBack; - - function startReading() { - $scope.currentState = scannerStates.visible; - console.log('Starting qrreader.'); - - isHandlerEnable = false; - qrReaderService.startReading().then( - function onStartReadingResolved(contents) { - handleSuccessfulScan(contents); - }, - function onStartReadingRejected(reason) { - $log.error('Failed to start reading QR code. ' + reason); - - var newScannerState = scannerStates.denied; - $scope.canOpenSettings = true; - // TODO: Handle all the different types of errors - $scope.currentState = newScannerState; - }); - } } })(); diff --git a/src/js/services/qr-reader.service.js b/src/js/services/qr-reader.service.js index 3a83daa89..50052ddbe 100644 --- a/src/js/services/qr-reader.service.js +++ b/src/js/services/qr-reader.service.js @@ -33,9 +33,10 @@ var service = { // Functions - openSettings: openSettings, - startReading: startReading, - stopReading: stopReading + openSettings: openSettings + , startReading: startReading + , stopReading: stopReading + , checkPermission: checkPermission }; return service; @@ -102,6 +103,28 @@ return deferred.promise; } + // No need to wait on this promise unless you want to start again + // immediately after + function checkPermission() { + var deferred = $q.defer(); + + qrReader.checkPermission( + function onSuccess(result) { + console.log('qrreader checkPermission() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrreader checkPermission() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + } })(); From fd1c2a6d09fd7c288dce0adc425a8f6bd878b3a0 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Fri, 22 Feb 2019 17:20:09 +0900 Subject: [PATCH 49/65] Migrate our new scanner in incomingData --- src/js/services/incoming-data.service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/services/incoming-data.service.js b/src/js/services/incoming-data.service.js index b7aadee98..0832e33a5 100644 --- a/src/js/services/incoming-data.service.js +++ b/src/js/services/incoming-data.service.js @@ -15,6 +15,8 @@ , $state , $rootScope , scannerService + , qrReaderService + , platformInfo , sendFlowService , gettextCatalog ) { @@ -39,7 +41,8 @@ cbError(new Error(errorMessage)); } } else { - scannerService.pausePreview(); + var qrService = platformInfo.isMobile ? qrReaderService : scannerService; + qrService.stopReading(); /** * Strategy for the action From 1313f0881cd27a1702480bdaf777d078702a3bde Mon Sep 17 00:00:00 2001 From: Angel Mortega Date: Fri, 22 Feb 2019 17:22:30 +0900 Subject: [PATCH 50/65] Added desktop scanner service. WIP --- src/js/controllers/tab-scan.controller.js | 16 ++- src/js/services/qr-scanner.service.js | 130 ++++++++++++++++++++++ 2 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 src/js/services/qr-scanner.service.js diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 2c867fc69..21df0c3c9 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -10,15 +10,16 @@ angular gettextCatalog , popupService , qrReaderService + , qrScannerService , $scope , $log , $timeout - , scannerService , incomingDataService , $state , $ionicHistory , $rootScope , $ionicNavBarDelegate + , platformInfo ) { var scannerStates = { @@ -28,6 +29,9 @@ angular visible: 'visible' }; + var isDesktop = !platformInfo.isCordova; + var qrService = isDesktop ? qrScannerService : qrReaderService; + $scope.onOpenSettings = onOpenSettings; $scope.onRetry = onRetry; $scope.scannerStates = scannerStates; @@ -43,7 +47,7 @@ angular }); $scope.$on("$ionicView.beforeLeave", function() { - qrReaderService.stopReading(); + qrService.stopReading(); document.removeEventListener("resume", onResume, true); }); @@ -84,7 +88,7 @@ angular function onOpenSettings(){ //scannerService.openSettings(); - qrReaderService.openSettings().then( + qrService.openSettings().then( function onOpenSettingsResolved(result) { console.log('Open settings resolved with:', result); }, @@ -99,15 +103,15 @@ angular }; function startReadingWithPermission() { - qrReaderService.checkPermission().then(function () { + qrService.checkPermission().then(function () { startReading(); }); } function startReading() { $scope.currentState = scannerStates.visible; - console.log('Starting qrreader.'); - qrReaderService.startReading().then( + console.log('Starting QR Service.'); + qrService.startReading().then( function onStartReadingResolved(contents) { handleSuccessfulScan(contents); }, diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js new file mode 100644 index 000000000..16d370d73 --- /dev/null +++ b/src/js/services/qr-scanner.service.js @@ -0,0 +1,130 @@ +'use strict'; + +(function() { + + angular + .module('bitcoincom.services') + .factory('qrScannerService', qrScannerService); + + function qrScannerService( + gettextCatalog + , $log + , $timeout + , platformInfo + , $q + , $rootScope + , $window + , scannerService + ) { + + var errors = { + // Common + + + // Android + + + // Desktop + + + // iOS + }; + var isDesktop = !platformInfo.isCordova; + var qrReader = $window.qrreader; + + var service = { + // Functions + openSettings: openSettings, + startReading: startReading, + stopReading: stopReading, + checkPermission: checkPermission + }; + + return service; + + function openSettings() { + var deferred = $q.defer(); + + scannerService.openSettings( + function onSuccess(result) { + console.log('qrscanner openSettings() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrscanner openSettings() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + + function startReading() { + var deferred = $q.defer(); + + scannerService.initialize( + function onSuccess(result) { + console.log('qrscanner startReading() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrscanner startReading() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + + // No need to wait on this promise unless you want to start again + // immediately after + function stopReading() { + var deferred = $q.defer(); + scannerService.deactivate( + function onSuccess(result) { + console.log('qrscanner stopReading() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrscanner stopReading() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + + // No need to wait on this promise unless you want to start again + // immediately after + function checkPermission() { + var deferred = $q.defer(); + + scannerService.getCapabilities( + function onSuccess(result) { + console.log('qrreader checkPermission() result:', result); + + deferred.resolve(result); + }, + function onError(error) { + console.error('qrreader checkPermission() error:', error); + + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + }); + + return deferred.promise; + } + + } + +})(); From cd9b4287f7b7ef6ee39844f82216a8b77e28f269 Mon Sep 17 00:00:00 2001 From: Angel Mortega Date: Fri, 22 Feb 2019 17:58:20 +0900 Subject: [PATCH 51/65] QR Scanner Service improvements. WIP --- src/js/services/qr-scanner.service.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js index 16d370d73..3f1fa841e 100644 --- a/src/js/services/qr-scanner.service.js +++ b/src/js/services/qr-scanner.service.js @@ -68,8 +68,9 @@ scannerService.initialize( function onSuccess(result) { console.log('qrscanner startReading() result:', result); - - deferred.resolve(result); + scannerService.scan( function onSuccessScan(content) { + deferred.resolve(content); + }); }, function onError(error) { console.error('qrscanner startReading() error:', error); @@ -108,19 +109,7 @@ function checkPermission() { var deferred = $q.defer(); - scannerService.getCapabilities( - function onSuccess(result) { - console.log('qrreader checkPermission() result:', result); - - deferred.resolve(result); - }, - function onError(error) { - console.error('qrreader checkPermission() error:', error); - - var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog.getString(errorMessage); - deferred.reject(translatedErrorMessage); - }); + deferred.resolve(scannerService.getCapabilities()); return deferred.promise; } From a3d3d2cb27c88fa2eff1c5f1f45a1b10c95f8777 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Sat, 23 Feb 2019 00:55:42 +0900 Subject: [PATCH 52/65] Clean + Fix desktop side --- .../cordova-plugin-qrreader/www/qrreader.js | 60 +++--------- src/js/controllers/tab-scan.controller.js | 9 +- src/js/services/qr-scanner.service.js | 97 ++++++++++--------- www/views/tab-scan.html | 2 +- 4 files changed, 68 insertions(+), 100 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index aab362307..223675471 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -1,53 +1,14 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { + + var argscheck = require('cordova/argscheck'); var channel = require('cordova/channel'); var utils = require('cordova/utils'); var exec = require('cordova/exec'); var cordova = require('cordova'); -/* -channel.createSticky('onCordovaInfoReady'); -// Tell cordova channel to wait on the CordovaInfoReady event -channel.waitForInitialization('onCordovaInfoReady'); -*/ - -function QRReader() { - this.testString = 'hello1'; - /* - this.available = false; - this.platform = null; - this.version = null; - this.uuid = null; - this.cordova = null; - this.model = null; - this.manufacturer = null; - this.isVirtual = null; - this.serial = null; - - var me = this; - channel.onCordovaReady.subscribe(function () { - me.getInfo(function (info) { - // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js - // TODO: CB-5105 native implementations should not return info.cordova - var buildLabel = cordova.version; - me.available = true; - me.platform = info.platform; - me.version = info.version; - me.uuid = info.uuid; - me.cordova = buildLabel; - me.model = info.model; - me.isVirtual = info.isVirtual; - me.manufacturer = info.manufacturer || 'unknown'; - me.serial = info.serial || 'unknown'; - channel.onCordovaInfoReady.fire(); - }, function (e) { - me.available = false; - utils.alert('[ERROR] Error initializing Cordova: ' + e); - }); - }); - */ -} +function QRReader() {} /** @@ -57,17 +18,16 @@ function QRReader() { * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) */ - -QRReader.prototype.getTestInfo = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.getTestInfo', arguments); - exec(successCallback, errorCallback, 'QRReader', 'getTestInfo', []); -}; - QRReader.prototype.openSettings = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); }; +QRReader.prototype.checkPermission = function (successCallback, errorCallback) { + argscheck.checkArgs('fF', 'QRReader.checkPermission', arguments); + exec(successCallback, errorCallback, 'QRReader', 'checkPermission', []); +}; + QRReader.prototype.startReading = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.startReading', arguments); exec(successCallback, errorCallback, 'QRReader', 'startReading', []); @@ -79,4 +39,8 @@ QRReader.prototype.stopReading = function (successCallback, errorCallback) { }; module.exports = new QRReader(); + + + + }); diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 21df0c3c9..048457fed 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -36,6 +36,7 @@ angular $scope.onRetry = onRetry; $scope.scannerStates = scannerStates; $scope.currentState = scannerStates.visible; + $scope.canOpenSettings = isDesktop ? false : true; $scope.$on("$ionicView.enter", function(event, data) { $ionicNavBarDelegate.showBar(true); @@ -83,7 +84,7 @@ angular }); function onRetry() { - startReading(); + startReadingWithPermission(); } function onOpenSettings(){ @@ -99,8 +100,9 @@ angular $scope.canOpenSettings = false; // TODO: Handle all the different types of errors $scope.currentState = newScannerState; - }); - }; + } + ); + } function startReadingWithPermission() { qrService.checkPermission().then(function () { @@ -119,7 +121,6 @@ angular $log.error('Failed to start reading QR code. ' + reason); var newScannerState = scannerStates.denied; - $scope.canOpenSettings = true; // TODO: Handle all the different types of errors $scope.currentState = newScannerState; }); diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js index 3f1fa841e..f38a4ba5b 100644 --- a/src/js/services/qr-scanner.service.js +++ b/src/js/services/qr-scanner.service.js @@ -8,13 +8,8 @@ function qrScannerService( gettextCatalog - , $log - , $timeout - , platformInfo , $q - , $rootScope , $window - , scannerService ) { var errors = { @@ -29,8 +24,9 @@ // iOS }; - var isDesktop = !platformInfo.isCordova; - var qrReader = $window.qrreader; + + var qrService = $window.QRScanner; + var scanDeferred = null; var service = { // Functions @@ -44,41 +40,44 @@ function openSettings() { var deferred = $q.defer(); + console.log('qrscanner openSettings()'); - scannerService.openSettings( - function onSuccess(result) { - console.log('qrscanner openSettings() result:', result); - - deferred.resolve(result); - }, - function onError(error) { - console.error('qrscanner openSettings() error:', error); - - var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog.getString(errorMessage); - deferred.reject(translatedErrorMessage); - }); + // Doesn't do anything + qrService.openSettings(); + // Resolve by default + deferred.resolve(); return deferred.promise; } function startReading() { var deferred = $q.defer(); + stopReading().finally(function() { + qrService.prepare( + function onPrepare(error, status) { + if (error) { + console.error('qrscanner startReading() error:', error); + var errorMessage = errors[error] || error; + var translatedErrorMessage = gettextCatalog.getString(errorMessage); + deferred.reject(translatedErrorMessage); + } else { + console.log('qrscanner startReading() status:', status); + + scanDeferred = deferred; + qrService.scan(function onScan(err, content) { + if (err) { + deferred.reject(err); + } else { + deferred.resolve(content); + } + }); + } + } + ); + + qrService.show(); + }); - scannerService.initialize( - function onSuccess(result) { - console.log('qrscanner startReading() result:', result); - scannerService.scan( function onSuccessScan(content) { - deferred.resolve(content); - }); - }, - function onError(error) { - console.error('qrscanner startReading() error:', error); - - var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog.getString(errorMessage); - deferred.reject(translatedErrorMessage); - }); return deferred.promise; } @@ -87,19 +86,16 @@ // immediately after function stopReading() { var deferred = $q.defer(); - scannerService.deactivate( - function onSuccess(result) { - console.log('qrscanner stopReading() result:', result); - - deferred.resolve(result); - }, - function onError(error) { - console.error('qrscanner stopReading() error:', error); + + if (scanDeferred) { + scanDeferred.reject(); + scanDeferred = null; + } - var errorMessage = errors[error] || error; - var translatedErrorMessage = gettextCatalog.getString(errorMessage); - deferred.reject(translatedErrorMessage); - }); + QRScanner.destroy(function onDestroy(status){ + console.log('qrscanner stopReading() result:', status); + deferred.resolve(); + }); return deferred.promise; } @@ -109,7 +105,14 @@ function checkPermission() { var deferred = $q.defer(); - deferred.resolve(scannerService.getCapabilities()); + // qrService.getStatus(function(status){ + // if(!status.authorized){ + // deferred.reject(status); + // } else { + // deferred.resolve(status); + // } + // }); + deferred.resolve(status); return deferred.promise; } diff --git a/www/views/tab-scan.html b/www/views/tab-scan.html index 59aa3b177..87bce635f 100644 --- a/www/views/tab-scan.html +++ b/www/views/tab-scan.html @@ -16,7 +16,7 @@
You can scan bitcoin addresses, payment requests, paper wallets, and more.
- +
From 2a1fb6b211c5641d2db760a6b749a7b346650707 Mon Sep 17 00:00:00 2001 From: jbdtky Date: Sat, 23 Feb 2019 01:18:25 +0900 Subject: [PATCH 53/65] Fix incoming-data scanner --- src/js/controllers/tab-scan.controller.js | 9 ++++----- src/js/services/incoming-data.service.js | 4 ++-- src/js/services/qr-scanner.service.js | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 048457fed..b7c37ce55 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -48,8 +48,8 @@ angular }); $scope.$on("$ionicView.beforeLeave", function() { - qrService.stopReading(); document.removeEventListener("resume", onResume, true); + qrService.stopReading(); }); function onResume() { @@ -96,10 +96,10 @@ angular function onOpenSettingsRejected(reason) { $log.error('Failed to open settings. ' + reason); - var newScannerState = scannerStates.unavailable; $scope.canOpenSettings = false; + // TODO: Handle all the different types of errors - $scope.currentState = newScannerState; + $scope.currentState = scannerStates.unavailable; } ); } @@ -120,9 +120,8 @@ angular function onStartReadingRejected(reason) { $log.error('Failed to start reading QR code. ' + reason); - var newScannerState = scannerStates.denied; // TODO: Handle all the different types of errors - $scope.currentState = newScannerState; + $scope.currentState = scannerStates.denied; }); } diff --git a/src/js/services/incoming-data.service.js b/src/js/services/incoming-data.service.js index 0832e33a5..5676352ca 100644 --- a/src/js/services/incoming-data.service.js +++ b/src/js/services/incoming-data.service.js @@ -14,7 +14,7 @@ , $log , $state , $rootScope - , scannerService + , qrScannerService , qrReaderService , platformInfo , sendFlowService @@ -41,7 +41,7 @@ cbError(new Error(errorMessage)); } } else { - var qrService = platformInfo.isMobile ? qrReaderService : scannerService; + var qrService = platformInfo.isMobile ? qrReaderService : qrScannerService; qrService.stopReading(); /** diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js index f38a4ba5b..b3830577d 100644 --- a/src/js/services/qr-scanner.service.js +++ b/src/js/services/qr-scanner.service.js @@ -54,10 +54,10 @@ var deferred = $q.defer(); stopReading().finally(function() { qrService.prepare( - function onPrepare(error, status) { - if (error) { - console.error('qrscanner startReading() error:', error); - var errorMessage = errors[error] || error; + function onPrepare(err, status) { + if (err) { + console.error('qrscanner startReading() error:', err); + var errorMessage = errors[err] || err; var translatedErrorMessage = gettextCatalog.getString(errorMessage); deferred.reject(translatedErrorMessage); } else { From 84abf24054260ed0be7a5a094876222378339617 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 25 Feb 2019 16:40:40 +1300 Subject: [PATCH 54/65] Using remote plugin for QR reader. --- app-template/config-template.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app-template/config-template.xml b/app-template/config-template.xml index db6acff36..5d5ba739e 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -79,6 +79,7 @@ + From 418aa257b292587bfe4915a2d3521e1af0bfe77e Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 25 Feb 2019 16:45:21 +1300 Subject: [PATCH 55/65] Updated to migrated plugin. --- app-template/config-template.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-template/config-template.xml b/app-template/config-template.xml index 5d5ba739e..ddbb239c8 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -79,7 +79,7 @@ - + From 02db81513c99f659e1ed218f3f9524995913f716 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 25 Feb 2019 16:53:44 +1300 Subject: [PATCH 56/65] Removed old local QR reader plugin. --- .../cordova-plugin-qrreader/LICENSE | 202 --- .../cordova-plugin-qrreader/package.json | 32 - .../cordova-plugin-qrreader/plugin.xml | 62 - .../cordova-plugin-qrreader/readme.md | 26 - .../src/android/BarcodeMapTracker.java | 89 -- .../src/android/BarcodeMapTrackerFactory.java | 29 - .../src/android/BarcodeUpdateListener.java | 13 - .../src/android/CameraSource.java | 1200 ----------------- .../src/android/CameraSourcePreview.java | 201 --- .../src/android/QRReader.java | 356 ----- .../src/android/qrreader.gradle | 4 - .../src/ios/QRReader.swift | 196 --- .../cordova-plugin-qrreader/www/qrreader.js | 84 -- 13 files changed, 2494 deletions(-) delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/LICENSE delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/package.json delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/readme.md delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift delete mode 100644 plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE b/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE deleted file mode 100644 index 7a4a3ea24..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/package.json b/plugins-bitcoincom/cordova-plugin-qrreader/package.json deleted file mode 100644 index 3b9b910c9..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "cordova-plugin-qrreader", - "version": "0.1.0", - "description": "Cordova QR Code Reader Plugin", - "cordova": { - "id": "cordova-plugin-qrreader", - "platforms": [ - "android" - ] - }, - "keywords": [ - "cordova", - "qrcode", - "ecosystem:cordova", - "cordova-android", - "cordova-ios", - "cordova-windows", - "cordova-browser", - "cordova-osx" - ], - "author": "Bitcoin.com", - "license": "Apache-2.0", - "engines": { - "cordovaDependencies": { - "0.1.0": { - "cordova-androd": ">6.0.0" - } - } - }, - "devDependencies": { - } -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml b/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml deleted file mode 100644 index 61d131fe1..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/plugin.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - QR Reader - Cordova QR Code Reader Plugin - Apache 2.0 - cordova,qrcode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md b/plugins-bitcoincom/cordova-plugin-qrreader/readme.md deleted file mode 100644 index 4cc102721..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/readme.md +++ /dev/null @@ -1,26 +0,0 @@ -1. After cloning, delete the platforms/android folder. It's convenient to use that folder for now when working on the plugin. We will remove it from the repo after the plugin dev is complete. - -2. `npm run apply:bitcoincom` - -3. `node_modules/cordova/bin/cordova plugin add ./plugins-bitcoincom/cordova-plugin-qrreader` - -4. `npm run start:android` it will run, but the scanning may not work. - -5. Add the contents of - - plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle - -to the end of - -platforms/android/build-extras.gradle - -Contents presented here for your convenience: - - dependencies { - // Important - the CameraSource implementation in this project requires version 8.1 or higher. - compile 'com.google.android.gms:play-services-vision:11.8.0' - } - - - 6. `npm run start:android` Scanning should work now. - diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java deleted file mode 100644 index cefdc54f4..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTracker.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.content.Context; -import android.util.Log; - - -import com.google.android.gms.vision.Detector; -import com.google.android.gms.vision.Tracker; -import com.google.android.gms.vision.barcode.Barcode; - -import java.util.HashMap; -import java.util.Map; - -/** - * Generic tracker which is used for tracking or reading a barcode (and can really be used for - * any type of item). This is used to receive newly detected items, add a graphical representation - * to an overlay, update the graphics as the item changes, and remove the graphics when the item - * goes away. - */ -public class BarcodeMapTracker extends Tracker { - - private Map mBarcodes; - private Integer mId; - private BarcodeUpdateListener mBarcodeUpdateListener; - - - - BarcodeMapTracker(Map barcodes, BarcodeUpdateListener listener) { - this.mBarcodes = barcodes; - //this.mOverlay = mOverlay; - //this.mGraphic = mGraphic; - this.mBarcodeUpdateListener = listener; - } - - /** - * Start tracking the detected item instance within the item overlay. - */ - @Override - public void onNewItem(int id, Barcode item) { - //mGraphic.setId(id); - mId = id; - mBarcodes.put(id, item); - Log.d("BarcodeGraphicTracker", "New barcode."); - if (mBarcodeUpdateListener != null) { - mBarcodeUpdateListener.onBarcodeDetected(item); - } - } - - /** - * Update the position/characteristics of the item within the overlay. - */ - @Override - public void onUpdate(Detector.Detections detectionResults, Barcode item) { - //mOverlay.add(mGraphic); - //mGraphic.updateItem(item); - if (mId != null) { - mBarcodes.put(mId, item); - } - } - - /** - * Hide the graphic when the corresponding object was not detected. This can happen for - * intermediate frames temporarily, for example if the object was momentarily blocked from - * view. - */ - @Override - public void onMissing(Detector.Detections detectionResults) { - //mOverlay.remove(mGraphic); - if (mId != null) { - mBarcodes.remove(mId); - mId = null; - } - } - - /** - * Called when the item is assumed to be gone for good. Remove the graphic annotation from - * the overlay. - */ - @Override - public void onDone() { - - //mOverlay.remove(mGraphic); - if (mId != null) { - mBarcodes.remove(mId); - mId = null; - } - } -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java deleted file mode 100644 index e1626d6e3..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeMapTrackerFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bitcoin.cordova.qrreader; - -import android.content.Context; -import com.google.android.gms.vision.MultiProcessor; -import com.google.android.gms.vision.Tracker; -import com.google.android.gms.vision.barcode.Barcode; - -import java.util.Map; - -/** - * Factory for creating a tracker and associated graphic to be associated with a new barcode. The - * multi-processor uses this factory to create barcode trackers as needed -- one for each barcode. - */ -class BarcodeMapTrackerFactory implements MultiProcessor.Factory { - private Map mBarcodes; - private BarcodeUpdateListener mListener; - - public BarcodeMapTrackerFactory(Map mBarcodes, - BarcodeUpdateListener listener) { - this.mBarcodes = mBarcodes; - this.mListener = listener; - } - - @Override - public Tracker create(Barcode barcode) { - return new BarcodeMapTracker(mBarcodes, mListener); - } - -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java deleted file mode 100644 index e32f618d3..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/BarcodeUpdateListener.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.bitcoin.cordova.qrreader; - -import android.support.annotation.UiThread; -import com.google.android.gms.vision.barcode.Barcode; - -/** - * Consume the item instance detected from an Activity or Fragment level by implementing the - * BarcodeUpdateListener interface method onBarcodeDetected. - */ -interface BarcodeUpdateListener { - @UiThread - void onBarcodeDetected(Barcode barcode); -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java deleted file mode 100644 index b8e4edb8b..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSource.java +++ /dev/null @@ -1,1200 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.ImageFormat; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.os.Build; -import android.os.SystemClock; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresPermission; -import android.support.annotation.StringDef; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.WindowManager; - -import com.google.android.gms.common.images.Size; -import com.google.android.gms.vision.Detector; -import com.google.android.gms.vision.Frame; - -import java.io.IOException; -import java.lang.Thread.State; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -// Note: This requires Google Play Services 8.1 or higher, due to using indirect byte buffers for -// storing images. - -/** - * Manages the camera in conjunction with an underlying - * {@link com.google.android.gms.vision.Detector}. This receives preview frames from the camera at - * a specified rate, sending those frames to the detector as fast as it is able to process those - * frames. - *

- * This camera source makes a best effort to manage processing on preview frames as fast as - * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector - * is unable to keep up with the rate of frames generated by the camera. You should use - * {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate that works well with - * the capabilities of the camera hardware and the detector options that you have selected. If CPU - * utilization is higher than you'd like, then you may want to consider reducing FPS. If the camera - * preview or detector results are too "jerky", then you may want to consider increasing FPS. - *

- * The following Android permission is required to use the camera: - *

    - *
  • android.permissions.CAMERA
  • - *
- */ -@SuppressWarnings("deprecation") -public class CameraSource { - @SuppressLint("InlinedApi") - public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK; - @SuppressLint("InlinedApi") - public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT; - - private static final String TAG = "OpenCameraSource"; - - /** - * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL - * context, we can choose any ID we want here. - */ - private static final int DUMMY_TEXTURE_NAME = 100; - - /** - * If the absolute difference between a preview size aspect ratio and a picture size aspect - * ratio is less than this tolerance, they are considered to be the same aspect ratio. - */ - private static final float ASPECT_RATIO_TOLERANCE = 0.01f; - - @StringDef({ - Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, - Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, - Camera.Parameters.FOCUS_MODE_AUTO, - Camera.Parameters.FOCUS_MODE_EDOF, - Camera.Parameters.FOCUS_MODE_FIXED, - Camera.Parameters.FOCUS_MODE_INFINITY, - Camera.Parameters.FOCUS_MODE_MACRO - }) - @Retention(RetentionPolicy.SOURCE) - private @interface FocusMode {} - - @StringDef({ - Camera.Parameters.FLASH_MODE_ON, - Camera.Parameters.FLASH_MODE_OFF, - Camera.Parameters.FLASH_MODE_AUTO, - Camera.Parameters.FLASH_MODE_RED_EYE, - Camera.Parameters.FLASH_MODE_TORCH - }) - @Retention(RetentionPolicy.SOURCE) - private @interface FlashMode {} - - private Context mContext; - - private final Object mCameraLock = new Object(); - - // Guarded by mCameraLock - private Camera mCamera; - - private int mFacing = CAMERA_FACING_BACK; - - /** - * Rotation of the device, and thus the associated preview images captured from the device. - * See {@link Frame.Metadata#getRotation()}. - */ - private int mRotation; - - private Size mPreviewSize; - - // These values may be requested by the caller. Due to hardware limitations, we may need to - // select close, but not exactly the same values for these. - private float mRequestedFps = 30.0f; - private int mRequestedPreviewWidth = 1024; - private int mRequestedPreviewHeight = 768; - - - private String mFocusMode = null; - private String mFlashMode = null; - - // These instances need to be held onto to avoid GC of their underlying resources. Even though - // these aren't used outside of the method that creates them, they still must have hard - // references maintained to them. - private SurfaceView mDummySurfaceView; - private SurfaceTexture mDummySurfaceTexture; - - /** - * Dedicated thread and associated runnable for calling into the detector with frames, as the - * frames become available from the camera. - */ - private Thread mProcessingThread; - private FrameProcessingRunnable mFrameProcessor; - - /** - * Map to convert between a byte array, received from the camera, and its associated byte - * buffer. We use byte buffers internally because this is a more efficient way to call into - * native code later (avoids a potential copy). - */ - private Map mBytesToByteBuffer = new HashMap(); - - //============================================================================================== - // Builder - //============================================================================================== - - /** - * Builder for configuring and creating an associated camera source. - */ - public static class Builder { - private final Detector mDetector; - private CameraSource mCameraSource = new CameraSource(); - - /** - * Creates a camera source builder with the supplied context and detector. Camera preview - * images will be streamed to the associated detector upon starting the camera source. - */ - public Builder(Context context, Detector detector) { - if (context == null) { - throw new IllegalArgumentException("No context supplied."); - } - if (detector == null) { - throw new IllegalArgumentException("No detector supplied."); - } - - mDetector = detector; - mCameraSource.mContext = context; - } - - /** - * Sets the requested frame rate in frames per second. If the exact requested value is not - * not available, the best matching available value is selected. Default: 30. - */ - public Builder setRequestedFps(float fps) { - if (fps <= 0) { - throw new IllegalArgumentException("Invalid fps: " + fps); - } - mCameraSource.mRequestedFps = fps; - return this; - } - - public Builder setFocusMode(@FocusMode String mode) { - mCameraSource.mFocusMode = mode; - return this; - } - - public Builder setFlashMode(@FlashMode String mode) { - mCameraSource.mFlashMode = mode; - return this; - } - - /** - * Sets the desired width and height of the camera frames in pixels. If the exact desired - * values are not available options, the best matching available options are selected. - * Also, we try to select a preview size which corresponds to the aspect ratio of an - * associated full picture size, if applicable. Default: 1024x768. - */ - public Builder setRequestedPreviewSize(int width, int height) { - // Restrict the requested range to something within the realm of possibility. The - // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that - // devices can support. We bound this to avoid int overflow in the code later. - final int MAX = 1000000; - if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) { - throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height); - } - mCameraSource.mRequestedPreviewWidth = width; - mCameraSource.mRequestedPreviewHeight = height; - return this; - } - - /** - * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or - * {@link #CAMERA_FACING_FRONT}). Default: back facing. - */ - public Builder setFacing(int facing) { - if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) { - throw new IllegalArgumentException("Invalid camera: " + facing); - } - mCameraSource.mFacing = facing; - return this; - } - - /** - * Creates an instance of the camera source. - */ - public CameraSource build() { - mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector); - return mCameraSource; - } - } - - //============================================================================================== - // Bridge Functionality for the Camera1 API - //============================================================================================== - - /** - * Callback interface used to signal the moment of actual image capture. - */ - public interface ShutterCallback { - /** - * Called as near as possible to the moment when a photo is captured from the sensor. This - * is a good opportunity to play a shutter sound or give other feedback of camera operation. - * This may be some time after the photo was triggered, but some time before the actual data - * is available. - */ - void onShutter(); - } - - /** - * Callback interface used to supply image data from a photo capture. - */ - public interface PictureCallback { - /** - * Called when image data is available after a picture is taken. The format of the data - * is a jpeg binary. - */ - void onPictureTaken(byte[] data); - } - - /** - * Callback interface used to notify on completion of camera auto focus. - */ - public interface AutoFocusCallback { - /** - * Called when the camera auto focus completes. If the camera - * does not support auto-focus and autoFocus is called, - * onAutoFocus will be called immediately with a fake value of - * success set to true. - *

- * The auto-focus routine does not lock auto-exposure and auto-white - * balance after it completes. - * - * @param success true if focus was successful, false if otherwise - */ - void onAutoFocus(boolean success); - } - - /** - * Callback interface used to notify on auto focus start and stop. - *

- *

This is only supported in continuous autofocus modes -- {@link - * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link - * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show - * autofocus animation based on this.

- */ - public interface AutoFocusMoveCallback { - /** - * Called when the camera auto focus starts or stops. - * - * @param start true if focus starts to move, false if focus stops to move - */ - void onAutoFocusMoving(boolean start); - } - - //============================================================================================== - // Public - //============================================================================================== - - /** - * Stops the camera and releases the resources of the camera and underlying detector. - */ - public void release() { - synchronized (mCameraLock) { - stop(); - mFrameProcessor.release(); - } - } - - /** - * Opens the camera and starts sending preview frames to the underlying detector. The preview - * frames are not displayed. - * - * @throws IOException if the camera's preview texture or display could not be initialized - */ - @RequiresPermission(Manifest.permission.CAMERA) - public CameraSource start() throws IOException { - synchronized (mCameraLock) { - if (mCamera != null) { - return this; - } - - mCamera = createCamera(); - - // SurfaceTexture was introduced in Honeycomb (11), so if we are running and - // old version of Android. fall back to use SurfaceView. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); - mCamera.setPreviewTexture(mDummySurfaceTexture); - } else { - mDummySurfaceView = new SurfaceView(mContext); - mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); - } - mCamera.startPreview(); - - mProcessingThread = new Thread(mFrameProcessor); - mFrameProcessor.setActive(true); - mProcessingThread.start(); - } - return this; - } - - /** - * Opens the camera and starts sending preview frames to the underlying detector. The supplied - * surface holder is used for the preview so frames can be displayed to the user. - * - * @param surfaceHolder the surface holder to use for the preview frames - * @throws IOException if the supplied surface holder could not be used as the preview display - */ - @RequiresPermission(Manifest.permission.CAMERA) - public CameraSource start(SurfaceHolder surfaceHolder) throws IOException { - synchronized (mCameraLock) { - if (mCamera != null) { - return this; - } - - mCamera = createCamera(); - mCamera.setPreviewDisplay(surfaceHolder); - mCamera.startPreview(); - - mProcessingThread = new Thread(mFrameProcessor); - mFrameProcessor.setActive(true); - mProcessingThread.start(); - } - return this; - } - - /** - * Closes the camera and stops sending frames to the underlying frame detector. - *

- * This camera source may be restarted again by calling {@link #start()} or - * {@link #start(SurfaceHolder)}. - *

- * Call {@link #release()} instead to completely shut down this camera source and release the - * resources of the underlying detector. - */ - public void stop() { - synchronized (mCameraLock) { - mFrameProcessor.setActive(false); - if (mProcessingThread != null) { - try { - // Wait for the thread to complete to ensure that we can't have multiple threads - // executing at the same time (i.e., which would happen if we called start too - // quickly after stop). - mProcessingThread.join(); - } catch (InterruptedException e) { - Log.d(TAG, "Frame processing thread interrupted on release."); - } - mProcessingThread = null; - } - - // clear the buffer to prevent oom exceptions - mBytesToByteBuffer.clear(); - - if (mCamera != null) { - mCamera.stopPreview(); - mCamera.setPreviewCallbackWithBuffer(null); - try { - // We want to be compatible back to Gingerbread, but SurfaceTexture - // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the - // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't - // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mCamera.setPreviewTexture(null); - - } else { - mCamera.setPreviewDisplay(null); - } - } catch (Exception e) { - Log.e(TAG, "Failed to clear camera preview: " + e); - } - mCamera.release(); - mCamera = null; - } - } - } - - /** - * Returns the preview size that is currently in use by the underlying camera. - */ - public Size getPreviewSize() { - return mPreviewSize; - } - - /** - * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or - * {@link #CAMERA_FACING_FRONT}. - */ - public int getCameraFacing() { - return mFacing; - } - - public int doZoom(float scale) { - synchronized (mCameraLock) { - if (mCamera == null) { - return 0; - } - int currentZoom = 0; - int maxZoom; - Camera.Parameters parameters = mCamera.getParameters(); - if (!parameters.isZoomSupported()) { - Log.w(TAG, "Zoom is not supported on this device"); - return currentZoom; - } - maxZoom = parameters.getMaxZoom(); - - currentZoom = parameters.getZoom() + 1; - float newZoom; - if (scale > 1) { - newZoom = currentZoom + scale * (maxZoom / 10); - } else { - newZoom = currentZoom * scale; - } - currentZoom = Math.round(newZoom) - 1; - if (currentZoom < 0) { - currentZoom = 0; - } else if (currentZoom > maxZoom) { - currentZoom = maxZoom; - } - parameters.setZoom(currentZoom); - mCamera.setParameters(parameters); - return currentZoom; - } - } - - /** - * Initiates taking a picture, which happens asynchronously. The camera source should have been - * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera - * preview is suspended while the picture is being taken, but will resume once picture taking is - * done. - * - * @param shutter the callback for image capture moment, or null - * @param jpeg the callback for JPEG image data, or null - */ - public void takePicture(ShutterCallback shutter, PictureCallback jpeg) { - synchronized (mCameraLock) { - if (mCamera != null) { - PictureStartCallback startCallback = new PictureStartCallback(); - startCallback.mDelegate = shutter; - PictureDoneCallback doneCallback = new PictureDoneCallback(); - doneCallback.mDelegate = jpeg; - mCamera.takePicture(startCallback, null, null, doneCallback); - } - } - } - - /** - * Gets the current focus mode setting. - * - * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link - * #autoFocus(AutoFocusCallback)} to start the focus if focus - * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO. - * @see Camera.Parameters#FOCUS_MODE_AUTO - * @see Camera.Parameters#FOCUS_MODE_INFINITY - * @see Camera.Parameters#FOCUS_MODE_MACRO - * @see Camera.Parameters#FOCUS_MODE_FIXED - * @see Camera.Parameters#FOCUS_MODE_EDOF - * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO - * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE - */ - @Nullable - @FocusMode - public String getFocusMode() { - return mFocusMode; - } - - /** - * Sets the focus mode. - * - * @param mode the focus mode - * @return {@code true} if the focus mode is set, {@code false} otherwise - * @see #getFocusMode() - */ - public boolean setFocusMode(@FocusMode String mode) { - synchronized (mCameraLock) { - if (mCamera != null && mode != null) { - Camera.Parameters parameters = mCamera.getParameters(); - if (parameters.getSupportedFocusModes().contains(mode)) { - parameters.setFocusMode(mode); - mCamera.setParameters(parameters); - mFocusMode = mode; - return true; - } - } - - return false; - } - } - - /** - * Gets the current flash mode setting. - * - * @return current flash mode. null if flash mode setting is not - * supported or the camera is not yet created. - * @see Camera.Parameters#FLASH_MODE_OFF - * @see Camera.Parameters#FLASH_MODE_AUTO - * @see Camera.Parameters#FLASH_MODE_ON - * @see Camera.Parameters#FLASH_MODE_RED_EYE - * @see Camera.Parameters#FLASH_MODE_TORCH - */ - @Nullable - @FlashMode - public String getFlashMode() { - return mFlashMode; - } - - /** - * Sets the flash mode. - * - * @param mode flash mode. - * @return {@code true} if the flash mode is set, {@code false} otherwise - * @see #getFlashMode() - */ - public boolean setFlashMode(@FlashMode String mode) { - synchronized (mCameraLock) { - if (mCamera != null && mode != null) { - Camera.Parameters parameters = mCamera.getParameters(); - if (parameters.getSupportedFlashModes().contains(mode)) { - parameters.setFlashMode(mode); - mCamera.setParameters(parameters); - mFlashMode = mode; - return true; - } - } - - return false; - } - } - - /** - * Starts camera auto-focus and registers a callback function to run when - * the camera is focused. This method is only valid when preview is active - * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}). - *

- *

Callers should check - * {@link #getFocusMode()} to determine if - * this method should be called. If the camera does not support auto-focus, - * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)} - * callback will be called immediately. - *

- *

If the current flash mode is not - * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be - * fired during auto-focus, depending on the driver and camera hardware.

- * - * @param cb the callback to run - * @see #cancelAutoFocus() - */ - public void autoFocus(@Nullable AutoFocusCallback cb) { - synchronized (mCameraLock) { - if (mCamera != null) { - CameraAutoFocusCallback autoFocusCallback = null; - if (cb != null) { - autoFocusCallback = new CameraAutoFocusCallback(); - autoFocusCallback.mDelegate = cb; - } - mCamera.autoFocus(autoFocusCallback); - } - } - } - - /** - * Cancels any auto-focus function in progress. - * Whether or not auto-focus is currently in progress, - * this function will return the focus position to the default. - * If the camera does not support auto-focus, this is a no-op. - * - * @see #autoFocus(AutoFocusCallback) - */ - public void cancelAutoFocus() { - synchronized (mCameraLock) { - if (mCamera != null) { - mCamera.cancelAutoFocus(); - } - } - } - - /** - * Sets camera auto-focus move callback. - * - * @param cb the callback to run - * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - return false; - } - - synchronized (mCameraLock) { - if (mCamera != null) { - CameraAutoFocusMoveCallback autoFocusMoveCallback = null; - if (cb != null) { - autoFocusMoveCallback = new CameraAutoFocusMoveCallback(); - autoFocusMoveCallback.mDelegate = cb; - } - mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback); - } - } - - return true; - } - - //============================================================================================== - // Private - //============================================================================================== - - /** - * Only allow creation via the builder class. - */ - private CameraSource() { - } - - /** - * Wraps the camera1 shutter callback so that the deprecated API isn't exposed. - */ - private class PictureStartCallback implements Camera.ShutterCallback { - private ShutterCallback mDelegate; - - @Override - public void onShutter() { - if (mDelegate != null) { - mDelegate.onShutter(); - } - } - } - - /** - * Wraps the final callback in the camera sequence, so that we can automatically turn the camera - * preview back on after the picture has been taken. - */ - private class PictureDoneCallback implements Camera.PictureCallback { - private PictureCallback mDelegate; - - @Override - public void onPictureTaken(byte[] data, Camera camera) { - if (mDelegate != null) { - mDelegate.onPictureTaken(data); - } - synchronized (mCameraLock) { - if (mCamera != null) { - mCamera.startPreview(); - } - } - } - } - - /** - * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed. - */ - private class CameraAutoFocusCallback implements Camera.AutoFocusCallback { - private AutoFocusCallback mDelegate; - - @Override - public void onAutoFocus(boolean success, Camera camera) { - if (mDelegate != null) { - mDelegate.onAutoFocus(success); - } - } - } - - /** - * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback { - private AutoFocusMoveCallback mDelegate; - - @Override - public void onAutoFocusMoving(boolean start, Camera camera) { - if (mDelegate != null) { - mDelegate.onAutoFocusMoving(start); - } - } - } - - /** - * Opens the camera and applies the user settings. - * - * @throws RuntimeException if the method fails - */ - @SuppressLint("InlinedApi") - private Camera createCamera() { - int requestedCameraId = getIdForRequestedCamera(mFacing); - if (requestedCameraId == -1) { - throw new RuntimeException("Could not find requested camera."); - } - Camera camera = Camera.open(requestedCameraId); - - SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight); - if (sizePair == null) { - throw new RuntimeException("Could not find suitable preview size."); - } - Size pictureSize = sizePair.pictureSize(); - mPreviewSize = sizePair.previewSize(); - - int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps); - if (previewFpsRange == null) { - throw new RuntimeException("Could not find suitable preview frames per second range."); - } - - Camera.Parameters parameters = camera.getParameters(); - - if (pictureSize != null) { - parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); - } - - parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); - parameters.setPreviewFpsRange( - previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], - previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); - parameters.setPreviewFormat(ImageFormat.NV21); - - setRotation(camera, parameters, requestedCameraId); - - if (mFocusMode != null) { - if (parameters.getSupportedFocusModes().contains( - mFocusMode)) { - parameters.setFocusMode(mFocusMode); - } else { - Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device."); - } - } - - // setting mFocusMode to the one set in the params - mFocusMode = parameters.getFocusMode(); - - if (mFlashMode != null) { - if (parameters.getSupportedFlashModes() != null) { - if (parameters.getSupportedFlashModes().contains( - mFlashMode)) { - parameters.setFlashMode(mFlashMode); - } else { - Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device."); - } - } - } - - // setting mFlashMode to the one set in the params - mFlashMode = parameters.getFlashMode(); - - camera.setParameters(parameters); - - // Four frame buffers are needed for working with the camera: - // - // one for the frame that is currently being executed upon in doing detection - // one for the next pending frame to process immediately upon completing detection - // two for the frames that the camera uses to populate future preview images - camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - - return camera; - } - - /** - * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such - * camera was found. - * - * @param facing the desired camera (front-facing or rear-facing) - */ - private static int getIdForRequestedCamera(int facing) { - CameraInfo cameraInfo = new CameraInfo(); - for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == facing) { - return i; - } - } - return -1; - } - - /** - * Selects the most suitable preview and picture size, given the desired width and height. - *

- * Even though we may only need the preview size, it's necessary to find both the preview - * size and the picture size of the camera together, because these need to have the same aspect - * ratio. On some hardware, if you would only set the preview size, you will get a distorted - * image. - * - * @param camera the camera to select a preview size from - * @param desiredWidth the desired width of the camera preview frames - * @param desiredHeight the desired height of the camera preview frames - * @return the selected preview and picture size pair - */ - private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { - List validPreviewSizes = generateValidPreviewSizeList(camera); - - // The method for selecting the best size is to minimize the sum of the differences between - // the desired values and the actual values for width and height. This is certainly not the - // only way to select the best size, but it provides a decent tradeoff between using the - // closest aspect ratio vs. using the closest pixel area. - SizePair selectedPair = null; - int minDiff = Integer.MAX_VALUE; - for (SizePair sizePair : validPreviewSizes) { - Size size = sizePair.previewSize(); - int diff = Math.abs(size.getWidth() - desiredWidth) + - Math.abs(size.getHeight() - desiredHeight); - if (diff < minDiff) { - selectedPair = sizePair; - minDiff = diff; - } - } - - return selectedPair; - } - - /** - * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted - * preview images on some devices, the picture size must be set to a size that is the same - * aspect ratio as the preview size or the preview may end up being distorted. If the picture - * size is null, then there is no picture size with the same aspect ratio as the preview size. - */ - private static class SizePair { - private Size mPreview; - private Size mPicture; - - public SizePair(android.hardware.Camera.Size previewSize, - android.hardware.Camera.Size pictureSize) { - mPreview = new Size(previewSize.width, previewSize.height); - if (pictureSize != null) { - mPicture = new Size(pictureSize.width, pictureSize.height); - } - } - - public Size previewSize() { - return mPreview; - } - - @SuppressWarnings("unused") - public Size pictureSize() { - return mPicture; - } - } - - /** - * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is - * not a corresponding picture size of the same aspect ratio. If there is a corresponding - * picture size of the same aspect ratio, the picture size is paired up with the preview size. - *

- * This is necessary because even if we don't use still pictures, the still picture size must be - * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the - * preview images may be distorted on some devices. - */ - private static List generateValidPreviewSizeList(Camera camera) { - Camera.Parameters parameters = camera.getParameters(); - List supportedPreviewSizes = - parameters.getSupportedPreviewSizes(); - List supportedPictureSizes = - parameters.getSupportedPictureSizes(); - List validPreviewSizes = new ArrayList(); - for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { - float previewAspectRatio = (float) previewSize.width / (float) previewSize.height; - - // By looping through the picture sizes in order, we favor the higher resolutions. - // We choose the highest resolution in order to support taking the full resolution - // picture later. - for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) { - float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height; - if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { - validPreviewSizes.add(new SizePair(previewSize, pictureSize)); - break; - } - } - } - - // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all - // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we - // still account for it. - if (validPreviewSizes.size() == 0) { - Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size"); - for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { - // The null picture size will let us know that we shouldn't set a picture size. - validPreviewSizes.add(new SizePair(previewSize, null)); - } - } - - return validPreviewSizes; - } - - /** - * Selects the most suitable preview frames per second range, given the desired frames per - * second. - * - * @param camera the camera to select a frames per second range from - * @param desiredPreviewFps the desired frames per second for the camera preview frames - * @return the selected preview frames per second range - */ - private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) { - // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame - // rates. - int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f); - - // The method for selecting the best range is to minimize the sum of the differences between - // the desired value and the upper and lower bounds of the range. This may select a range - // that the desired value is outside of, but this is often preferred. For example, if the - // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the - // range (15, 30). - int[] selectedFpsRange = null; - int minDiff = Integer.MAX_VALUE; - List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange(); - for (int[] range : previewFpsRangeList) { - int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; - int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; - int diff = Math.abs(deltaMin) + Math.abs(deltaMax); - if (diff < minDiff) { - selectedFpsRange = range; - minDiff = diff; - } - } - return selectedFpsRange; - } - - /** - * Calculates the correct rotation for the given camera id and sets the rotation in the - * parameters. It also sets the camera's display orientation and rotation. - * - * @param parameters the camera parameters for which to set the rotation - * @param cameraId the camera id to set rotation based on - */ - private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) { - WindowManager windowManager = - (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - int degrees = 0; - int rotation = windowManager.getDefaultDisplay().getRotation(); - switch (rotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 90; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 270; - break; - default: - Log.e(TAG, "Bad rotation value: " + rotation); - } - - CameraInfo cameraInfo = new CameraInfo(); - Camera.getCameraInfo(cameraId, cameraInfo); - - int angle; - int displayAngle; - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - angle = (cameraInfo.orientation + degrees) % 360; - displayAngle = (360 - angle) % 360; // compensate for it being mirrored - } else { // back-facing - angle = (cameraInfo.orientation - degrees + 360) % 360; - displayAngle = angle; - } - - // This corresponds to the rotation constants in {@link Frame}. - mRotation = angle / 90; - - camera.setDisplayOrientation(displayAngle); - parameters.setRotation(angle); - } - - /** - * Creates one buffer for the camera preview callback. The size of the buffer is based off of - * the camera preview size and the format of the camera image. - * - * @return a new preview buffer of the appropriate size for the current camera settings - */ - private byte[] createPreviewBuffer(Size previewSize) { - int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); - long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; - int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1; - - // - // NOTICE: This code only works when using play services v. 8.1 or higher. - // - - // Creating the byte array this way and wrapping it, as opposed to using .allocate(), - // should guarantee that there will be an array to work with. - byte[] byteArray = new byte[bufferSize]; - ByteBuffer buffer = ByteBuffer.wrap(byteArray); - if (!buffer.hasArray() || (buffer.array() != byteArray)) { - // I don't think that this will ever happen. But if it does, then we wouldn't be - // passing the preview content to the underlying detector later. - throw new IllegalStateException("Failed to create valid buffer for camera source."); - } - - mBytesToByteBuffer.put(byteArray, buffer); - return byteArray; - } - - //============================================================================================== - // Frame processing - //============================================================================================== - - /** - * Called when the camera has a new preview frame. - */ - private class CameraPreviewCallback implements Camera.PreviewCallback { - @Override - public void onPreviewFrame(byte[] data, Camera camera) { - mFrameProcessor.setNextFrame(data, camera); - } - } - - /** - * This runnable controls access to the underlying receiver, calling it to process frames when - * available from the camera. This is designed to run detection on frames as fast as possible - * (i.e., without unnecessary context switching or waiting on the next frame). - *

- * While detection is running on a frame, new frames may be received from the camera. As these - * frames come in, the most recent frame is held onto as pending. As soon as detection and its - * associated processing are done for the previous frame, detection on the mostly recently - * received frame will immediately start on the same thread. - */ - private class FrameProcessingRunnable implements Runnable { - private Detector mDetector; - private long mStartTimeMillis = SystemClock.elapsedRealtime(); - - // This lock guards all of the member variables below. - private final Object mLock = new Object(); - private boolean mActive = true; - - // These pending variables hold the state associated with the new frame awaiting processing. - private long mPendingTimeMillis; - private int mPendingFrameId = 0; - private ByteBuffer mPendingFrameData; - - FrameProcessingRunnable(Detector detector) { - mDetector = detector; - } - - /** - * Releases the underlying receiver. This is only safe to do after the associated thread - * has completed, which is managed in camera source's release method above. - */ - @SuppressLint("Assert") - void release() { - assert (mProcessingThread.getState() == State.TERMINATED); - mDetector.release(); - mDetector = null; - } - - /** - * Marks the runnable as active/not active. Signals any blocked threads to continue. - */ - void setActive(boolean active) { - synchronized (mLock) { - mActive = active; - mLock.notifyAll(); - } - } - - /** - * Sets the frame data received from the camera. This adds the previous unused frame buffer - * (if present) back to the camera, and keeps a pending reference to the frame data for - * future use. - */ - void setNextFrame(byte[] data, Camera camera) { - synchronized (mLock) { - if (mPendingFrameData != null) { - camera.addCallbackBuffer(mPendingFrameData.array()); - mPendingFrameData = null; - } - - if (!mBytesToByteBuffer.containsKey(data)) { - Log.d(TAG, - "Skipping frame. Could not find ByteBuffer associated with the image " + - "data from the camera."); - return; - } - - // Timestamp and frame ID are maintained here, which will give downstream code some - // idea of the timing of frames received and when frames were dropped along the way. - mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis; - mPendingFrameId++; - mPendingFrameData = mBytesToByteBuffer.get(data); - - // Notify the processor thread if it is waiting on the next frame (see below). - mLock.notifyAll(); - } - } - - /** - * As long as the processing thread is active, this executes detection on frames - * continuously. The next pending frame is either immediately available or hasn't been - * received yet. Once it is available, we transfer the frame info to local variables and - * run detection on that frame. It immediately loops back for the next frame without - * pausing. - *

- * If detection takes longer than the time in between new frames from the camera, this will - * mean that this loop will run without ever waiting on a frame, avoiding any context - * switching or frame acquisition time latency. - *

- * If you find that this is using more CPU than you'd like, you should probably decrease the - * FPS setting above to allow for some idle time in between frames. - */ - @Override - public void run() { - Frame outputFrame; - ByteBuffer data; - - while (true) { - synchronized (mLock) { - while (mActive && (mPendingFrameData == null)) { - try { - // Wait for the next frame to be received from the camera, since we - // don't have it yet. - mLock.wait(); - } catch (InterruptedException e) { - Log.d(TAG, "Frame processing loop terminated.", e); - return; - } - } - - if (!mActive) { - // Exit the loop once this camera source is stopped or released. We check - // this here, immediately after the wait() above, to handle the case where - // setActive(false) had been called, triggering the termination of this - // loop. - return; - } - - outputFrame = new Frame.Builder() - .setImageData(mPendingFrameData, mPreviewSize.getWidth(), - mPreviewSize.getHeight(), ImageFormat.NV21) - .setId(mPendingFrameId) - .setTimestampMillis(mPendingTimeMillis) - .setRotation(mRotation) - .build(); - - // Hold onto the frame data locally, so that we can use this for detection - // below. We need to clear mPendingFrameData to ensure that this buffer isn't - // recycled back to the camera before we are done using that data. - data = mPendingFrameData; - mPendingFrameData = null; - } - - // The code below needs to run outside of synchronization, because this will allow - // the camera to add pending frame(s) while we are running detection on the current - // frame. - - try { - mDetector.receiveFrame(outputFrame); - } catch (Throwable t) { - Log.e(TAG, "Exception thrown from receiver.", t); - } finally { - mCamera.addCallbackBuffer(data.array()); - } - } - } - } -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java deleted file mode 100644 index 258628ef8..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/CameraSourcePreview.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.Manifest; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Color; -import android.support.annotation.RequiresPermission; -import android.util.AttributeSet; -import android.util.Log; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.ViewGroup; - -import com.google.android.gms.common.images.Size; - -import java.io.IOException; - -public class CameraSourcePreview extends ViewGroup { - private static final String TAG = "CameraSourcePreview"; - - private Context mContext; - private SurfaceView mSurfaceView; - private boolean mStartRequested; - private boolean mSurfaceAvailable; - private CameraSource mCameraSource; - - //private GraphicOverlay mOverlay; - - public CameraSourcePreview(Context context) { - super(context); - mContext = context; - mStartRequested = false; - mSurfaceAvailable = false; - - setBackgroundColor(Color.BLACK); - - mSurfaceView = new SurfaceView(context); - mSurfaceView.getHolder().addCallback(new SurfaceCallback()); - addView(mSurfaceView); - } - - public CameraSourcePreview(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - mStartRequested = false; - mSurfaceAvailable = false; - - mSurfaceView = new SurfaceView(context); - mSurfaceView.getHolder().addCallback(new SurfaceCallback()); - addView(mSurfaceView); - } - - @RequiresPermission(Manifest.permission.CAMERA) - public void start(CameraSource cameraSource) throws IOException, SecurityException { - if (cameraSource == null) { - stop(); - } - - mCameraSource = cameraSource; - - if (mCameraSource != null) { - mStartRequested = true; - startIfReady(); - } - } - - /* - @RequiresPermission(Manifest.permission.CAMERA) - public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException { - mOverlay = overlay; - start(cameraSource); - } - */ - - public void stop() { - if (mCameraSource != null) { - mCameraSource.stop(); - } - } - - public void release() { - if (mCameraSource != null) { - mCameraSource.release(); - mCameraSource = null; - } - } - - @RequiresPermission(Manifest.permission.CAMERA) - private void startIfReady() throws IOException, SecurityException { - if (mStartRequested && mSurfaceAvailable) { - mCameraSource.start(mSurfaceView.getHolder()); - /* - if (mOverlay != null) { - Size size = mCameraSource.getPreviewSize(); - int min = Math.min(size.getWidth(), size.getHeight()); - int max = Math.max(size.getWidth(), size.getHeight()); - if (isPortraitMode()) { - // Swap width and height sizes when in portrait, since it will be rotated by - // 90 degrees - mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing()); - } else { - mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing()); - } - mOverlay.clear(); - } - */ - mStartRequested = false; - } - } - - private class SurfaceCallback implements SurfaceHolder.Callback { - @Override - public void surfaceCreated(SurfaceHolder surface) { - mSurfaceAvailable = true; - try { - startIfReady(); - } catch (SecurityException se) { - Log.e(TAG,"Do not have permission to start the camera", se); - } catch (IOException e) { - Log.e(TAG, "Could not start camera source.", e); - } - } - - @Override - public void surfaceDestroyed(SurfaceHolder surface) { - mSurfaceAvailable = false; - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int width = 320; - int height = 240; - if (mCameraSource != null) { - Size size = mCameraSource.getPreviewSize(); - if (size != null) { - width = size.getWidth(); - height = size.getHeight(); - } - } - - // Swap width and height sizes when in portrait, since it will be rotated 90 degrees - if (isPortraitMode()) { - int tmp = width; - //noinspection SuspiciousNameCombination - width = height; - height = tmp; - } - - final int layoutWidth = right - left; - final int layoutHeight = bottom - top; - - // Computes height and width for potentially doing fit width. - int childWidth = layoutWidth; - int childHeight = (int)(((float) layoutWidth / (float) width) * height); - - // If height is too tall using fit width, does fit height instead. - if (childHeight > layoutHeight) { - childHeight = layoutHeight; - childWidth = (int)(((float) layoutHeight / (float) height) * width); - } - - // Centre the child in the preview bounds - int childTop = (layoutHeight - childHeight) / 2; - int childBottom = childTop + childHeight; - - int childLeft = (layoutWidth - childWidth) / 2; - int childRight = childLeft + childWidth; - - for (int i = 0; i < getChildCount(); ++i) { - getChildAt(i).layout(childLeft, childTop, childRight, childBottom); - } - - try { - startIfReady(); - } catch (SecurityException se) { - Log.e(TAG,"Do not have permission to start the camera", se); - } catch (IOException e) { - Log.e(TAG, "Could not start camera source.", e); - } - } - - private boolean isPortraitMode() { - int orientation = mContext.getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - return false; - } - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - return true; - } - - Log.d(TAG, "isPortraitMode returning false by default"); - return false; - } -} - diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java deleted file mode 100644 index 296e6a7c6..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/QRReader.java +++ /dev/null @@ -1,356 +0,0 @@ - -package com.bitcoin.cordova.qrreader; - - - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.hardware.Camera; -import android.net.Uri; -import android.os.Build; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.vision.MultiProcessor; -import com.google.android.gms.vision.barcode.Barcode; -import com.google.android.gms.vision.barcode.BarcodeDetector; - -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaInterface; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.provider.Settings; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.Toast; - - - -public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { - public static final String TAG = "QRReader"; - - public static String platform; // Device OS - public static String uuid; // Device UUID - - enum QRReaderError { - ERROR_PERMISSION_DENIED, - ERROR_SCANNING_UNSUPPORTED, - ERROR_OPEN_SETTINGS_UNAVAILABLE - } - - public static final String CAMERA = Manifest.permission.CAMERA; - public static final int CAMERA_REQ_CODE = 774980; - - // intent request code to handle updating play services if needed. - private static final int RC_HANDLE_GMS = 9001; - - - private Map mBarcodes = new HashMap(); - private CameraSource mCameraSource; - private CameraSourcePreview mCameraSourcePreview; - private CallbackContext mStartCallbackContext; - - public QRReader() { - } - - - public void initialize(CordovaInterface cordova, CordovaWebView webView) { - super.initialize(cordova, webView); - //QRReader.uuid = getUuid(); - - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - } - - }); - } - - - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - Log.d(TAG, "execute() with \"" + action + "\""); - if ("openSettings".equals(action)) { - openSettings(callbackContext); - } else if ("startReading".equals(action)) { - startReading(callbackContext); - } else if ("stopReading".equals(action)) { - stopReading(callbackContext); - } else if ("checkPermission".equals(action)) { - checkPermission(callbackContext); - } else { - return false; - } - return true; - } - - @Override - public void onBarcodeDetected(Barcode barcode) { - String contents = barcode.rawValue; - Log.d(TAG, "Detected new barcode."); - if (mStartCallbackContext != null) { - mStartCallbackContext.success(contents); - } else { - Log.e(TAG, "No callback context when detecting new barcode."); - } - } - - - /** - * Creates and starts the camera. Note that this uses a higher resolution in comparison - * to other detection examples to enable the barcode detector to detect small barcodes - * at long distances. - * - * Suppressing InlinedApi since there is a check that the minimum version is met before using - * the constant. - */ - @SuppressLint("InlinedApi") - private Boolean createCameraSource(Context context, boolean useFlash, CallbackContext callbackContext) { - - boolean autoFocus = true; - // A barcode detector is created to track barcodes. An associated multi-processor instance - // is set to receive the barcode detection results, track the barcodes, and maintain - // graphics for each barcode on screen. The factory is used by the multi-processor to - // create a separate tracker instance for each barcode. - BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); - BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, this); - barcodeDetector.setProcessor( - new MultiProcessor.Builder(barcodeFactory).build()); - - if (!barcodeDetector.isOperational()) { - // Note: The first time that an app using the barcode or face API is installed on a - // device, GMS will download a native libraries to the device in order to do detection. - // Usually this completes before the app is run for the first time. But if that - // download has not yet completed, then the above call will not detect any barcodes - // and/or faces. - // - // isOperational() can be used to check if the required native libraries are currently - // available. The detectors will automatically become operational once the library - // downloads complete on device. - Log.w(TAG, "Detector dependencies are not yet available."); - callbackContext.error("Detector dependencies are not yet available."); - return false; - - - /* TODO: Handle this better later? - // Check for low storage. If there is low storage, the native library will not be - // downloaded, so detection will not become operational. - IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); - boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null; - - if (hasLowStorage) { - Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show(); - Log.w(TAG, "Low storage error."); - } - */ - } - - // Creates and starts the camera. Note that this uses a higher resolution in comparison - // to other detection examples to enable the barcode detector to detect small barcodes - // at long distances. - CameraSource.Builder builder = new CameraSource.Builder(context, barcodeDetector) - .setFacing(CameraSource.CAMERA_FACING_BACK) - .setRequestedPreviewSize(1600, 1024) - .setRequestedFps(15.0f); - - // make sure that auto focus is an available option - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - builder = builder.setFocusMode( - autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); - } - - mCameraSource = builder - .setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null) - .build(); - - return true; - } - - public void onRequestPermissionResult(int requestCode, String[] permissions, - int[] grantResults) throws JSONException - { - Log.d(TAG, "onRequestPermissionResult()"); - - if (requestCode == CAMERA_REQ_CODE) { - for (int r : grantResults) { - if (r == PackageManager.PERMISSION_DENIED) { - if (this.mStartCallbackContext != null) { - this.mStartCallbackContext.error("Camera permission denied."); - } - return; - } - } - if (this.mStartCallbackContext != null) { - startReadingWithPermission(mStartCallbackContext); - } - } - - } - - private void initPreview(@Nullable CallbackContext callbackContext) { - final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); - if (viewGroup == null) { - Log.e(TAG, "Failed to get view group"); - if (callbackContext != null) { - callbackContext.error("Failed to get view group."); - } - return; - } - - final Context context = cordova.getActivity().getApplicationContext(); - mCameraSourcePreview = new CameraSourcePreview(context); - - FrameLayout.LayoutParams childCenterLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER); - viewGroup.addView(mCameraSourcePreview, childCenterLayout); - } - - private void checkPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); - } - - private void openSettings(CallbackContext callbackContext) { - Log.d(TAG, "openSettings()"); - try { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); - intent.setData(uri); - Log.d(TAG, "Starting settings activity..."); - this.cordova.getActivity().getApplicationContext().startActivity(intent); - - callbackContext.success(); - //Log.d(TAG, "About to start reading."); - //startReading(callbackContext); - - } catch (Exception e) { - Log.e(TAG, "Error opening settings. " + e.getMessage()); - callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); - } - - } - - /** - * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet - * (e.g., because onResume was called before the camera source was created), this will be called - * again when the camera source is created. - */ - private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { - - - // check that the device has play services available. - int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); - if (code != ConnectionResult.SUCCESS) { - Dialog dlg = - GoogleApiAvailability.getInstance().getErrorDialog(cordova.getActivity(), code, RC_HANDLE_GMS); - dlg.show(); - callbackContext.error("Google Play services is unavailable."); - return false; - } - - // TODO: Check for valid mCameraSourcePreview - if (mCameraSource != null) { - try { - mCameraSourcePreview.start(mCameraSource); - } catch (IOException e) { - Log.e(TAG, "Unable to start camera source.", e); - mCameraSource.release(); - mCameraSource = null; - callbackContext.error("Unable to start camera source. " + e.getMessage()); - return false; - } - } else { - Log.e(TAG, "No camera source to start."); - callbackContext.error("No camera source to start."); - return false; - } - return true; - } - - private void startReading(CallbackContext callbackContext) { - Log.d(TAG, "startReading()"); - mStartCallbackContext = callbackContext; - - if(cordova.hasPermission(CAMERA)) { - startReadingWithPermission(callbackContext); - } else { - callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.toString()); - } - } - - private void startReadingWithPermission(final CallbackContext callbackContext) { - Log.d(TAG, "startReadingWithPermission()"); - - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - - if (mCameraSourcePreview == null) { - initPreview(callbackContext); - } - - if (mCameraSourcePreview != null) { - - final Context context = cordova.getActivity().getApplicationContext(); - if (mCameraSource == null) { - if (!createCameraSource(context, false, callbackContext)) { - return; - } - } else { - callbackContext.error("Reader already started."); - return; - } - - webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); - - webView.getView().bringToFront(); - //viewGroup.bringChildToFront(preview); - //viewGroup.bringChildToFront(webView.getView()); - - try { - startCameraSource(context, callbackContext); - } catch (SecurityException e) { - Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); - callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); - return; - } - - } - } - }); - - - } - - private void stopReading(CallbackContext callbackContext) { - Log.d(TAG, "stopReading()"); - - if (mCameraSource != null) { - mCameraSource.stop(); - mCameraSource = null; - } - webView.getView().setBackgroundColor(Color.WHITE); - - callbackContext.success("stopped"); - } - -} \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle b/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle deleted file mode 100644 index 82556217b..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/android/qrreader.gradle +++ /dev/null @@ -1,4 +0,0 @@ -dependencies { - // Important - the CameraSource implementation in this project requires version 8.1 or higher. - compile 'com.google.android.gms:play-services-vision:11.8.0' -} \ No newline at end of file diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift b/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift deleted file mode 100644 index 8a3d919b5..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/src/ios/QRReader.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// QRReader.swift -// Bitcoin.com -// -// Copyright © 2019 Jean-Baptiste Dominguez -// Copyright © 2019 Bitcoin Cash Supporters developers -// - -// Migration Native to Cordova -// In progress - -import UIKit -import AVKit - -@objc(QRReader) -class QRReader: CDVPlugin, AVCaptureMetadataOutputObjectsDelegate { - - fileprivate var readingCommand: CDVInvokedUrlCommand? - fileprivate var captureSession: AVCaptureSession! - fileprivate var previewLayer: AVCaptureVideoPreviewLayer! - fileprivate var cameraView: UIView! - - enum QRReaderError: String { - case ERROR_PERMISSION_DENIED - case ERROR_SCANNING_UNSUPPORTED - case ERROR_OPEN_SETTINGS_UNAVAILABLE - } - -} - - -// Initialization -// -extension QRReader { - - override func pluginInitialize() { - super.pluginInitialize() - cameraView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) - cameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]; - } - - func captureOutput(_ output: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { - captureSession.stopRunning() - - if let metadataObject = metadataObjects.first { - guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } - - guard let result = readableObject.stringValue else { - return - } - - // Vigration to test - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - - // Callback - onSuccess(result) - } - } -} - -// Expose methods -// -extension QRReader { - - func checkPermission(_ command: CDVInvokedUrlCommand) { - self.callback(command, status: CDVCommandStatus_OK) - } - - func openSettings(_ command: CDVInvokedUrlCommand) { - guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString), UIApplication.shared.canOpenURL(settingsUrl) else { - self.callback(command, status: CDVCommandStatus_ERROR, message: QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.rawValue) - return - } - - UIApplication.shared.open(settingsUrl, completionHandler: { [weak self] (success) in - self?.callback(command, status: CDVCommandStatus_OK) - }) - } - - func startReading(_ command: CDVInvokedUrlCommand){ - - // Keep the callback - readingCommand = command - - // If it is already initialized or webview missing, return - - guard let _ = self.cameraView - , let webView = self.webView - , let superView = webView.superview else { - return - } - - guard self.previewLayer == nil - , self.captureSession == nil else { - - if !self.captureSession.isRunning { - self.captureSession.startRunning() - } - return - } - - cameraView.backgroundColor = UIColor.white - captureSession = AVCaptureSession() - - guard let videoCaptureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else { - // TODO: Handle the case without permission "Permission" - onFailed(QRReaderError.ERROR_PERMISSION_DENIED) - return - } - - let videoInput: AVCaptureDeviceInput - - do { - videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) - } catch { - // TODO: Handle this case "Retry" - onFailed(QRReaderError.ERROR_PERMISSION_DENIED) - return - } - - guard captureSession.canAddInput(videoInput) else { - onFailed(QRReaderError.ERROR_SCANNING_UNSUPPORTED) - return - } - captureSession.addInput(videoInput) - - let metadataOutput = AVCaptureMetadataOutput() - - guard captureSession.canAddOutput(metadataOutput) else { - onFailed(QRReaderError.ERROR_SCANNING_UNSUPPORTED) - return - } - captureSession.addOutput(metadataOutput) - - metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode] - - previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - previewLayer.frame = cameraView.layer.bounds - previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill - - cameraView.layer.addSublayer(previewLayer) - superView.insertSubview(cameraView, belowSubview: webView) - - captureSession.startRunning() - } - - func stopReading(_ command: CDVInvokedUrlCommand){ - captureSession.stopRunning() - } -} - -// Private methods -// -extension QRReader { - - fileprivate func callback(_ command: CDVInvokedUrlCommand, status: CDVCommandStatus) { - callback(command, status: status, message: nil) - } - - fileprivate func callback(_ command: CDVInvokedUrlCommand, status: CDVCommandStatus, message: String?) { - guard let callbackId = command.callbackId - , let commandDelegate = self.commandDelegate else { - return - } - - var pluginResult: CDVPluginResult - - // Callback - if let _ = message { - pluginResult = CDVPluginResult(status: status, messageAs: message) - } else { - pluginResult = CDVPluginResult(status: status) - } - - commandDelegate.send(pluginResult, callbackId: callbackId) - } - - func onSuccess(_ result: String) { - guard let readingCommand = self.readingCommand else { - return - } - - callback(readingCommand, status: CDVCommandStatus_OK, message: result) - self.readingCommand = nil - } - - func onFailed(_ error: QRReaderError) { - guard let readingCommand = self.readingCommand else { - return - } - - callback(readingCommand, status: CDVCommandStatus_ERROR, message: error.rawValue) - self.readingCommand = nil - } -} diff --git a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js b/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js deleted file mode 100644 index cbfa96cc3..000000000 --- a/plugins-bitcoincom/cordova-plugin-qrreader/www/qrreader.js +++ /dev/null @@ -1,84 +0,0 @@ - - - -var argscheck = require('cordova/argscheck'); -var channel = require('cordova/channel'); -var utils = require('cordova/utils'); -var exec = require('cordova/exec'); -var cordova = require('cordova'); -/* -channel.createSticky('onCordovaInfoReady'); -// Tell cordova channel to wait on the CordovaInfoReady event -channel.waitForInitialization('onCordovaInfoReady'); -*/ - -function QRReader() { - this.testString = 'hello1'; - /* - this.available = false; - this.platform = null; - this.version = null; - this.uuid = null; - this.cordova = null; - this.model = null; - this.manufacturer = null; - this.isVirtual = null; - this.serial = null; - - var me = this; - - channel.onCordovaReady.subscribe(function () { - me.getInfo(function (info) { - // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js - // TODO: CB-5105 native implementations should not return info.cordova - var buildLabel = cordova.version; - me.available = true; - me.platform = info.platform; - me.version = info.version; - me.uuid = info.uuid; - me.cordova = buildLabel; - me.model = info.model; - me.isVirtual = info.isVirtual; - me.manufacturer = info.manufacturer || 'unknown'; - me.serial = info.serial || 'unknown'; - channel.onCordovaInfoReady.fire(); - }, function (e) { - me.available = false; - utils.alert('[ERROR] Error initializing Cordova: ' + e); - }); - }); - */ -} - - -/** - * Get device info - * - * @param {Function} successCallback The function to call when the heading data is available - * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) - */ - -QRReader.prototype.openSettings = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); - exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); -}; - -QRReader.prototype.checkPermission = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.checkPermission', arguments); - exec(successCallback, errorCallback, 'QRReader', 'checkPermission', []); -}; - -QRReader.prototype.startReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.startReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'startReading', []); -}; - -QRReader.prototype.stopReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.stopReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'stopReading', []); -}; - -module.exports = new QRReader(); - - - From ce218fc5bd6e435ab49947981fa6463f106e07a6 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 25 Feb 2019 17:23:23 +1300 Subject: [PATCH 57/65] Renamed QR read function to make the other things it does more clear. --- src/js/controllers/tab-scan.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index b7c37ce55..2beb33040 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -43,7 +43,7 @@ angular }); $scope.$on("$ionicView.afterEnter", function() { - startReadingWithPermission(); + checkPermissionsThenStartReading(); document.addEventListener("resume", onResume, true); }); @@ -84,7 +84,7 @@ angular }); function onRetry() { - startReadingWithPermission(); + checkPermissionsThenStartReading(); } function onOpenSettings(){ @@ -104,7 +104,7 @@ angular ); } - function startReadingWithPermission() { + function checkPermissionsThenStartReading() { qrService.checkPermission().then(function () { startReading(); }); From 5c5541926b5d7d399a72e1d034ef5dc936079194 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 11:46:43 +1300 Subject: [PATCH 58/65] Android now works well when swapping between apps and the phone settings. --- .../cordova-plugin-qrreader/www/qrreader.js | 18 +--- .../bitcoin/cordova/qrreader/QRReader.java | 59 +++++++--- src/js/controllers/tab-scan.controller.js | 101 ++++++++++++++---- src/js/services/qr-reader.service.js | 24 +++-- www/views/tab-scan.html | 2 +- 5 files changed, 142 insertions(+), 62 deletions(-) diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js index 223675471..dbe61f740 100644 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js @@ -1,22 +1,12 @@ cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { - - var argscheck = require('cordova/argscheck'); -var channel = require('cordova/channel'); -var utils = require('cordova/utils'); var exec = require('cordova/exec'); -var cordova = require('cordova'); - -function QRReader() {} -/** - * Get device info - * - * @param {Function} successCallback The function to call when the heading data is available - * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) - */ +function QRReader() { + this.testString = 'hello1'; +} QRReader.prototype.openSettings = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); @@ -41,6 +31,4 @@ QRReader.prototype.stopReading = function (successCallback, errorCallback) { module.exports = new QRReader(); - - }); diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java index 296e6a7c6..fd2583a48 100644 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java @@ -49,13 +49,25 @@ public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { public static final String TAG = "QRReader"; - public static String platform; // Device OS - public static String uuid; // Device UUID + enum QRReaderPermissionResult { + PERMISSION_DENIED, + PERMISSION_GRANTED + } enum QRReaderError { + // Shared with iOS ERROR_PERMISSION_DENIED, ERROR_SCANNING_UNSUPPORTED, - ERROR_OPEN_SETTINGS_UNAVAILABLE + ERROR_OPEN_SETTINGS_UNAVAILABLE, + + // Android specific + ERROR_CAMERA_FAILED_TO_START, + ERROR_CAMERA_SECURITY_EXCEPTION, + ERROR_CAMERA_UNAVAILABLE, + ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, + ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, + ERROR_READ_ALREADY_STARTED, + ERROR_UI_SETUP_FAILED } public static final String CAMERA = Manifest.permission.CAMERA; @@ -69,6 +81,7 @@ enum QRReaderError { private CameraSource mCameraSource; private CameraSourcePreview mCameraSourcePreview; private CallbackContext mStartCallbackContext; + private CallbackContext mPermissionCallbackContext; public QRReader() { } @@ -109,6 +122,7 @@ public void onBarcodeDetected(Barcode barcode) { Log.d(TAG, "Detected new barcode."); if (mStartCallbackContext != null) { mStartCallbackContext.success(contents); + mStartCallbackContext = null; } else { Log.e(TAG, "No callback context when detecting new barcode."); } @@ -125,6 +139,7 @@ public void onBarcodeDetected(Barcode barcode) { */ @SuppressLint("InlinedApi") private Boolean createCameraSource(Context context, boolean useFlash, CallbackContext callbackContext) { + Log.d(TAG, "createCameraSource()"); boolean autoFocus = true; // A barcode detector is created to track barcodes. An associated multi-processor instance @@ -147,7 +162,7 @@ private Boolean createCameraSource(Context context, boolean useFlash, CallbackCo // available. The detectors will automatically become operational once the library // downloads complete on device. Log.w(TAG, "Detector dependencies are not yet available."); - callbackContext.error("Detector dependencies are not yet available."); + callbackContext.error(QRReaderError.ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE.name()); return false; @@ -193,14 +208,17 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, if (requestCode == CAMERA_REQ_CODE) { for (int r : grantResults) { if (r == PackageManager.PERMISSION_DENIED) { - if (this.mStartCallbackContext != null) { - this.mStartCallbackContext.error("Camera permission denied."); + if (this.mPermissionCallbackContext != null) { + this.mPermissionCallbackContext.success(QRReaderPermissionResult.PERMISSION_DENIED.name()); + this.mPermissionCallbackContext = null; } return; } } - if (this.mStartCallbackContext != null) { - startReadingWithPermission(mStartCallbackContext); + + if (this.mPermissionCallbackContext != null) { + this.mPermissionCallbackContext.success(QRReaderPermissionResult.PERMISSION_GRANTED.name()); + this.mPermissionCallbackContext = null; } } @@ -211,7 +229,7 @@ private void initPreview(@Nullable CallbackContext callbackContext) { if (viewGroup == null) { Log.e(TAG, "Failed to get view group"); if (callbackContext != null) { - callbackContext.error("Failed to get view group."); + callbackContext.error(QRReaderError.ERROR_UI_SETUP_FAILED.name()); } return; } @@ -224,7 +242,12 @@ private void initPreview(@Nullable CallbackContext callbackContext) { } private void checkPermission(CallbackContext callbackContext) { - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + if(cordova.hasPermission(CAMERA)) { + callbackContext.success(QRReaderPermissionResult.PERMISSION_GRANTED.name()); + } else { + mPermissionCallbackContext = callbackContext; + cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); + } } private void openSettings(CallbackContext callbackContext) { @@ -244,7 +267,7 @@ private void openSettings(CallbackContext callbackContext) { } catch (Exception e) { Log.e(TAG, "Error opening settings. " + e.getMessage()); - callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.toString()); + callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.name()); } } @@ -263,7 +286,7 @@ private Boolean startCameraSource(Context context, CallbackContext callbackConte Dialog dlg = GoogleApiAvailability.getInstance().getErrorDialog(cordova.getActivity(), code, RC_HANDLE_GMS); dlg.show(); - callbackContext.error("Google Play services is unavailable."); + callbackContext.error(QRReaderError.ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE.name()); return false; } @@ -275,12 +298,12 @@ private Boolean startCameraSource(Context context, CallbackContext callbackConte Log.e(TAG, "Unable to start camera source.", e); mCameraSource.release(); mCameraSource = null; - callbackContext.error("Unable to start camera source. " + e.getMessage()); + callbackContext.error(QRReaderError.ERROR_CAMERA_FAILED_TO_START.name() + " " + e.getMessage()); return false; } } else { Log.e(TAG, "No camera source to start."); - callbackContext.error("No camera source to start."); + callbackContext.error(QRReaderError.ERROR_CAMERA_UNAVAILABLE.name()); return false; } return true; @@ -293,7 +316,7 @@ private void startReading(CallbackContext callbackContext) { if(cordova.hasPermission(CAMERA)) { startReadingWithPermission(callbackContext); } else { - callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.toString()); + callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.name()); } } @@ -316,7 +339,8 @@ public void run() { return; } } else { - callbackContext.error("Reader already started."); + Log.d(TAG, "mCamera source was not null"); + callbackContext.error(QRReaderError.ERROR_READ_ALREADY_STARTED.name()); return; } @@ -330,7 +354,7 @@ public void run() { startCameraSource(context, callbackContext); } catch (SecurityException e) { Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); - callbackContext.error("Security Exception when starting camera source. " + e.getMessage()); + callbackContext.error(QRReaderError.ERROR_CAMERA_SECURITY_EXCEPTION.name() + " " + e.getMessage()); return; } @@ -347,6 +371,7 @@ private void stopReading(CallbackContext callbackContext) { if (mCameraSource != null) { mCameraSource.stop(); mCameraSource = null; + Log.d(TAG, "Set mCameraSource to null."); } webView.getView().setBackgroundColor(Color.WHITE); diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 2beb33040..52e203f6d 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -22,13 +22,19 @@ angular , platformInfo ) { + var qrPermissionResult = { + denied: 'PERMISSION_DENIED', + granted: 'PERMISSION_GRANTED', + }; + var scannerStates = { - unauthorized: 'unauthorized', denied: 'denied', unavailable: 'unavailable', visible: 'visible' }; + var isCheckingPermissions = false; + var isReading = false; var isDesktop = !platformInfo.isCordova; var qrService = isDesktop ? qrScannerService : qrReaderService; @@ -43,22 +49,51 @@ angular }); $scope.$on("$ionicView.afterEnter", function() { - checkPermissionsThenStartReading(); - document.addEventListener("resume", onResume, true); + _checkPermissionThenStartReading(); + document.addEventListener("resume", _onResume, true); }); $scope.$on("$ionicView.beforeLeave", function() { - document.removeEventListener("resume", onResume, true); + document.removeEventListener("resume", _onResume, true); qrService.stopReading(); }); - function onResume() { - $scope.$apply(function () { - startReading(); - }); + + function _onResume() { + console.log('onResume()'); + + // Give everything time to settle since onResume gets call after the app resumes, + // and some things are already happening, such as: + // - Permission check has returned and reading was started.o + + $timeout(function onResumeTimeout() { + + if (isCheckingPermissions) { + console.log('onResume(), was checking permissions, so don\'t do anything.'); + // Resume is called after using the permissions dialog + // Let's hope they are not switching back to this app from another app + return; + } + + if (isReading) { + console.log('onResume(), was reading, so restart reading.'); + qrService.stopReading().then( + function onStoppedReadingSuccess() { + console.log('onResume(), Starting reading after stopping.'); + _startReading(); + }, + function onStoppedReadingFailed(err) { + $log.error('Failed to restart reading', err); + $scope.currentState = scannerStates.unavailable; + } + ); + } + + }, 200); + } - function handleSuccessfulScan(contents){ + function _handleSuccessfulScan(contents){ $log.debug('Scan returned: "' + contents + '"'); //scannerService.pausePreview(); @@ -70,21 +105,21 @@ angular var title = gettextCatalog.getString('Scan Failed'); popupService.showAlert(title, err.message, function onAlertShown() { // Enable another scan since we won't receive incomingDataMenu.menuHidden - startReading(); + _startReading(); }); } else { - startReading(); + _startReading(); } }); } $rootScope.$on('incomingDataMenu.menuHidden', function() { - startReading(); + _startReading(); }); function onRetry() { - checkPermissionsThenStartReading(); + _checkPermissionThenStartReading(); } function onOpenSettings(){ @@ -92,6 +127,10 @@ angular qrService.openSettings().then( function onOpenSettingsResolved(result) { console.log('Open settings resolved with:', result); + //_checkPermissionThenStartReading(); + // Allow to manually retry the camera + $scope.canOpenSettings = false; + }, function onOpenSettingsRejected(reason) { $log.error('Failed to open settings. ' + reason); @@ -104,24 +143,44 @@ angular ); } - function checkPermissionsThenStartReading() { - qrService.checkPermission().then(function () { - startReading(); - }); + function _checkPermissionThenStartReading() { + isCheckingPermissions = true; + qrService.checkPermission().then( + function onCheckPermissionSuccess(result) { + console.log('onPermissionSuccess() ', result); + isCheckingPermissions = false; + if (result === qrPermissionResult.granted) { + _startReading(); + } else { + $scope.currentState = scannerStates.denied; + } + }, + function onCheckPermissionFailed(err) { + isCheckingPermissions = false; + $log.error('Failed to check permission.', err); + } + ); } - function startReading() { + function _startReading() { $scope.currentState = scannerStates.visible; + isReading = true; console.log('Starting QR Service.'); qrService.startReading().then( function onStartReadingResolved(contents) { - handleSuccessfulScan(contents); + isReading = false; + _handleSuccessfulScan(contents); }, function onStartReadingRejected(reason) { + isReading = false; $log.error('Failed to start reading QR code. ' + reason); - // TODO: Handle all the different types of errors - $scope.currentState = scannerStates.denied; + if (reason === qrService.errors.permissionDenied) { + $scope.currentState = scannerStates.denied; + } else { + // TODO: Handle all the different types of errors + $scope.currentState = scannerStates.unavailable; + } }); } diff --git a/src/js/services/qr-reader.service.js b/src/js/services/qr-reader.service.js index 50052ddbe..c7713e5a8 100644 --- a/src/js/services/qr-reader.service.js +++ b/src/js/services/qr-reader.service.js @@ -18,10 +18,18 @@ var errors = { // Common - + permissionDenied: 'ERROR_PERMISSION_DENIED', + scanningUnsupported: 'ERROR_SCANNING_UNSUPPORTED', + openSettingsUnavailable: 'ERROR_OPEN_SETTINGS_UNAVAILABLE', // Android - + cameraFailedToStart: 'ERROR_CAMERA_FAILED_TO_START', + cameraSecurityException: 'ERROR_CAMERA_SECURITY_EXCEPTION', + cameraUnavailable: 'ERROR_CAMERA_UNAVAILABLE', + detectorDependenciesUnavailable: 'ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE', + googlePlayServicesUnavailable: 'ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE', + readAlreadyStarted: 'ERROR_READ_ALREADY_STARTED', + errorUiSetupFailed: 'ERROR_UI_SETUP_FAILED' // Desktop @@ -32,6 +40,8 @@ var qrReader = $window.qrreader; var service = { + errors: errors, + // Functions openSettings: openSettings , startReading: startReading @@ -88,12 +98,12 @@ qrReader.stopReading( function onSuccess(result) { - console.log('qrreader stopReading() result:', result); + console.log('qrReader stopReading() result:', result); deferred.resolve(result); }, function onError(error) { - console.error('qrreader stopReading() error:', error); + $log.error('qrReader stopReading() error:', error); var errorMessage = errors[error] || error; var translatedErrorMessage = gettextCatalog.getString(errorMessage); @@ -103,19 +113,17 @@ return deferred.promise; } - // No need to wait on this promise unless you want to start again - // immediately after function checkPermission() { var deferred = $q.defer(); qrReader.checkPermission( function onSuccess(result) { - console.log('qrreader checkPermission() result:', result); + console.log('qrReader checkPermission() result:', result); deferred.resolve(result); }, function onError(error) { - console.error('qrreader checkPermission() error:', error); + $log.error('qrReader checkPermission() error:', error); var errorMessage = errors[error] || error; var translatedErrorMessage = gettextCatalog.getString(errorMessage); diff --git a/www/views/tab-scan.html b/www/views/tab-scan.html index 87bce635f..d20934ae1 100644 --- a/www/views/tab-scan.html +++ b/www/views/tab-scan.html @@ -8,7 +8,7 @@ -

+
From 8b68fbdb09f1abb719323516f701a55ae756404c Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 12:33:33 +1300 Subject: [PATCH 59/65] Bugfix for resuming on iOS. --- src/js/controllers/tab-scan.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 52e203f6d..5e5c539b7 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -75,7 +75,7 @@ angular return; } - if (isReading) { + if (isReading && platformInfo.isAndroid) { // Don't need to do this on iOS, in fact it breaks if you do console.log('onResume(), was reading, so restart reading.'); qrService.stopReading().then( function onStoppedReadingSuccess() { From 75118c1e7ef736653ac040457a15bcbbf41205cb Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 13:51:40 +1300 Subject: [PATCH 60/65] Handling the extra permission states on iOS. --- src/js/controllers/tab-scan.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/controllers/tab-scan.controller.js b/src/js/controllers/tab-scan.controller.js index 5e5c539b7..de9360834 100644 --- a/src/js/controllers/tab-scan.controller.js +++ b/src/js/controllers/tab-scan.controller.js @@ -25,6 +25,10 @@ angular var qrPermissionResult = { denied: 'PERMISSION_DENIED', granted: 'PERMISSION_GRANTED', + + // iOS + restricted: 'PERMISSION_RESTRICTED', + notDetermined: 'PERMISSION_NOT_DETERMINED' }; var scannerStates = { @@ -149,7 +153,7 @@ angular function onCheckPermissionSuccess(result) { console.log('onPermissionSuccess() ', result); isCheckingPermissions = false; - if (result === qrPermissionResult.granted) { + if (result === qrPermissionResult.granted || result === qrPermissionResult.notDetermined) { _startReading(); } else { $scope.currentState = scannerStates.denied; From 5c2ba4aa75f848344f611b65b5b3766141e9082a Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 14:25:02 +1300 Subject: [PATCH 61/65] Desktop scanner permissions now compatible with new mobile plugin. --- src/js/services/qr-scanner.service.js | 37 +++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js index b3830577d..4318b7048 100644 --- a/src/js/services/qr-scanner.service.js +++ b/src/js/services/qr-scanner.service.js @@ -14,10 +14,18 @@ var errors = { // Common - + permissionDenied: 'ERROR_PERMISSION_DENIED', + scanningUnsupported: 'ERROR_SCANNING_UNSUPPORTED', + openSettingsUnavailable: 'ERROR_OPEN_SETTINGS_UNAVAILABLE', // Android - + cameraFailedToStart: 'ERROR_CAMERA_FAILED_TO_START', + cameraSecurityException: 'ERROR_CAMERA_SECURITY_EXCEPTION', + cameraUnavailable: 'ERROR_CAMERA_UNAVAILABLE', + detectorDependenciesUnavailable: 'ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE', + googlePlayServicesUnavailable: 'ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE', + readAlreadyStarted: 'ERROR_READ_ALREADY_STARTED', + errorUiSetupFailed: 'ERROR_UI_SETUP_FAILED' // Desktop @@ -29,6 +37,8 @@ var scanDeferred = null; var service = { + errors: errors, + // Functions openSettings: openSettings, startReading: startReading, @@ -105,14 +115,21 @@ function checkPermission() { var deferred = $q.defer(); - // qrService.getStatus(function(status){ - // if(!status.authorized){ - // deferred.reject(status); - // } else { - // deferred.resolve(status); - // } - // }); - deferred.resolve(status); + qrService.getStatus(function onStatus(status) { + var result = ''; + if (status.authorized) { + result = 'PERMISSION_GRANTED'; + } else if (status.authorized) { + result = 'PERMISSION_DENIED'; + } else if (status.restricted) { + result = 'PEMISSION_RESTRICTED' + } else { + result = 'PERMISSION_NOT_DETERMINED'; + } + + deferred.resolve(result); + }); + return deferred.promise; } From a0ce94220bb1d6e03d103ebd75cabe5461084dca Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 18:05:00 +1300 Subject: [PATCH 62/65] Removed the platforms folder that was used for plugin development. --- .../cordova-plugin-qrreader/www/qrreader.js | 34 - .../cordova/qrreader/BarcodeMapTracker.java | 89 -- .../qrreader/BarcodeMapTrackerFactory.java | 29 - .../qrreader/BarcodeUpdateListener.java | 13 - .../cordova/qrreader/CameraSource.java | 1200 ----------------- .../cordova/qrreader/CameraSourcePreview.java | 201 --- .../bitcoin/cordova/qrreader/QRReader.java | 381 ------ 7 files changed, 1947 deletions(-) delete mode 100644 platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java delete mode 100644 platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js deleted file mode 100644 index dbe61f740..000000000 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ /dev/null @@ -1,34 +0,0 @@ -cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { - -var argscheck = require('cordova/argscheck'); -var exec = require('cordova/exec'); - - -function QRReader() { - this.testString = 'hello1'; -} - -QRReader.prototype.openSettings = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); - exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); -}; - -QRReader.prototype.checkPermission = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.checkPermission', arguments); - exec(successCallback, errorCallback, 'QRReader', 'checkPermission', []); -}; - -QRReader.prototype.startReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.startReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'startReading', []); -}; - -QRReader.prototype.stopReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.stopReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'stopReading', []); -}; - -module.exports = new QRReader(); - - -}); diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java deleted file mode 100644 index cefdc54f4..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTracker.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.content.Context; -import android.util.Log; - - -import com.google.android.gms.vision.Detector; -import com.google.android.gms.vision.Tracker; -import com.google.android.gms.vision.barcode.Barcode; - -import java.util.HashMap; -import java.util.Map; - -/** - * Generic tracker which is used for tracking or reading a barcode (and can really be used for - * any type of item). This is used to receive newly detected items, add a graphical representation - * to an overlay, update the graphics as the item changes, and remove the graphics when the item - * goes away. - */ -public class BarcodeMapTracker extends Tracker { - - private Map mBarcodes; - private Integer mId; - private BarcodeUpdateListener mBarcodeUpdateListener; - - - - BarcodeMapTracker(Map barcodes, BarcodeUpdateListener listener) { - this.mBarcodes = barcodes; - //this.mOverlay = mOverlay; - //this.mGraphic = mGraphic; - this.mBarcodeUpdateListener = listener; - } - - /** - * Start tracking the detected item instance within the item overlay. - */ - @Override - public void onNewItem(int id, Barcode item) { - //mGraphic.setId(id); - mId = id; - mBarcodes.put(id, item); - Log.d("BarcodeGraphicTracker", "New barcode."); - if (mBarcodeUpdateListener != null) { - mBarcodeUpdateListener.onBarcodeDetected(item); - } - } - - /** - * Update the position/characteristics of the item within the overlay. - */ - @Override - public void onUpdate(Detector.Detections detectionResults, Barcode item) { - //mOverlay.add(mGraphic); - //mGraphic.updateItem(item); - if (mId != null) { - mBarcodes.put(mId, item); - } - } - - /** - * Hide the graphic when the corresponding object was not detected. This can happen for - * intermediate frames temporarily, for example if the object was momentarily blocked from - * view. - */ - @Override - public void onMissing(Detector.Detections detectionResults) { - //mOverlay.remove(mGraphic); - if (mId != null) { - mBarcodes.remove(mId); - mId = null; - } - } - - /** - * Called when the item is assumed to be gone for good. Remove the graphic annotation from - * the overlay. - */ - @Override - public void onDone() { - - //mOverlay.remove(mGraphic); - if (mId != null) { - mBarcodes.remove(mId); - mId = null; - } - } -} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java deleted file mode 100644 index e1626d6e3..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeMapTrackerFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bitcoin.cordova.qrreader; - -import android.content.Context; -import com.google.android.gms.vision.MultiProcessor; -import com.google.android.gms.vision.Tracker; -import com.google.android.gms.vision.barcode.Barcode; - -import java.util.Map; - -/** - * Factory for creating a tracker and associated graphic to be associated with a new barcode. The - * multi-processor uses this factory to create barcode trackers as needed -- one for each barcode. - */ -class BarcodeMapTrackerFactory implements MultiProcessor.Factory { - private Map mBarcodes; - private BarcodeUpdateListener mListener; - - public BarcodeMapTrackerFactory(Map mBarcodes, - BarcodeUpdateListener listener) { - this.mBarcodes = mBarcodes; - this.mListener = listener; - } - - @Override - public Tracker create(Barcode barcode) { - return new BarcodeMapTracker(mBarcodes, mListener); - } - -} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java b/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java deleted file mode 100644 index e32f618d3..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/BarcodeUpdateListener.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.bitcoin.cordova.qrreader; - -import android.support.annotation.UiThread; -import com.google.android.gms.vision.barcode.Barcode; - -/** - * Consume the item instance detected from an Activity or Fragment level by implementing the - * BarcodeUpdateListener interface method onBarcodeDetected. - */ -interface BarcodeUpdateListener { - @UiThread - void onBarcodeDetected(Barcode barcode); -} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java deleted file mode 100644 index b8e4edb8b..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSource.java +++ /dev/null @@ -1,1200 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.ImageFormat; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.os.Build; -import android.os.SystemClock; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresPermission; -import android.support.annotation.StringDef; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.WindowManager; - -import com.google.android.gms.common.images.Size; -import com.google.android.gms.vision.Detector; -import com.google.android.gms.vision.Frame; - -import java.io.IOException; -import java.lang.Thread.State; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -// Note: This requires Google Play Services 8.1 or higher, due to using indirect byte buffers for -// storing images. - -/** - * Manages the camera in conjunction with an underlying - * {@link com.google.android.gms.vision.Detector}. This receives preview frames from the camera at - * a specified rate, sending those frames to the detector as fast as it is able to process those - * frames. - *

- * This camera source makes a best effort to manage processing on preview frames as fast as - * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector - * is unable to keep up with the rate of frames generated by the camera. You should use - * {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate that works well with - * the capabilities of the camera hardware and the detector options that you have selected. If CPU - * utilization is higher than you'd like, then you may want to consider reducing FPS. If the camera - * preview or detector results are too "jerky", then you may want to consider increasing FPS. - *

- * The following Android permission is required to use the camera: - *

    - *
  • android.permissions.CAMERA
  • - *
- */ -@SuppressWarnings("deprecation") -public class CameraSource { - @SuppressLint("InlinedApi") - public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK; - @SuppressLint("InlinedApi") - public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT; - - private static final String TAG = "OpenCameraSource"; - - /** - * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL - * context, we can choose any ID we want here. - */ - private static final int DUMMY_TEXTURE_NAME = 100; - - /** - * If the absolute difference between a preview size aspect ratio and a picture size aspect - * ratio is less than this tolerance, they are considered to be the same aspect ratio. - */ - private static final float ASPECT_RATIO_TOLERANCE = 0.01f; - - @StringDef({ - Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, - Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, - Camera.Parameters.FOCUS_MODE_AUTO, - Camera.Parameters.FOCUS_MODE_EDOF, - Camera.Parameters.FOCUS_MODE_FIXED, - Camera.Parameters.FOCUS_MODE_INFINITY, - Camera.Parameters.FOCUS_MODE_MACRO - }) - @Retention(RetentionPolicy.SOURCE) - private @interface FocusMode {} - - @StringDef({ - Camera.Parameters.FLASH_MODE_ON, - Camera.Parameters.FLASH_MODE_OFF, - Camera.Parameters.FLASH_MODE_AUTO, - Camera.Parameters.FLASH_MODE_RED_EYE, - Camera.Parameters.FLASH_MODE_TORCH - }) - @Retention(RetentionPolicy.SOURCE) - private @interface FlashMode {} - - private Context mContext; - - private final Object mCameraLock = new Object(); - - // Guarded by mCameraLock - private Camera mCamera; - - private int mFacing = CAMERA_FACING_BACK; - - /** - * Rotation of the device, and thus the associated preview images captured from the device. - * See {@link Frame.Metadata#getRotation()}. - */ - private int mRotation; - - private Size mPreviewSize; - - // These values may be requested by the caller. Due to hardware limitations, we may need to - // select close, but not exactly the same values for these. - private float mRequestedFps = 30.0f; - private int mRequestedPreviewWidth = 1024; - private int mRequestedPreviewHeight = 768; - - - private String mFocusMode = null; - private String mFlashMode = null; - - // These instances need to be held onto to avoid GC of their underlying resources. Even though - // these aren't used outside of the method that creates them, they still must have hard - // references maintained to them. - private SurfaceView mDummySurfaceView; - private SurfaceTexture mDummySurfaceTexture; - - /** - * Dedicated thread and associated runnable for calling into the detector with frames, as the - * frames become available from the camera. - */ - private Thread mProcessingThread; - private FrameProcessingRunnable mFrameProcessor; - - /** - * Map to convert between a byte array, received from the camera, and its associated byte - * buffer. We use byte buffers internally because this is a more efficient way to call into - * native code later (avoids a potential copy). - */ - private Map mBytesToByteBuffer = new HashMap(); - - //============================================================================================== - // Builder - //============================================================================================== - - /** - * Builder for configuring and creating an associated camera source. - */ - public static class Builder { - private final Detector mDetector; - private CameraSource mCameraSource = new CameraSource(); - - /** - * Creates a camera source builder with the supplied context and detector. Camera preview - * images will be streamed to the associated detector upon starting the camera source. - */ - public Builder(Context context, Detector detector) { - if (context == null) { - throw new IllegalArgumentException("No context supplied."); - } - if (detector == null) { - throw new IllegalArgumentException("No detector supplied."); - } - - mDetector = detector; - mCameraSource.mContext = context; - } - - /** - * Sets the requested frame rate in frames per second. If the exact requested value is not - * not available, the best matching available value is selected. Default: 30. - */ - public Builder setRequestedFps(float fps) { - if (fps <= 0) { - throw new IllegalArgumentException("Invalid fps: " + fps); - } - mCameraSource.mRequestedFps = fps; - return this; - } - - public Builder setFocusMode(@FocusMode String mode) { - mCameraSource.mFocusMode = mode; - return this; - } - - public Builder setFlashMode(@FlashMode String mode) { - mCameraSource.mFlashMode = mode; - return this; - } - - /** - * Sets the desired width and height of the camera frames in pixels. If the exact desired - * values are not available options, the best matching available options are selected. - * Also, we try to select a preview size which corresponds to the aspect ratio of an - * associated full picture size, if applicable. Default: 1024x768. - */ - public Builder setRequestedPreviewSize(int width, int height) { - // Restrict the requested range to something within the realm of possibility. The - // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that - // devices can support. We bound this to avoid int overflow in the code later. - final int MAX = 1000000; - if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) { - throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height); - } - mCameraSource.mRequestedPreviewWidth = width; - mCameraSource.mRequestedPreviewHeight = height; - return this; - } - - /** - * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or - * {@link #CAMERA_FACING_FRONT}). Default: back facing. - */ - public Builder setFacing(int facing) { - if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) { - throw new IllegalArgumentException("Invalid camera: " + facing); - } - mCameraSource.mFacing = facing; - return this; - } - - /** - * Creates an instance of the camera source. - */ - public CameraSource build() { - mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector); - return mCameraSource; - } - } - - //============================================================================================== - // Bridge Functionality for the Camera1 API - //============================================================================================== - - /** - * Callback interface used to signal the moment of actual image capture. - */ - public interface ShutterCallback { - /** - * Called as near as possible to the moment when a photo is captured from the sensor. This - * is a good opportunity to play a shutter sound or give other feedback of camera operation. - * This may be some time after the photo was triggered, but some time before the actual data - * is available. - */ - void onShutter(); - } - - /** - * Callback interface used to supply image data from a photo capture. - */ - public interface PictureCallback { - /** - * Called when image data is available after a picture is taken. The format of the data - * is a jpeg binary. - */ - void onPictureTaken(byte[] data); - } - - /** - * Callback interface used to notify on completion of camera auto focus. - */ - public interface AutoFocusCallback { - /** - * Called when the camera auto focus completes. If the camera - * does not support auto-focus and autoFocus is called, - * onAutoFocus will be called immediately with a fake value of - * success set to true. - *

- * The auto-focus routine does not lock auto-exposure and auto-white - * balance after it completes. - * - * @param success true if focus was successful, false if otherwise - */ - void onAutoFocus(boolean success); - } - - /** - * Callback interface used to notify on auto focus start and stop. - *

- *

This is only supported in continuous autofocus modes -- {@link - * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link - * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show - * autofocus animation based on this.

- */ - public interface AutoFocusMoveCallback { - /** - * Called when the camera auto focus starts or stops. - * - * @param start true if focus starts to move, false if focus stops to move - */ - void onAutoFocusMoving(boolean start); - } - - //============================================================================================== - // Public - //============================================================================================== - - /** - * Stops the camera and releases the resources of the camera and underlying detector. - */ - public void release() { - synchronized (mCameraLock) { - stop(); - mFrameProcessor.release(); - } - } - - /** - * Opens the camera and starts sending preview frames to the underlying detector. The preview - * frames are not displayed. - * - * @throws IOException if the camera's preview texture or display could not be initialized - */ - @RequiresPermission(Manifest.permission.CAMERA) - public CameraSource start() throws IOException { - synchronized (mCameraLock) { - if (mCamera != null) { - return this; - } - - mCamera = createCamera(); - - // SurfaceTexture was introduced in Honeycomb (11), so if we are running and - // old version of Android. fall back to use SurfaceView. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); - mCamera.setPreviewTexture(mDummySurfaceTexture); - } else { - mDummySurfaceView = new SurfaceView(mContext); - mCamera.setPreviewDisplay(mDummySurfaceView.getHolder()); - } - mCamera.startPreview(); - - mProcessingThread = new Thread(mFrameProcessor); - mFrameProcessor.setActive(true); - mProcessingThread.start(); - } - return this; - } - - /** - * Opens the camera and starts sending preview frames to the underlying detector. The supplied - * surface holder is used for the preview so frames can be displayed to the user. - * - * @param surfaceHolder the surface holder to use for the preview frames - * @throws IOException if the supplied surface holder could not be used as the preview display - */ - @RequiresPermission(Manifest.permission.CAMERA) - public CameraSource start(SurfaceHolder surfaceHolder) throws IOException { - synchronized (mCameraLock) { - if (mCamera != null) { - return this; - } - - mCamera = createCamera(); - mCamera.setPreviewDisplay(surfaceHolder); - mCamera.startPreview(); - - mProcessingThread = new Thread(mFrameProcessor); - mFrameProcessor.setActive(true); - mProcessingThread.start(); - } - return this; - } - - /** - * Closes the camera and stops sending frames to the underlying frame detector. - *

- * This camera source may be restarted again by calling {@link #start()} or - * {@link #start(SurfaceHolder)}. - *

- * Call {@link #release()} instead to completely shut down this camera source and release the - * resources of the underlying detector. - */ - public void stop() { - synchronized (mCameraLock) { - mFrameProcessor.setActive(false); - if (mProcessingThread != null) { - try { - // Wait for the thread to complete to ensure that we can't have multiple threads - // executing at the same time (i.e., which would happen if we called start too - // quickly after stop). - mProcessingThread.join(); - } catch (InterruptedException e) { - Log.d(TAG, "Frame processing thread interrupted on release."); - } - mProcessingThread = null; - } - - // clear the buffer to prevent oom exceptions - mBytesToByteBuffer.clear(); - - if (mCamera != null) { - mCamera.stopPreview(); - mCamera.setPreviewCallbackWithBuffer(null); - try { - // We want to be compatible back to Gingerbread, but SurfaceTexture - // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the - // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't - // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb. - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - mCamera.setPreviewTexture(null); - - } else { - mCamera.setPreviewDisplay(null); - } - } catch (Exception e) { - Log.e(TAG, "Failed to clear camera preview: " + e); - } - mCamera.release(); - mCamera = null; - } - } - } - - /** - * Returns the preview size that is currently in use by the underlying camera. - */ - public Size getPreviewSize() { - return mPreviewSize; - } - - /** - * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or - * {@link #CAMERA_FACING_FRONT}. - */ - public int getCameraFacing() { - return mFacing; - } - - public int doZoom(float scale) { - synchronized (mCameraLock) { - if (mCamera == null) { - return 0; - } - int currentZoom = 0; - int maxZoom; - Camera.Parameters parameters = mCamera.getParameters(); - if (!parameters.isZoomSupported()) { - Log.w(TAG, "Zoom is not supported on this device"); - return currentZoom; - } - maxZoom = parameters.getMaxZoom(); - - currentZoom = parameters.getZoom() + 1; - float newZoom; - if (scale > 1) { - newZoom = currentZoom + scale * (maxZoom / 10); - } else { - newZoom = currentZoom * scale; - } - currentZoom = Math.round(newZoom) - 1; - if (currentZoom < 0) { - currentZoom = 0; - } else if (currentZoom > maxZoom) { - currentZoom = maxZoom; - } - parameters.setZoom(currentZoom); - mCamera.setParameters(parameters); - return currentZoom; - } - } - - /** - * Initiates taking a picture, which happens asynchronously. The camera source should have been - * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera - * preview is suspended while the picture is being taken, but will resume once picture taking is - * done. - * - * @param shutter the callback for image capture moment, or null - * @param jpeg the callback for JPEG image data, or null - */ - public void takePicture(ShutterCallback shutter, PictureCallback jpeg) { - synchronized (mCameraLock) { - if (mCamera != null) { - PictureStartCallback startCallback = new PictureStartCallback(); - startCallback.mDelegate = shutter; - PictureDoneCallback doneCallback = new PictureDoneCallback(); - doneCallback.mDelegate = jpeg; - mCamera.takePicture(startCallback, null, null, doneCallback); - } - } - } - - /** - * Gets the current focus mode setting. - * - * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link - * #autoFocus(AutoFocusCallback)} to start the focus if focus - * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO. - * @see Camera.Parameters#FOCUS_MODE_AUTO - * @see Camera.Parameters#FOCUS_MODE_INFINITY - * @see Camera.Parameters#FOCUS_MODE_MACRO - * @see Camera.Parameters#FOCUS_MODE_FIXED - * @see Camera.Parameters#FOCUS_MODE_EDOF - * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO - * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE - */ - @Nullable - @FocusMode - public String getFocusMode() { - return mFocusMode; - } - - /** - * Sets the focus mode. - * - * @param mode the focus mode - * @return {@code true} if the focus mode is set, {@code false} otherwise - * @see #getFocusMode() - */ - public boolean setFocusMode(@FocusMode String mode) { - synchronized (mCameraLock) { - if (mCamera != null && mode != null) { - Camera.Parameters parameters = mCamera.getParameters(); - if (parameters.getSupportedFocusModes().contains(mode)) { - parameters.setFocusMode(mode); - mCamera.setParameters(parameters); - mFocusMode = mode; - return true; - } - } - - return false; - } - } - - /** - * Gets the current flash mode setting. - * - * @return current flash mode. null if flash mode setting is not - * supported or the camera is not yet created. - * @see Camera.Parameters#FLASH_MODE_OFF - * @see Camera.Parameters#FLASH_MODE_AUTO - * @see Camera.Parameters#FLASH_MODE_ON - * @see Camera.Parameters#FLASH_MODE_RED_EYE - * @see Camera.Parameters#FLASH_MODE_TORCH - */ - @Nullable - @FlashMode - public String getFlashMode() { - return mFlashMode; - } - - /** - * Sets the flash mode. - * - * @param mode flash mode. - * @return {@code true} if the flash mode is set, {@code false} otherwise - * @see #getFlashMode() - */ - public boolean setFlashMode(@FlashMode String mode) { - synchronized (mCameraLock) { - if (mCamera != null && mode != null) { - Camera.Parameters parameters = mCamera.getParameters(); - if (parameters.getSupportedFlashModes().contains(mode)) { - parameters.setFlashMode(mode); - mCamera.setParameters(parameters); - mFlashMode = mode; - return true; - } - } - - return false; - } - } - - /** - * Starts camera auto-focus and registers a callback function to run when - * the camera is focused. This method is only valid when preview is active - * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}). - *

- *

Callers should check - * {@link #getFocusMode()} to determine if - * this method should be called. If the camera does not support auto-focus, - * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)} - * callback will be called immediately. - *

- *

If the current flash mode is not - * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be - * fired during auto-focus, depending on the driver and camera hardware.

- * - * @param cb the callback to run - * @see #cancelAutoFocus() - */ - public void autoFocus(@Nullable AutoFocusCallback cb) { - synchronized (mCameraLock) { - if (mCamera != null) { - CameraAutoFocusCallback autoFocusCallback = null; - if (cb != null) { - autoFocusCallback = new CameraAutoFocusCallback(); - autoFocusCallback.mDelegate = cb; - } - mCamera.autoFocus(autoFocusCallback); - } - } - } - - /** - * Cancels any auto-focus function in progress. - * Whether or not auto-focus is currently in progress, - * this function will return the focus position to the default. - * If the camera does not support auto-focus, this is a no-op. - * - * @see #autoFocus(AutoFocusCallback) - */ - public void cancelAutoFocus() { - synchronized (mCameraLock) { - if (mCamera != null) { - mCamera.cancelAutoFocus(); - } - } - } - - /** - * Sets camera auto-focus move callback. - * - * @param cb the callback to run - * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - return false; - } - - synchronized (mCameraLock) { - if (mCamera != null) { - CameraAutoFocusMoveCallback autoFocusMoveCallback = null; - if (cb != null) { - autoFocusMoveCallback = new CameraAutoFocusMoveCallback(); - autoFocusMoveCallback.mDelegate = cb; - } - mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback); - } - } - - return true; - } - - //============================================================================================== - // Private - //============================================================================================== - - /** - * Only allow creation via the builder class. - */ - private CameraSource() { - } - - /** - * Wraps the camera1 shutter callback so that the deprecated API isn't exposed. - */ - private class PictureStartCallback implements Camera.ShutterCallback { - private ShutterCallback mDelegate; - - @Override - public void onShutter() { - if (mDelegate != null) { - mDelegate.onShutter(); - } - } - } - - /** - * Wraps the final callback in the camera sequence, so that we can automatically turn the camera - * preview back on after the picture has been taken. - */ - private class PictureDoneCallback implements Camera.PictureCallback { - private PictureCallback mDelegate; - - @Override - public void onPictureTaken(byte[] data, Camera camera) { - if (mDelegate != null) { - mDelegate.onPictureTaken(data); - } - synchronized (mCameraLock) { - if (mCamera != null) { - mCamera.startPreview(); - } - } - } - } - - /** - * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed. - */ - private class CameraAutoFocusCallback implements Camera.AutoFocusCallback { - private AutoFocusCallback mDelegate; - - @Override - public void onAutoFocus(boolean success, Camera camera) { - if (mDelegate != null) { - mDelegate.onAutoFocus(success); - } - } - } - - /** - * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback { - private AutoFocusMoveCallback mDelegate; - - @Override - public void onAutoFocusMoving(boolean start, Camera camera) { - if (mDelegate != null) { - mDelegate.onAutoFocusMoving(start); - } - } - } - - /** - * Opens the camera and applies the user settings. - * - * @throws RuntimeException if the method fails - */ - @SuppressLint("InlinedApi") - private Camera createCamera() { - int requestedCameraId = getIdForRequestedCamera(mFacing); - if (requestedCameraId == -1) { - throw new RuntimeException("Could not find requested camera."); - } - Camera camera = Camera.open(requestedCameraId); - - SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight); - if (sizePair == null) { - throw new RuntimeException("Could not find suitable preview size."); - } - Size pictureSize = sizePair.pictureSize(); - mPreviewSize = sizePair.previewSize(); - - int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps); - if (previewFpsRange == null) { - throw new RuntimeException("Could not find suitable preview frames per second range."); - } - - Camera.Parameters parameters = camera.getParameters(); - - if (pictureSize != null) { - parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); - } - - parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); - parameters.setPreviewFpsRange( - previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], - previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); - parameters.setPreviewFormat(ImageFormat.NV21); - - setRotation(camera, parameters, requestedCameraId); - - if (mFocusMode != null) { - if (parameters.getSupportedFocusModes().contains( - mFocusMode)) { - parameters.setFocusMode(mFocusMode); - } else { - Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device."); - } - } - - // setting mFocusMode to the one set in the params - mFocusMode = parameters.getFocusMode(); - - if (mFlashMode != null) { - if (parameters.getSupportedFlashModes() != null) { - if (parameters.getSupportedFlashModes().contains( - mFlashMode)) { - parameters.setFlashMode(mFlashMode); - } else { - Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device."); - } - } - } - - // setting mFlashMode to the one set in the params - mFlashMode = parameters.getFlashMode(); - - camera.setParameters(parameters); - - // Four frame buffers are needed for working with the camera: - // - // one for the frame that is currently being executed upon in doing detection - // one for the next pending frame to process immediately upon completing detection - // two for the frames that the camera uses to populate future preview images - camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize)); - - return camera; - } - - /** - * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such - * camera was found. - * - * @param facing the desired camera (front-facing or rear-facing) - */ - private static int getIdForRequestedCamera(int facing) { - CameraInfo cameraInfo = new CameraInfo(); - for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == facing) { - return i; - } - } - return -1; - } - - /** - * Selects the most suitable preview and picture size, given the desired width and height. - *

- * Even though we may only need the preview size, it's necessary to find both the preview - * size and the picture size of the camera together, because these need to have the same aspect - * ratio. On some hardware, if you would only set the preview size, you will get a distorted - * image. - * - * @param camera the camera to select a preview size from - * @param desiredWidth the desired width of the camera preview frames - * @param desiredHeight the desired height of the camera preview frames - * @return the selected preview and picture size pair - */ - private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { - List validPreviewSizes = generateValidPreviewSizeList(camera); - - // The method for selecting the best size is to minimize the sum of the differences between - // the desired values and the actual values for width and height. This is certainly not the - // only way to select the best size, but it provides a decent tradeoff between using the - // closest aspect ratio vs. using the closest pixel area. - SizePair selectedPair = null; - int minDiff = Integer.MAX_VALUE; - for (SizePair sizePair : validPreviewSizes) { - Size size = sizePair.previewSize(); - int diff = Math.abs(size.getWidth() - desiredWidth) + - Math.abs(size.getHeight() - desiredHeight); - if (diff < minDiff) { - selectedPair = sizePair; - minDiff = diff; - } - } - - return selectedPair; - } - - /** - * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted - * preview images on some devices, the picture size must be set to a size that is the same - * aspect ratio as the preview size or the preview may end up being distorted. If the picture - * size is null, then there is no picture size with the same aspect ratio as the preview size. - */ - private static class SizePair { - private Size mPreview; - private Size mPicture; - - public SizePair(android.hardware.Camera.Size previewSize, - android.hardware.Camera.Size pictureSize) { - mPreview = new Size(previewSize.width, previewSize.height); - if (pictureSize != null) { - mPicture = new Size(pictureSize.width, pictureSize.height); - } - } - - public Size previewSize() { - return mPreview; - } - - @SuppressWarnings("unused") - public Size pictureSize() { - return mPicture; - } - } - - /** - * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is - * not a corresponding picture size of the same aspect ratio. If there is a corresponding - * picture size of the same aspect ratio, the picture size is paired up with the preview size. - *

- * This is necessary because even if we don't use still pictures, the still picture size must be - * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the - * preview images may be distorted on some devices. - */ - private static List generateValidPreviewSizeList(Camera camera) { - Camera.Parameters parameters = camera.getParameters(); - List supportedPreviewSizes = - parameters.getSupportedPreviewSizes(); - List supportedPictureSizes = - parameters.getSupportedPictureSizes(); - List validPreviewSizes = new ArrayList(); - for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { - float previewAspectRatio = (float) previewSize.width / (float) previewSize.height; - - // By looping through the picture sizes in order, we favor the higher resolutions. - // We choose the highest resolution in order to support taking the full resolution - // picture later. - for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) { - float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height; - if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { - validPreviewSizes.add(new SizePair(previewSize, pictureSize)); - break; - } - } - } - - // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all - // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we - // still account for it. - if (validPreviewSizes.size() == 0) { - Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size"); - for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) { - // The null picture size will let us know that we shouldn't set a picture size. - validPreviewSizes.add(new SizePair(previewSize, null)); - } - } - - return validPreviewSizes; - } - - /** - * Selects the most suitable preview frames per second range, given the desired frames per - * second. - * - * @param camera the camera to select a frames per second range from - * @param desiredPreviewFps the desired frames per second for the camera preview frames - * @return the selected preview frames per second range - */ - private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) { - // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame - // rates. - int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f); - - // The method for selecting the best range is to minimize the sum of the differences between - // the desired value and the upper and lower bounds of the range. This may select a range - // that the desired value is outside of, but this is often preferred. For example, if the - // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the - // range (15, 30). - int[] selectedFpsRange = null; - int minDiff = Integer.MAX_VALUE; - List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange(); - for (int[] range : previewFpsRangeList) { - int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; - int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; - int diff = Math.abs(deltaMin) + Math.abs(deltaMax); - if (diff < minDiff) { - selectedFpsRange = range; - minDiff = diff; - } - } - return selectedFpsRange; - } - - /** - * Calculates the correct rotation for the given camera id and sets the rotation in the - * parameters. It also sets the camera's display orientation and rotation. - * - * @param parameters the camera parameters for which to set the rotation - * @param cameraId the camera id to set rotation based on - */ - private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) { - WindowManager windowManager = - (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - int degrees = 0; - int rotation = windowManager.getDefaultDisplay().getRotation(); - switch (rotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 90; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 270; - break; - default: - Log.e(TAG, "Bad rotation value: " + rotation); - } - - CameraInfo cameraInfo = new CameraInfo(); - Camera.getCameraInfo(cameraId, cameraInfo); - - int angle; - int displayAngle; - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - angle = (cameraInfo.orientation + degrees) % 360; - displayAngle = (360 - angle) % 360; // compensate for it being mirrored - } else { // back-facing - angle = (cameraInfo.orientation - degrees + 360) % 360; - displayAngle = angle; - } - - // This corresponds to the rotation constants in {@link Frame}. - mRotation = angle / 90; - - camera.setDisplayOrientation(displayAngle); - parameters.setRotation(angle); - } - - /** - * Creates one buffer for the camera preview callback. The size of the buffer is based off of - * the camera preview size and the format of the camera image. - * - * @return a new preview buffer of the appropriate size for the current camera settings - */ - private byte[] createPreviewBuffer(Size previewSize) { - int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); - long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; - int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1; - - // - // NOTICE: This code only works when using play services v. 8.1 or higher. - // - - // Creating the byte array this way and wrapping it, as opposed to using .allocate(), - // should guarantee that there will be an array to work with. - byte[] byteArray = new byte[bufferSize]; - ByteBuffer buffer = ByteBuffer.wrap(byteArray); - if (!buffer.hasArray() || (buffer.array() != byteArray)) { - // I don't think that this will ever happen. But if it does, then we wouldn't be - // passing the preview content to the underlying detector later. - throw new IllegalStateException("Failed to create valid buffer for camera source."); - } - - mBytesToByteBuffer.put(byteArray, buffer); - return byteArray; - } - - //============================================================================================== - // Frame processing - //============================================================================================== - - /** - * Called when the camera has a new preview frame. - */ - private class CameraPreviewCallback implements Camera.PreviewCallback { - @Override - public void onPreviewFrame(byte[] data, Camera camera) { - mFrameProcessor.setNextFrame(data, camera); - } - } - - /** - * This runnable controls access to the underlying receiver, calling it to process frames when - * available from the camera. This is designed to run detection on frames as fast as possible - * (i.e., without unnecessary context switching or waiting on the next frame). - *

- * While detection is running on a frame, new frames may be received from the camera. As these - * frames come in, the most recent frame is held onto as pending. As soon as detection and its - * associated processing are done for the previous frame, detection on the mostly recently - * received frame will immediately start on the same thread. - */ - private class FrameProcessingRunnable implements Runnable { - private Detector mDetector; - private long mStartTimeMillis = SystemClock.elapsedRealtime(); - - // This lock guards all of the member variables below. - private final Object mLock = new Object(); - private boolean mActive = true; - - // These pending variables hold the state associated with the new frame awaiting processing. - private long mPendingTimeMillis; - private int mPendingFrameId = 0; - private ByteBuffer mPendingFrameData; - - FrameProcessingRunnable(Detector detector) { - mDetector = detector; - } - - /** - * Releases the underlying receiver. This is only safe to do after the associated thread - * has completed, which is managed in camera source's release method above. - */ - @SuppressLint("Assert") - void release() { - assert (mProcessingThread.getState() == State.TERMINATED); - mDetector.release(); - mDetector = null; - } - - /** - * Marks the runnable as active/not active. Signals any blocked threads to continue. - */ - void setActive(boolean active) { - synchronized (mLock) { - mActive = active; - mLock.notifyAll(); - } - } - - /** - * Sets the frame data received from the camera. This adds the previous unused frame buffer - * (if present) back to the camera, and keeps a pending reference to the frame data for - * future use. - */ - void setNextFrame(byte[] data, Camera camera) { - synchronized (mLock) { - if (mPendingFrameData != null) { - camera.addCallbackBuffer(mPendingFrameData.array()); - mPendingFrameData = null; - } - - if (!mBytesToByteBuffer.containsKey(data)) { - Log.d(TAG, - "Skipping frame. Could not find ByteBuffer associated with the image " + - "data from the camera."); - return; - } - - // Timestamp and frame ID are maintained here, which will give downstream code some - // idea of the timing of frames received and when frames were dropped along the way. - mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis; - mPendingFrameId++; - mPendingFrameData = mBytesToByteBuffer.get(data); - - // Notify the processor thread if it is waiting on the next frame (see below). - mLock.notifyAll(); - } - } - - /** - * As long as the processing thread is active, this executes detection on frames - * continuously. The next pending frame is either immediately available or hasn't been - * received yet. Once it is available, we transfer the frame info to local variables and - * run detection on that frame. It immediately loops back for the next frame without - * pausing. - *

- * If detection takes longer than the time in between new frames from the camera, this will - * mean that this loop will run without ever waiting on a frame, avoiding any context - * switching or frame acquisition time latency. - *

- * If you find that this is using more CPU than you'd like, you should probably decrease the - * FPS setting above to allow for some idle time in between frames. - */ - @Override - public void run() { - Frame outputFrame; - ByteBuffer data; - - while (true) { - synchronized (mLock) { - while (mActive && (mPendingFrameData == null)) { - try { - // Wait for the next frame to be received from the camera, since we - // don't have it yet. - mLock.wait(); - } catch (InterruptedException e) { - Log.d(TAG, "Frame processing loop terminated.", e); - return; - } - } - - if (!mActive) { - // Exit the loop once this camera source is stopped or released. We check - // this here, immediately after the wait() above, to handle the case where - // setActive(false) had been called, triggering the termination of this - // loop. - return; - } - - outputFrame = new Frame.Builder() - .setImageData(mPendingFrameData, mPreviewSize.getWidth(), - mPreviewSize.getHeight(), ImageFormat.NV21) - .setId(mPendingFrameId) - .setTimestampMillis(mPendingTimeMillis) - .setRotation(mRotation) - .build(); - - // Hold onto the frame data locally, so that we can use this for detection - // below. We need to clear mPendingFrameData to ensure that this buffer isn't - // recycled back to the camera before we are done using that data. - data = mPendingFrameData; - mPendingFrameData = null; - } - - // The code below needs to run outside of synchronization, because this will allow - // the camera to add pending frame(s) while we are running detection on the current - // frame. - - try { - mDetector.receiveFrame(outputFrame); - } catch (Throwable t) { - Log.e(TAG, "Exception thrown from receiver.", t); - } finally { - mCamera.addCallbackBuffer(data.array()); - } - } - } - } -} diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java b/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java deleted file mode 100644 index 258628ef8..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/CameraSourcePreview.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.bitcoin.cordova.qrreader; - - -import android.Manifest; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Color; -import android.support.annotation.RequiresPermission; -import android.util.AttributeSet; -import android.util.Log; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.ViewGroup; - -import com.google.android.gms.common.images.Size; - -import java.io.IOException; - -public class CameraSourcePreview extends ViewGroup { - private static final String TAG = "CameraSourcePreview"; - - private Context mContext; - private SurfaceView mSurfaceView; - private boolean mStartRequested; - private boolean mSurfaceAvailable; - private CameraSource mCameraSource; - - //private GraphicOverlay mOverlay; - - public CameraSourcePreview(Context context) { - super(context); - mContext = context; - mStartRequested = false; - mSurfaceAvailable = false; - - setBackgroundColor(Color.BLACK); - - mSurfaceView = new SurfaceView(context); - mSurfaceView.getHolder().addCallback(new SurfaceCallback()); - addView(mSurfaceView); - } - - public CameraSourcePreview(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - mStartRequested = false; - mSurfaceAvailable = false; - - mSurfaceView = new SurfaceView(context); - mSurfaceView.getHolder().addCallback(new SurfaceCallback()); - addView(mSurfaceView); - } - - @RequiresPermission(Manifest.permission.CAMERA) - public void start(CameraSource cameraSource) throws IOException, SecurityException { - if (cameraSource == null) { - stop(); - } - - mCameraSource = cameraSource; - - if (mCameraSource != null) { - mStartRequested = true; - startIfReady(); - } - } - - /* - @RequiresPermission(Manifest.permission.CAMERA) - public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException { - mOverlay = overlay; - start(cameraSource); - } - */ - - public void stop() { - if (mCameraSource != null) { - mCameraSource.stop(); - } - } - - public void release() { - if (mCameraSource != null) { - mCameraSource.release(); - mCameraSource = null; - } - } - - @RequiresPermission(Manifest.permission.CAMERA) - private void startIfReady() throws IOException, SecurityException { - if (mStartRequested && mSurfaceAvailable) { - mCameraSource.start(mSurfaceView.getHolder()); - /* - if (mOverlay != null) { - Size size = mCameraSource.getPreviewSize(); - int min = Math.min(size.getWidth(), size.getHeight()); - int max = Math.max(size.getWidth(), size.getHeight()); - if (isPortraitMode()) { - // Swap width and height sizes when in portrait, since it will be rotated by - // 90 degrees - mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing()); - } else { - mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing()); - } - mOverlay.clear(); - } - */ - mStartRequested = false; - } - } - - private class SurfaceCallback implements SurfaceHolder.Callback { - @Override - public void surfaceCreated(SurfaceHolder surface) { - mSurfaceAvailable = true; - try { - startIfReady(); - } catch (SecurityException se) { - Log.e(TAG,"Do not have permission to start the camera", se); - } catch (IOException e) { - Log.e(TAG, "Could not start camera source.", e); - } - } - - @Override - public void surfaceDestroyed(SurfaceHolder surface) { - mSurfaceAvailable = false; - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int width = 320; - int height = 240; - if (mCameraSource != null) { - Size size = mCameraSource.getPreviewSize(); - if (size != null) { - width = size.getWidth(); - height = size.getHeight(); - } - } - - // Swap width and height sizes when in portrait, since it will be rotated 90 degrees - if (isPortraitMode()) { - int tmp = width; - //noinspection SuspiciousNameCombination - width = height; - height = tmp; - } - - final int layoutWidth = right - left; - final int layoutHeight = bottom - top; - - // Computes height and width for potentially doing fit width. - int childWidth = layoutWidth; - int childHeight = (int)(((float) layoutWidth / (float) width) * height); - - // If height is too tall using fit width, does fit height instead. - if (childHeight > layoutHeight) { - childHeight = layoutHeight; - childWidth = (int)(((float) layoutHeight / (float) height) * width); - } - - // Centre the child in the preview bounds - int childTop = (layoutHeight - childHeight) / 2; - int childBottom = childTop + childHeight; - - int childLeft = (layoutWidth - childWidth) / 2; - int childRight = childLeft + childWidth; - - for (int i = 0; i < getChildCount(); ++i) { - getChildAt(i).layout(childLeft, childTop, childRight, childBottom); - } - - try { - startIfReady(); - } catch (SecurityException se) { - Log.e(TAG,"Do not have permission to start the camera", se); - } catch (IOException e) { - Log.e(TAG, "Could not start camera source.", e); - } - } - - private boolean isPortraitMode() { - int orientation = mContext.getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - return false; - } - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - return true; - } - - Log.d(TAG, "isPortraitMode returning false by default"); - return false; - } -} - diff --git a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java b/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java deleted file mode 100644 index fd2583a48..000000000 --- a/platforms/android/src/com/bitcoin/cordova/qrreader/QRReader.java +++ /dev/null @@ -1,381 +0,0 @@ - -package com.bitcoin.cordova.qrreader; - - - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.hardware.Camera; -import android.net.Uri; -import android.os.Build; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.vision.MultiProcessor; -import com.google.android.gms.vision.barcode.Barcode; -import com.google.android.gms.vision.barcode.BarcodeDetector; - -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaInterface; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.provider.Settings; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.Toast; - - - -public class QRReader extends CordovaPlugin implements BarcodeUpdateListener { - public static final String TAG = "QRReader"; - - enum QRReaderPermissionResult { - PERMISSION_DENIED, - PERMISSION_GRANTED - } - - enum QRReaderError { - // Shared with iOS - ERROR_PERMISSION_DENIED, - ERROR_SCANNING_UNSUPPORTED, - ERROR_OPEN_SETTINGS_UNAVAILABLE, - - // Android specific - ERROR_CAMERA_FAILED_TO_START, - ERROR_CAMERA_SECURITY_EXCEPTION, - ERROR_CAMERA_UNAVAILABLE, - ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE, - ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE, - ERROR_READ_ALREADY_STARTED, - ERROR_UI_SETUP_FAILED - } - - public static final String CAMERA = Manifest.permission.CAMERA; - public static final int CAMERA_REQ_CODE = 774980; - - // intent request code to handle updating play services if needed. - private static final int RC_HANDLE_GMS = 9001; - - - private Map mBarcodes = new HashMap(); - private CameraSource mCameraSource; - private CameraSourcePreview mCameraSourcePreview; - private CallbackContext mStartCallbackContext; - private CallbackContext mPermissionCallbackContext; - - public QRReader() { - } - - - public void initialize(CordovaInterface cordova, CordovaWebView webView) { - super.initialize(cordova, webView); - //QRReader.uuid = getUuid(); - - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - } - - }); - } - - - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - Log.d(TAG, "execute() with \"" + action + "\""); - if ("openSettings".equals(action)) { - openSettings(callbackContext); - } else if ("startReading".equals(action)) { - startReading(callbackContext); - } else if ("stopReading".equals(action)) { - stopReading(callbackContext); - } else if ("checkPermission".equals(action)) { - checkPermission(callbackContext); - } else { - return false; - } - return true; - } - - @Override - public void onBarcodeDetected(Barcode barcode) { - String contents = barcode.rawValue; - Log.d(TAG, "Detected new barcode."); - if (mStartCallbackContext != null) { - mStartCallbackContext.success(contents); - mStartCallbackContext = null; - } else { - Log.e(TAG, "No callback context when detecting new barcode."); - } - } - - - /** - * Creates and starts the camera. Note that this uses a higher resolution in comparison - * to other detection examples to enable the barcode detector to detect small barcodes - * at long distances. - * - * Suppressing InlinedApi since there is a check that the minimum version is met before using - * the constant. - */ - @SuppressLint("InlinedApi") - private Boolean createCameraSource(Context context, boolean useFlash, CallbackContext callbackContext) { - Log.d(TAG, "createCameraSource()"); - - boolean autoFocus = true; - // A barcode detector is created to track barcodes. An associated multi-processor instance - // is set to receive the barcode detection results, track the barcodes, and maintain - // graphics for each barcode on screen. The factory is used by the multi-processor to - // create a separate tracker instance for each barcode. - BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build(); - BarcodeMapTrackerFactory barcodeFactory = new BarcodeMapTrackerFactory(mBarcodes, this); - barcodeDetector.setProcessor( - new MultiProcessor.Builder(barcodeFactory).build()); - - if (!barcodeDetector.isOperational()) { - // Note: The first time that an app using the barcode or face API is installed on a - // device, GMS will download a native libraries to the device in order to do detection. - // Usually this completes before the app is run for the first time. But if that - // download has not yet completed, then the above call will not detect any barcodes - // and/or faces. - // - // isOperational() can be used to check if the required native libraries are currently - // available. The detectors will automatically become operational once the library - // downloads complete on device. - Log.w(TAG, "Detector dependencies are not yet available."); - callbackContext.error(QRReaderError.ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE.name()); - return false; - - - /* TODO: Handle this better later? - // Check for low storage. If there is low storage, the native library will not be - // downloaded, so detection will not become operational. - IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); - boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null; - - if (hasLowStorage) { - Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show(); - Log.w(TAG, "Low storage error."); - } - */ - } - - // Creates and starts the camera. Note that this uses a higher resolution in comparison - // to other detection examples to enable the barcode detector to detect small barcodes - // at long distances. - CameraSource.Builder builder = new CameraSource.Builder(context, barcodeDetector) - .setFacing(CameraSource.CAMERA_FACING_BACK) - .setRequestedPreviewSize(1600, 1024) - .setRequestedFps(15.0f); - - // make sure that auto focus is an available option - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - builder = builder.setFocusMode( - autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null); - } - - mCameraSource = builder - .setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null) - .build(); - - return true; - } - - public void onRequestPermissionResult(int requestCode, String[] permissions, - int[] grantResults) throws JSONException - { - Log.d(TAG, "onRequestPermissionResult()"); - - if (requestCode == CAMERA_REQ_CODE) { - for (int r : grantResults) { - if (r == PackageManager.PERMISSION_DENIED) { - if (this.mPermissionCallbackContext != null) { - this.mPermissionCallbackContext.success(QRReaderPermissionResult.PERMISSION_DENIED.name()); - this.mPermissionCallbackContext = null; - } - return; - } - } - - if (this.mPermissionCallbackContext != null) { - this.mPermissionCallbackContext.success(QRReaderPermissionResult.PERMISSION_GRANTED.name()); - this.mPermissionCallbackContext = null; - } - } - - } - - private void initPreview(@Nullable CallbackContext callbackContext) { - final ViewGroup viewGroup = ((ViewGroup) webView.getView().getParent()); - if (viewGroup == null) { - Log.e(TAG, "Failed to get view group"); - if (callbackContext != null) { - callbackContext.error(QRReaderError.ERROR_UI_SETUP_FAILED.name()); - } - return; - } - - final Context context = cordova.getActivity().getApplicationContext(); - mCameraSourcePreview = new CameraSourcePreview(context); - - FrameLayout.LayoutParams childCenterLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER); - viewGroup.addView(mCameraSourcePreview, childCenterLayout); - } - - private void checkPermission(CallbackContext callbackContext) { - if(cordova.hasPermission(CAMERA)) { - callbackContext.success(QRReaderPermissionResult.PERMISSION_GRANTED.name()); - } else { - mPermissionCallbackContext = callbackContext; - cordova.requestPermission(this, CAMERA_REQ_CODE, CAMERA); - } - } - - private void openSettings(CallbackContext callbackContext) { - Log.d(TAG, "openSettings()"); - try { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", this.cordova.getActivity().getPackageName(), null); - intent.setData(uri); - Log.d(TAG, "Starting settings activity..."); - this.cordova.getActivity().getApplicationContext().startActivity(intent); - - callbackContext.success(); - //Log.d(TAG, "About to start reading."); - //startReading(callbackContext); - - } catch (Exception e) { - Log.e(TAG, "Error opening settings. " + e.getMessage()); - callbackContext.error(QRReaderError.ERROR_OPEN_SETTINGS_UNAVAILABLE.name()); - } - - } - - /** - * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet - * (e.g., because onResume was called before the camera source was created), this will be called - * again when the camera source is created. - */ - private Boolean startCameraSource(Context context, CallbackContext callbackContext) throws SecurityException { - - - // check that the device has play services available. - int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); - if (code != ConnectionResult.SUCCESS) { - Dialog dlg = - GoogleApiAvailability.getInstance().getErrorDialog(cordova.getActivity(), code, RC_HANDLE_GMS); - dlg.show(); - callbackContext.error(QRReaderError.ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE.name()); - return false; - } - - // TODO: Check for valid mCameraSourcePreview - if (mCameraSource != null) { - try { - mCameraSourcePreview.start(mCameraSource); - } catch (IOException e) { - Log.e(TAG, "Unable to start camera source.", e); - mCameraSource.release(); - mCameraSource = null; - callbackContext.error(QRReaderError.ERROR_CAMERA_FAILED_TO_START.name() + " " + e.getMessage()); - return false; - } - } else { - Log.e(TAG, "No camera source to start."); - callbackContext.error(QRReaderError.ERROR_CAMERA_UNAVAILABLE.name()); - return false; - } - return true; - } - - private void startReading(CallbackContext callbackContext) { - Log.d(TAG, "startReading()"); - mStartCallbackContext = callbackContext; - - if(cordova.hasPermission(CAMERA)) { - startReadingWithPermission(callbackContext); - } else { - callbackContext.error(QRReaderError.ERROR_PERMISSION_DENIED.name()); - } - } - - private void startReadingWithPermission(final CallbackContext callbackContext) { - Log.d(TAG, "startReadingWithPermission()"); - - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - - if (mCameraSourcePreview == null) { - initPreview(callbackContext); - } - - if (mCameraSourcePreview != null) { - - final Context context = cordova.getActivity().getApplicationContext(); - if (mCameraSource == null) { - if (!createCameraSource(context, false, callbackContext)) { - return; - } - } else { - Log.d(TAG, "mCamera source was not null"); - callbackContext.error(QRReaderError.ERROR_READ_ALREADY_STARTED.name()); - return; - } - - webView.getView().setBackgroundColor(Color.argb(1, 0, 0, 0)); - - webView.getView().bringToFront(); - //viewGroup.bringChildToFront(preview); - //viewGroup.bringChildToFront(webView.getView()); - - try { - startCameraSource(context, callbackContext); - } catch (SecurityException e) { - Log.e(TAG, "Security Exception when starting camera source. " + e.getMessage()); - callbackContext.error(QRReaderError.ERROR_CAMERA_SECURITY_EXCEPTION.name() + " " + e.getMessage()); - return; - } - - } - } - }); - - - } - - private void stopReading(CallbackContext callbackContext) { - Log.d(TAG, "stopReading()"); - - if (mCameraSource != null) { - mCameraSource.stop(); - mCameraSource = null; - Log.d(TAG, "Set mCameraSource to null."); - } - webView.getView().setBackgroundColor(Color.WHITE); - - callbackContext.success("stopped"); - } - -} \ No newline at end of file From ebe4cbaff4db4427f915a9654fffba9899560542 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 19:32:07 +1300 Subject: [PATCH 63/65] Added errors to desktop scanner service. --- src/js/services/qr-scanner.service.js | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/js/services/qr-scanner.service.js b/src/js/services/qr-scanner.service.js index b3830577d..2cfa08d91 100644 --- a/src/js/services/qr-scanner.service.js +++ b/src/js/services/qr-scanner.service.js @@ -14,10 +14,18 @@ var errors = { // Common - + permissionDenied: 'ERROR_PERMISSION_DENIED', + scanningUnsupported: 'ERROR_SCANNING_UNSUPPORTED', + openSettingsUnavailable: 'ERROR_OPEN_SETTINGS_UNAVAILABLE', // Android - + cameraFailedToStart: 'ERROR_CAMERA_FAILED_TO_START', + cameraSecurityException: 'ERROR_CAMERA_SECURITY_EXCEPTION', + cameraUnavailable: 'ERROR_CAMERA_UNAVAILABLE', + detectorDependenciesUnavailable: 'ERROR_DETECTOR_DEPENDENCIES_UNAVAILABLE', + googlePlayServicesUnavailable: 'ERROR_GOOGLE_PLAY_SERVICES_UNAVAILABLE', + readAlreadyStarted: 'ERROR_READ_ALREADY_STARTED', + errorUiSetupFailed: 'ERROR_UI_SETUP_FAILED' // Desktop @@ -29,6 +37,8 @@ var scanDeferred = null; var service = { + errors: errors, + // Functions openSettings: openSettings, startReading: startReading, @@ -105,14 +115,22 @@ function checkPermission() { var deferred = $q.defer(); - // qrService.getStatus(function(status){ - // if(!status.authorized){ - // deferred.reject(status); - // } else { - // deferred.resolve(status); - // } - // }); - deferred.resolve(status); + qrService.getStatus(function onStatus(status) { + var result = ''; + if (status.authorized) { + result = 'PERMISSION_GRANTED'; + } else if (status.authorized) { + result = 'PERMISSION_DENIED'; + } else if (status.restricted) { + result = 'PEMISSION_RESTRICTED' + } else { + result = 'PERMISSION_NOT_DETERMINED'; + } + + console.log('Desktop QR scanner returning permission "' + result + '"'); + deferred.resolve(result); + }); + return deferred.promise; } From dc7d85dce58f45eb83948024dd41e14493810db7 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 26 Feb 2019 19:33:11 +1300 Subject: [PATCH 64/65] Removed plugin file from platforms folder. --- .../cordova-plugin-qrreader/www/qrreader.js | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js diff --git a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js b/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js deleted file mode 100644 index dbe61f740..000000000 --- a/platforms/android/platform_www/plugins/cordova-plugin-qrreader/www/qrreader.js +++ /dev/null @@ -1,34 +0,0 @@ -cordova.define("cordova-plugin-qrreader.qrreader", function(require, exports, module) { - -var argscheck = require('cordova/argscheck'); -var exec = require('cordova/exec'); - - -function QRReader() { - this.testString = 'hello1'; -} - -QRReader.prototype.openSettings = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.openSettings', arguments); - exec(successCallback, errorCallback, 'QRReader', 'openSettings', []); -}; - -QRReader.prototype.checkPermission = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.checkPermission', arguments); - exec(successCallback, errorCallback, 'QRReader', 'checkPermission', []); -}; - -QRReader.prototype.startReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.startReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'startReading', []); -}; - -QRReader.prototype.stopReading = function (successCallback, errorCallback) { - argscheck.checkArgs('fF', 'QRReader.stopReading', arguments); - exec(successCallback, errorCallback, 'QRReader', 'stopReading', []); -}; - -module.exports = new QRReader(); - - -}); From 4a3686ac1ebaaa8c33f0b6a71b888cb7d507b87f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Tue, 26 Feb 2019 18:40:12 +0900 Subject: [PATCH 65/65] Remove comments --- src/js/routes.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/js/routes.js b/src/js/routes.js index 35a6df5c9..879ac54f5 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1429,42 +1429,6 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr if (platformInfo.isCordova) { channel = "firebase"; } - - //console.log('calling getInfo()...'); - //window.Device.getInfo(); - if (window.qrreader) { - console.log('qrreader found.'); - /* - console.log('qrreader present with testString:', window.qrreader.testString); - console.log('qrreader get test info.'); - window.qrreader.getTestInfo( - function onSuccess(result) { - console.log('qrreader getTestInfo() result:', result); - }, - function onError(error) { - console.error('qrreader getTestInfo() error:', error); - }); - - window.qrreader.startReading( - function onSuccess(result) { - console.log('qrreader startReading() result:', result); - }, - function onError(error) { - console.error('qrreader startReading() error:', error); - }); - - window.qrreader.stopReading( - function onSuccess(result) { - console.log('qrreader stopReading() result:', result); - }, - function onError(error) { - console.error('qrreader stopReading() error:', error); - }); - */ - } else { - console.log('qrreader missing.'); - } - // Send a log to test var log = new window.BitAnalytics.LogEvent("wallet_opened", [{}, {}, {}], [channel, 'leanplum']);