diff --git a/app/build.gradle b/app/build.gradle index 3900d36317a0b1199a4374fce581f6599f4668bf..aeef040605e5af6a3f740dbc404cb649b56c5140 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { defaultConfig { applicationId "foundation.e.camera" - minSdkVersion 25 + minSdkVersion 26 targetSdkVersion 33 renderscriptTargetApi 21 @@ -79,6 +79,9 @@ dependencies { implementation 'androidx.exifinterface:exifinterface:1.3.6' + implementation 'androidx.camera:camera-core:1.2.3' + implementation 'com.google.zxing:core:3.5.2' + testImplementation 'junit:junit:4.13.2' // newer AndroidJUnit4 InstrumentedTest diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 8b0bffe862d3ed4d6103d359832e1f1bc2448019..692599deee5ceb397a2946aa4e29d7c248857c50 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -83,6 +83,7 @@ import net.sourceforge.opencamera.cameracontroller.CameraControllerManager; import net.sourceforge.opencamera.cameracontroller.CameraControllerManager2; import net.sourceforge.opencamera.preview.Preview; import net.sourceforge.opencamera.preview.VideoProfile; +import net.sourceforge.opencamera.qr.QrImageAnalyzer; import net.sourceforge.opencamera.remotecontrol.BluetoothRemoteControl; import net.sourceforge.opencamera.ui.CircleImageView; import net.sourceforge.opencamera.ui.DrawPreview; @@ -109,6 +110,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import foundation.e.camera.R; +import kotlin.coroutines.CoroutineContext; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Dispatchers; +import kotlinx.coroutines.Job; +import kotlinx.coroutines.JobKt; /** * The main Activity for Open Camera. @@ -241,6 +247,11 @@ public class MainActivity extends AppCompatActivity { //public static final boolean lock_to_landscape = true; public static final boolean lock_to_landscape = false; + // QRCode + public QrImageAnalyzer qrImageAnalyzer; + private Job job; + private CoroutineScope coroutineScope; + // handling for lock_to_landscape==false: public enum SystemOrientation { @@ -273,6 +284,16 @@ public class MainActivity extends AppCompatActivity { Log.d(TAG, "activity_count: " + activity_count); super.onCreate(savedInstanceState); + //QRCode + job = JobKt.Job(null); + coroutineScope = new CoroutineScope() { + @Override + public CoroutineContext getCoroutineContext() { + return Dispatchers.getMain().plus(job); + } + }; + qrImageAnalyzer = new QrImageAnalyzer(this, coroutineScope); + setContentView(R.layout.activity_main); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values @@ -2661,6 +2682,45 @@ public class MainActivity extends AppCompatActivity { } } + /* getBetterQRCodeCameraID() + Returns the best camera ID, based on the fact that it's probably the first rear camera available. + The user always has the option of selecting with the lens switcher in case the choice is wrong. + Returns -1 if no camera available. In that case we do *NOT* trig any switch. + */ + public int getBetterQRCodeCameraID() { + int best_qrcode_camera = -1; + if( MyDebug.LOG ) + Log.d(TAG, "getBetterQRCodeCameraID"); + if( !isMultiCamEnabled() ) { + Log.e(TAG, "getBetterQRCodeCameraID switch multi camera icon shouldn't have been visible"); + return best_qrcode_camera; + } + if( preview.isOpeningCamera() ) { + if( MyDebug.LOG ) + Log.d(TAG, "getBetterQRCodeCameraID already opening camera in background thread"); + return best_qrcode_camera; + } + if( this.preview.canSwitchCamera() ) { + try { + CameraManager _cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); + for (String _cameraId : _cameraManager.getCameraIdList()) { + CameraCharacteristics characteristics = _cameraManager.getCameraCharacteristics(_cameraId); + Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { + best_qrcode_camera = Integer.parseInt(_cameraId); + if( MyDebug.LOG ) + Log.d(TAG, "best_qrcode_camera ="+best_qrcode_camera); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return best_qrcode_camera; + } + + private void updateMultiCameraIcon() { Button multiCameraButton = findViewById(R.id.switch_multi_camera); @@ -6258,7 +6318,7 @@ public class MainActivity extends AppCompatActivity { simple = false; } } - else { + else { //Camera if( photo_mode == MyApplicationInterface.PhotoMode.Panorama ) { // don't show resolution in panorama mode toast_string = ""; @@ -6292,89 +6352,92 @@ public class MainActivity extends AppCompatActivity { simple = false; } } - if( applicationInterface.getFaceDetectionPref() ) { - // important so that the user realises why touching for focus/metering areas won't work - easy to forget that face detection has been turned on! - toast_string += "\n" + getResources().getString(R.string.preference_face_detection); - simple = false; - } - if( !video_high_speed ) { - //manual ISO only supported for high speed video - String iso_value = applicationInterface.getISOPref(); - if( !iso_value.equals(CameraController.ISO_DEFAULT) ) { - toast_string += "\nISO: " + iso_value; - if( preview.supportsExposureTime() ) { - long exposure_time_value = applicationInterface.getExposureTimePref(); - toast_string += " " + preview.getExposureTimeString(exposure_time_value); - } - simple = false; - } - int current_exposure = camera_controller.getExposureCompensation(); - if( current_exposure != 0 ) { - toast_string += "\n" + preview.getExposureCompensationString(current_exposure); - simple = false; - } - } - try { - String scene_mode = camera_controller.getSceneMode(); - String white_balance = camera_controller.getWhiteBalance(); - String color_effect = camera_controller.getColorEffect(); - if( scene_mode != null && !scene_mode.equals(CameraController.SCENE_MODE_DEFAULT) ) { - toast_string += "\n" + getResources().getString(R.string.scene_mode) + ": " + mainUI.getEntryForSceneMode(scene_mode); + if (preview.isQRCode()) { + toast_string = "QRCode"; + } else { //Camera || Video + if (applicationInterface.getFaceDetectionPref()) { + // important so that the user realises why touching for focus/metering areas won't work - easy to forget that face detection has been turned on! + toast_string += "\n" + getResources().getString(R.string.preference_face_detection); simple = false; } - if( white_balance != null && !white_balance.equals(CameraController.WHITE_BALANCE_DEFAULT) ) { - toast_string += "\n" + getResources().getString(R.string.white_balance) + ": " + mainUI.getEntryForWhiteBalance(white_balance); - if( white_balance.equals("manual") && preview.supportsWhiteBalanceTemperature() ) { - toast_string += " " + camera_controller.getWhiteBalanceTemperature(); + if (!video_high_speed) { + //manual ISO only supported for high speed video + String iso_value = applicationInterface.getISOPref(); + if (!iso_value.equals(CameraController.ISO_DEFAULT)) { + toast_string += "\nISO: " + iso_value; + if (preview.supportsExposureTime()) { + long exposure_time_value = applicationInterface.getExposureTimePref(); + toast_string += " " + preview.getExposureTimeString(exposure_time_value); + } + simple = false; + } + int current_exposure = camera_controller.getExposureCompensation(); + if (current_exposure != 0) { + toast_string += "\n" + preview.getExposureCompensationString(current_exposure); + simple = false; } - simple = false; } - if( color_effect != null && !color_effect.equals(CameraController.COLOR_EFFECT_DEFAULT) ) { - toast_string += "\n" + getResources().getString(R.string.color_effect) + ": " + mainUI.getEntryForColorEffect(color_effect); - simple = false; + try { + String scene_mode = camera_controller.getSceneMode(); + String white_balance = camera_controller.getWhiteBalance(); + String color_effect = camera_controller.getColorEffect(); + if (scene_mode != null && !scene_mode.equals(CameraController.SCENE_MODE_DEFAULT)) { + toast_string += "\n" + getResources().getString(R.string.scene_mode) + ": " + mainUI.getEntryForSceneMode(scene_mode); + simple = false; + } + if (white_balance != null && !white_balance.equals(CameraController.WHITE_BALANCE_DEFAULT)) { + toast_string += "\n" + getResources().getString(R.string.white_balance) + ": " + mainUI.getEntryForWhiteBalance(white_balance); + if (white_balance.equals("manual") && preview.supportsWhiteBalanceTemperature()) { + toast_string += " " + camera_controller.getWhiteBalanceTemperature(); + } + simple = false; + } + if (color_effect != null && !color_effect.equals(CameraController.COLOR_EFFECT_DEFAULT)) { + toast_string += "\n" + getResources().getString(R.string.color_effect) + ": " + mainUI.getEntryForColorEffect(color_effect); + simple = false; + } + } catch (RuntimeException e) { + // catch runtime error from camera_controller old API from camera.getParameters() + e.printStackTrace(); } - } - catch(RuntimeException e) { - // catch runtime error from camera_controller old API from camera.getParameters() - e.printStackTrace(); - } - String lock_orientation = applicationInterface.getLockOrientationPref(); - if( !lock_orientation.equals("none") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) { - // panorama locks to portrait, but don't want to display that in the toast - String [] entries_array = getResources().getStringArray(R.array.preference_lock_orientation_entries); - String [] values_array = getResources().getStringArray(R.array.preference_lock_orientation_values); - int index = Arrays.asList(values_array).indexOf(lock_orientation); - if( index != -1 ) { // just in case! - String entry = entries_array[index]; - toast_string += "\n" + entry; - simple = false; + String lock_orientation = applicationInterface.getLockOrientationPref(); + if (!lock_orientation.equals("none") && photo_mode != MyApplicationInterface.PhotoMode.Panorama) { + // panorama locks to portrait, but don't want to display that in the toast + String[] entries_array = getResources().getStringArray(R.array.preference_lock_orientation_entries); + String[] values_array = getResources().getStringArray(R.array.preference_lock_orientation_values); + int index = Arrays.asList(values_array).indexOf(lock_orientation); + if (index != -1) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + entry; + simple = false; + } } - } - String timer = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0"); - if( !timer.equals("0") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) { - String [] entries_array = getResources().getStringArray(R.array.preference_timer_entries); - String [] values_array = getResources().getStringArray(R.array.preference_timer_values); - int index = Arrays.asList(values_array).indexOf(timer); - if( index != -1 ) { // just in case! - String entry = entries_array[index]; - toast_string += "\n" + getResources().getString(R.string.preference_timer) + ": " + entry; - simple = false; + String timer = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0"); + if (!timer.equals("0") && photo_mode != MyApplicationInterface.PhotoMode.Panorama) { + String[] entries_array = getResources().getStringArray(R.array.preference_timer_entries); + String[] values_array = getResources().getStringArray(R.array.preference_timer_values); + int index = Arrays.asList(values_array).indexOf(timer); + if (index != -1) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + getResources().getString(R.string.preference_timer) + ": " + entry; + simple = false; + } } - } - String repeat = applicationInterface.getRepeatPref(); - if( !repeat.equals("1") ) { - String [] entries_array = getResources().getStringArray(R.array.preference_burst_mode_entries); - String [] values_array = getResources().getStringArray(R.array.preference_burst_mode_values); - int index = Arrays.asList(values_array).indexOf(repeat); - if( index != -1 ) { // just in case! - String entry = entries_array[index]; - toast_string += "\n" + getResources().getString(R.string.preference_burst_mode) + ": " + entry; - simple = false; + String repeat = applicationInterface.getRepeatPref(); + if (!repeat.equals("1")) { + String[] entries_array = getResources().getStringArray(R.array.preference_burst_mode_entries); + String[] values_array = getResources().getStringArray(R.array.preference_burst_mode_values); + int index = Arrays.asList(values_array).indexOf(repeat); + if (index != -1) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + getResources().getString(R.string.preference_burst_mode) + ": " + entry; + simple = false; + } } + /*if( audio_listener != null ) { + toast_string += "\n" + getResources().getString(R.string.preference_audio_noise_control); + }*/ } - /*if( audio_listener != null ) { - toast_string += "\n" + getResources().getString(R.string.preference_audio_noise_control); - }*/ if( MyDebug.LOG ) { Log.d(TAG, "toast_string: " + toast_string); diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/AddressBookParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/AddressBookParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..58bdeee365a8086611001f366508b577a2c6e9d3 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/AddressBookParsedResult.kt @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.ContactsContract +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.AddressBookParsedResult +import foundation.e.camera.R + +fun AddressBookParsedResult.createIntent() = Intent( + Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI +).apply { + names.firstOrNull()?.let { + putExtra(ContactsContract.Intents.Insert.NAME, it) + } + + pronunciation?.let { + putExtra(ContactsContract.Intents.Insert.PHONETIC_NAME, it) + } + + phoneNumbers?.let { phoneNumbers -> + val phoneTypes = phoneTypes ?: arrayOf() + + for ((key, keys) in listOf( + listOf( + ContactsContract.Intents.Insert.PHONE, + ContactsContract.Intents.Insert.PHONE_TYPE, + ), + listOf( + ContactsContract.Intents.Insert.SECONDARY_PHONE, + ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE, + ), + listOf( + ContactsContract.Intents.Insert.TERTIARY_PHONE, + ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE, + ), + ).withIndex()) { + phoneNumbers.getOrNull(key)?.let { phone -> + putExtra(keys.first(), phone) + phoneTypes.getOrNull(key)?.let { + putExtra(keys.last(), it) + } + } + } + } + + emails?.let { emails -> + val emailTypes = emailTypes ?: arrayOf() + + for ((key, keys) in listOf( + listOf( + ContactsContract.Intents.Insert.EMAIL, + ContactsContract.Intents.Insert.EMAIL_TYPE, + ), + listOf( + ContactsContract.Intents.Insert.SECONDARY_EMAIL, + ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE, + ), + listOf( + ContactsContract.Intents.Insert.TERTIARY_EMAIL, + ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE, + ), + ).withIndex()) { + emails.getOrNull(key)?.let { phone -> + putExtra(keys.first(), phone) + emailTypes.getOrNull(key)?.let { + putExtra(keys.last(), it) + } + } + } + } + + instantMessenger?.let { + putExtra(ContactsContract.Intents.Insert.IM_HANDLE, it) + } + + note?.let { + putExtra(ContactsContract.Intents.Insert.NOTES, it) + } + + addresses?.let { emails -> + val addressTypes = addressTypes ?: arrayOf() + + for ((key, keys) in listOf( + listOf( + ContactsContract.Intents.Insert.POSTAL, + ContactsContract.Intents.Insert.POSTAL_TYPE, + ), + ).withIndex()) { + emails.getOrNull(key)?.let { phone -> + putExtra(keys.first(), phone) + addressTypes.getOrNull(key)?.let { + putExtra(keys.last(), it) + } + } + } + } + + org?.let { + putExtra(ContactsContract.Intents.Insert.COMPANY, it) + } +} + +fun AddressBookParsedResult.createTextClassification( + context: Context +) = TextClassification.Builder() + .setText(title ?: names.firstOrNull() ?: "") + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_contact_phone, + R.string.qr_address_title, + R.string.qr_address_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/CalendarParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/CalendarParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..09a45490c8874954a0eab97ff6c738b20bce82bc --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/CalendarParsedResult.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.CalendarContract +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import androidx.core.os.bundleOf +import com.google.zxing.client.result.CalendarParsedResult +import foundation.e.camera.R + +fun CalendarParsedResult.createIntent() = Intent( + Intent.ACTION_INSERT, CalendarContract.Events.CONTENT_URI +).apply { + summary?.let { + putExtra(CalendarContract.Events.TITLE, it) + } + description?.let { + putExtra(CalendarContract.Events.DESCRIPTION, it) + } + location?.let { + putExtra(CalendarContract.Events.EVENT_LOCATION, it) + } + organizer?.let { + putExtra(CalendarContract.Events.ORGANIZER, it) + } + attendees?.let { + putExtra(Intent.EXTRA_EMAIL, it.joinToString(",")) + } + + putExtras( + bundleOf( + CalendarContract.EXTRA_EVENT_BEGIN_TIME to startTimestamp, + CalendarContract.EXTRA_EVENT_END_TIME to endTimestamp, + CalendarContract.EXTRA_EVENT_ALL_DAY to (isStartAllDay && isEndAllDay), + ) + ) +} + +fun CalendarParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(summary) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_calendar_add_on, + R.string.qr_calendar_title, + R.string.qr_calendar_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/Context.kt b/app/src/main/java/net/sourceforge/opencamera/ext/Context.kt new file mode 100644 index 0000000000000000000000000000000000000000..4026fb52ee304448cbbbdf029b665cd6bde33690 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/Context.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.content.Context +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt + +@ColorInt +fun Context.getThemeColor(@AttrRes attribute: Int) = TypedValue().let { + theme.resolveAttribute(attribute, it, true) + it.data +} diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/EmailAddressParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/EmailAddressParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f89a63d0aff27366eae0deab73249a7d3ba60ab --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/EmailAddressParsedResult.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import androidx.core.os.bundleOf +import com.google.zxing.client.result.EmailAddressParsedResult +import foundation.e.camera.R + +fun EmailAddressParsedResult.createIntent() = Intent( + Intent.ACTION_SENDTO, + Uri.parse("mailto:${tos?.firstOrNull() ?: ""}") +).apply { + putExtras( + bundleOf( + Intent.EXTRA_EMAIL to tos, + Intent.EXTRA_CC to cCs, + Intent.EXTRA_BCC to bcCs, + Intent.EXTRA_SUBJECT to subject, + Intent.EXTRA_TEXT to body, + ) + ) +} + +fun EmailAddressParsedResult.createTextClassification( + context: Context +) = TextClassification.Builder() + .setText(tos.joinToString()) + .setEntityType(TextClassifier.TYPE_EMAIL, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_email, + R.string.qr_email_title, + R.string.qr_email_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/GeoParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/GeoParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c7fb4597e544ee58f8e61ea81fe0ab6dd06a518 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/GeoParsedResult.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.GeoParsedResult +import foundation.e.camera.R + +fun GeoParsedResult.createIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(geoURI)) + +fun GeoParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(displayResult) + .setEntityType(TextClassifier.TYPE_ADDRESS, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_location_on, + R.string.qr_geo_title, + R.string.qr_geo_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/ISBNParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/ISBNParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..f71f031e37ba6de43f9ce0cd22f29a9040d56a34 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/ISBNParsedResult.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.ISBNParsedResult +import foundation.e.camera.R + +fun ISBNParsedResult.createIntent() = Intent( + Intent.ACTION_VIEW, Uri.parse("https://isbnsearch.org/isbn/${isbn}") +) + +fun ISBNParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(isbn) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_book, + R.string.qr_isbn_title, + R.string.qr_isbn_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/ImageProxy.kt b/app/src/main/java/net/sourceforge/opencamera/ext/ImageProxy.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b0d19456afd94def412d71280151712a7775823 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/ImageProxy.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2022 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import androidx.camera.core.ImageProxy +import com.google.zxing.PlanarYUVLuminanceSource + +private fun rotateYUVLuminancePlane(data: ByteArray, width: Int, height: Int): ByteArray { + val yuv = ByteArray(width * height) + // Rotate the Y luma + var i = 0 + for (x in 0 until width) { + for (y in height - 1 downTo 0) { + yuv[i] = data[y * width + x] + i++ + } + } + return yuv +} + +internal val ImageProxy.planarYUVLuminanceSource: PlanarYUVLuminanceSource + get() { + val plane = planes[0] + val buffer = plane.buffer + var bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + + var width = width + var height = height + + if (imageInfo.rotationDegrees == 90 || imageInfo.rotationDegrees == 270) { + bytes = rotateYUVLuminancePlane(bytes, width, height) + width = height.also { height = width } + } + + return PlanarYUVLuminanceSource( + bytes, width, height, 0, 0, width, height, true + ) + } diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/Int.kt b/app/src/main/java/net/sourceforge/opencamera/ext/Int.kt new file mode 100644 index 0000000000000000000000000000000000000000..e3e8d83ff0a2df53e9291e54075e90107e68a8ba --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/Int.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2022 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.content.res.Resources.getSystem +import android.util.Range +import kotlin.math.roundToInt + +val Int.px + get() = (this * getSystem().displayMetrics.density).roundToInt() + +val Int.dp + get() = (this / getSystem().displayMetrics.density).roundToInt() + +internal fun Int.Companion.mapToRange(range: Range, percentage: Float): Int { + return (((range.upper - range.lower) * percentage) + range.lower).roundToInt() +} diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/ParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/ParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..a35f89e48e0da9e75ef4af3275a889f855451c6c --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/ParsedResult.kt @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.content.Context +import com.google.zxing.client.result.AddressBookParsedResult +import com.google.zxing.client.result.CalendarParsedResult +import com.google.zxing.client.result.EmailAddressParsedResult +import com.google.zxing.client.result.GeoParsedResult +import com.google.zxing.client.result.ISBNParsedResult +import com.google.zxing.client.result.ParsedResult +import com.google.zxing.client.result.ProductParsedResult +import com.google.zxing.client.result.SMSParsedResult +import com.google.zxing.client.result.TelParsedResult +import com.google.zxing.client.result.TextParsedResult +import com.google.zxing.client.result.URIParsedResult +import com.google.zxing.client.result.VINParsedResult +import com.google.zxing.client.result.WifiParsedResult + +fun ParsedResult.createTextClassification(context: Context) = when (this) { + is AddressBookParsedResult -> createTextClassification(context) + + is CalendarParsedResult -> createTextClassification(context) + + is EmailAddressParsedResult -> createTextClassification(context) + + is GeoParsedResult -> createTextClassification(context) + + is ISBNParsedResult -> createTextClassification(context) + + is ProductParsedResult -> createTextClassification(context) + + is SMSParsedResult -> createTextClassification(context) + + is TelParsedResult -> createTextClassification(context) + + is TextParsedResult -> null // Try with the next methods + + is URIParsedResult -> null // We handle this manually + + is VINParsedResult -> createTextClassification(context) + + is WifiParsedResult -> createTextClassification(context) + + else -> null +} + diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/ProductParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/ProductParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..43177e0cfb0edeef0ca5fb13d7df9cbbc1530e4e --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/ProductParsedResult.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.ProductParsedResult +import foundation.e.camera.R + +fun ProductParsedResult.createIntent() = Intent( + Intent.ACTION_VIEW, Uri.parse("https://www.barcodelookup.com/${productID}") +) + +fun ProductParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(productID) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_shopping_cart, + R.string.qr_product_title, + R.string.qr_product_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/RemoteAction.kt b/app/src/main/java/net/sourceforge/opencamera/ext/RemoteAction.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc510110112ae93a994e22ee5b95e1915ad36a28 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/RemoteAction.kt @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.PendingIntent +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.graphics.drawable.toBitmap +import kotlin.reflect.KClass + +fun KClass.build( + context: Context, + @DrawableRes iconRes: Int, + @StringRes titleRes: Int, + @StringRes contentDescriptionRes: Int, + intent: Intent, + requestCode: Int = 0, + flags: Int = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + @AttrRes iconTint: Int = com.google.android.material.R.attr.colorOnBackground, +) = RemoteAction( + Icon.createWithBitmap( + AppCompatResources.getDrawable(context, iconRes)?.let { + DrawableCompat.wrap(it.mutate()).apply { + DrawableCompat.setTint( + this, + context.getThemeColor(iconTint) + ) + } + }?.toBitmap() + ), + context.getString(titleRes), + context.getString(contentDescriptionRes), + PendingIntent.getActivity(context, requestCode, intent, flags) +) diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/SMSParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/SMSParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..d12aea8f255cc29572d209804cf1a07004694681 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/SMSParsedResult.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.SMSParsedResult +import foundation.e.camera.R + +fun SMSParsedResult.createIntent() = Intent(Intent.ACTION_SENDTO, Uri.parse(smsuri)) + +fun SMSParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(numbers.first()) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_sms, + R.string.qr_sms_title, + R.string.qr_sms_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/TelParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/TelParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..17d3c177bc1684c43e67c3254c582843505c42eb --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/TelParsedResult.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.TelParsedResult +import foundation.e.camera.R + +fun TelParsedResult.createIntent() = Intent(Intent.ACTION_SENDTO, Uri.parse(telURI)) + +fun TelParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(number) + .setEntityType(TextClassifier.TYPE_PHONE, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_phone, + R.string.qr_tel_title, + R.string.qr_tel_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/VINParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/VINParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b4ca3f0f2fa4b2e241888ceeb73d942a5768ef9 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/VINParsedResult.kt @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.client.result.VINParsedResult +import foundation.e.camera.R + +fun VINParsedResult.createIntent() = Intent( + Intent.ACTION_VIEW, Uri.parse("https://www.vindecoderz.com/EN/check-lookup/${vin}") +) + +fun VINParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(vin) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_directions_car, + R.string.qr_vin_title, + R.string.qr_vin_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/ext/WifiParsedResult.kt b/app/src/main/java/net/sourceforge/opencamera/ext/WifiParsedResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b00f92c10d70d946cbdc422f53fabfc8cb31d7b --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/ext/WifiParsedResult.kt @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.ext + + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.wifi.WifiNetworkSuggestion +import android.os.Build +import android.provider.Settings +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import androidx.annotation.RequiresApi +import com.google.zxing.client.result.WifiParsedResult +import foundation.e.camera.R + +@RequiresApi(Build.VERSION_CODES.R) +fun WifiParsedResult.createIntent() = Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply { + putExtra( + Settings.EXTRA_WIFI_NETWORK_LIST, + arrayListOf( + WifiNetworkSuggestion.Builder() + .setSsid(ssid) + .setIsHiddenSsid(isHidden) + .apply { + password?.let { + when (networkEncryption) { + "WPA" -> { + // Per specs, Wi-Fi QR codes are only used for + // WPA2 and WPA-Mixed networks, we can safely assume + // this networks supports WPA2 + setWpa2Passphrase(it) + } + + "SAE" -> { + setWpa3Passphrase(it) + } + } + } + } + .build() + ) + ) +} + +fun WifiParsedResult.createTextClassification(context: Context) = TextClassification.Builder() + .setText(ssid) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_network_wifi, + R.string.qr_wifi_title, + R.string.qr_wifi_content_description, + createIntent() + ) + ) + } + } + .build() diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index eb4e623b32030d2400f440687c12aee21d6e8cb7..e6f72256fdedf659e307315ea72249a135b49712 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -95,6 +95,14 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.RGBLuminanceSource; + import foundation.e.camera.R; /** This class was originally named due to encapsulating the camera preview, @@ -197,7 +205,17 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private AsyncTask open_camera_task; // background task used for opening camera private CloseCameraTask close_camera_task; // background task used for closing camera private boolean has_permissions = true; // whether we have permissions necessary to operate the camera (camera, storage); assume true until we've been denied one of them - private boolean is_video; + + public static enum FunctionalMode { + PHOTO, + VIDEO, + QRCODE + }; + private FunctionalMode functionalMode = FunctionalMode.PHOTO; + private boolean is_video() {return functionalMode == FunctionalMode.VIDEO;}; + private boolean is_qrcode() {return functionalMode == FunctionalMode.QRCODE;}; + private boolean is_photo() {return functionalMode == FunctionalMode.PHOTO;}; + private volatile MediaRecorder video_recorder; // must be volatile for test project reading the state private volatile boolean video_start_time_set; // must be volatile for test project reading the state private long video_start_time; // system time when the video recording was started, or last resumed if it was paused @@ -686,14 +704,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( MyDebug.LOG ) Log.d(TAG, "handleSingleTouch"); - if( !this.is_video && this.isTakingPhotoOrOnTimer() ) { + if( !this.is_video() && this.isTakingPhotoOrOnTimer() ) { // if video, okay to refocus when recording return true; } // note, we always try to force start the preview (in case is_preview_paused has become false) // except if recording video (firstly, the preview should be running; secondly, we don't want to reset the phase!) - if (!this.is_video) { + if (!this.is_video()) { startCameraPreview(); } cancelAutoFocus(); @@ -1095,6 +1113,38 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu @Override public void onSurfaceTextureUpdated(@NonNull SurfaceTexture arg0) { refreshPreviewBitmap(); + + if (isQRCode()) { + enablePreviewBitmap(); + TextureView textureView = (TextureView) this.cameraSurface; + Bitmap bitmap = textureView.getBitmap(preview_bitmap); + + if (bitmap!=null) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] pixels = new int[width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + + LuminanceSource source = new RGBLuminanceSource(width, height, pixels); + BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); + Reader reader = new MultiFormatReader(); + + try { + Result result = reader.decode(binaryBitmap); + String qrcodeContent = result.getText(); + MainActivity mActivity = (MainActivity) this.getContext(); + if (MyDebug.LOG) + Log.d(TAG, "Find QRCode qrcodeContent="+qrcodeContent ); + mActivity.qrImageAnalyzer.showQrDialog(result); + } catch (Exception e) { + // K1ZFP TODO Error 2 + } + } else { + // K1ZFP TODO Error 1 + } + } else { + disablePreviewBitmap(); + } } private void configureTransform() { @@ -1263,7 +1313,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } if (due_to_max_filesize || remaining_restart_video > 0) { - if (is_video) { + if (is_video()) { String toast = null; if (!due_to_max_filesize) toast = remaining_restart_video + " " + getContext().getResources().getString(R.string.repeats_to_go); @@ -1639,7 +1689,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu current_focus_index = -1; max_num_focus_areas = 0; applicationInterface.cameraInOperation(false, false); - if (is_video) + if (is_video()) applicationInterface.cameraInOperation(false, true); if (!this.has_surface) { if (MyDebug.LOG) { @@ -2025,7 +2075,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu saved_is_video = false; } // must switch video before setupCameraParameters(), and starting preview - if (saved_is_video != this.is_video) { + if (saved_is_video != this.is_video()) { if (MyDebug.LOG) Log.d(TAG, "switch video mode as not in correct mode"); this.switchVideo(true, false); @@ -2076,7 +2126,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu updateFlashForVideo(); if (take_photo) { - if (this.is_video) { + if (this.is_video()) { if (MyDebug.LOG) Log.d(TAG, "switch to video for take_photo widget"); this.switchVideo(true, true); @@ -2085,8 +2135,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // must be done after switching to video mode (so is_video is set correctly) if (MyDebug.LOG) - Log.d(TAG, "is_video?: " + is_video); - if (this.is_video) { + Log.d(TAG, "is_video?: " + is_video()); + if (this.is_video()) { CameraController.TonemapProfile tonemap_profile = CameraController.TonemapProfile.TONEMAPPROFILE_OFF; if (supports_tonemap_curve) { tonemap_profile = applicationInterface.getVideoTonemapProfile(); @@ -2105,7 +2155,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // Setup for high speed - must be done after setupCameraParameters() and switching to video mode, but before setPreviewSize() and startCameraPreview(). // In theory it shouldn't matter if we call setVideoHighSpeed(true) if is_video==false, as it should only have an effect // when recording video; but don't set high speed mode in photo mode just to be safe. - camera_controller.setVideoHighSpeed(is_video && video_high_speed); + camera_controller.setVideoHighSpeed(is_video() && video_high_speed); if (do_startup_focus && using_android_l && camera_controller.supportsAutoFocus()) { // need to switch flash off for autofocus - and for Android L, need to do this before starting preview (otherwise it won't work in time); for old camera API, need to do this after starting preview! @@ -2615,10 +2665,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu { if (MyDebug.LOG) { Log.d(TAG, "set up video stabilization"); - Log.d(TAG, "is_video?: " + is_video); + Log.d(TAG, "is_video?: " + is_video()); } if (this.supports_video_stabilization) { - boolean using_video_stabilization = is_video && applicationInterface.getVideoStabilizationPref(); + boolean using_video_stabilization = is_video() && applicationInterface.getVideoStabilizationPref(); if (MyDebug.LOG) Log.d(TAG, "using_video_stabilization?: " + using_video_stabilization); camera_controller.setVideoStabilization(using_video_stabilization); @@ -3107,7 +3157,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "video_high_speed?: " + video_high_speed); } - if (is_video && video_high_speed && supports_iso_range && is_manual_iso) { + if (is_video() && video_high_speed && supports_iso_range && is_manual_iso) { if (MyDebug.LOG) Log.d(TAG, "manual mode not supported for video_high_speed"); camera_controller.setManualISO(false, 0); @@ -3257,7 +3307,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } // first set picture size (for photo mode, must be done now so we can set the picture size from this; for video, doesn't really matter when we set it) CameraController.Size new_size; - if (this.is_video) { + if (this.is_video()) { // see comments for getOptimalVideoPictureSize() VideoProfile profile = getVideoProfile(); if (MyDebug.LOG) @@ -3770,8 +3820,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu String preview_size = applicationInterface.getPreviewSizePref(); // should always use wysiwig for video mode, otherwise we get incorrect aspect ratio shown when recording video (at least on Galaxy Nexus, e.g., at 640x480) // also not using wysiwyg mode with video caused corruption on Samsung cameras (tested with Samsung S3, Android 4.3, front camera, infinity focus) - if (preview_size.equals("preference_preview_size_wysiwyg") || this.is_video) { - if (this.is_video) { + if (preview_size.equals("preference_preview_size_wysiwyg") || this.is_video()) { + if (this.is_video()) { if (MyDebug.LOG) Log.d(TAG, "set preview aspect ratio from video size (wysiwyg)"); VideoProfile profile = getVideoProfile(); @@ -3828,7 +3878,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu final double ASPECT_TOLERANCE = 0.05; if (sizes == null) return null; - if (is_video && video_high_speed) { + if (is_video() && video_high_speed) { VideoProfile profile = getVideoProfile(); if (MyDebug.LOG) Log.d(TAG, "video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight); @@ -4648,7 +4698,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // (important not to return here however - still want to call // camera_controller.clearPreviewFpsRange() to clear a previously set fps) } - else if( this.is_video ) { + else if( this.is_video() ) { // For Nexus 5 and Nexus 6, we need to set the preview fps using matchPreviewFpsToVideo to avoid problem of dark preview in low light, as described above. // When the video recording starts, the preview automatically adjusts, but still good to avoid too-dark preview before the user starts recording. // However I'm wary of changing the behaviour for all devices at the moment, since some devices can be @@ -4714,33 +4764,45 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "camera not opened!"); return; } - if (!is_video && !supports_video) { + if (!is_video() && !supports_video) { if (MyDebug.LOG) Log.d(TAG, "video not supported"); return; } - boolean old_is_video = is_video; - if (this.is_video) { + boolean old_is_video = is_video(); + boolean old_is_qrcode = is_qrcode(); + if (this.is_video()) { if (video_recorder != null) { stopVideo(false); } - this.is_video = false; - } else { + this.functionalMode = FunctionalMode.QRCODE; + int qrcodeCamId = ((MainActivity)getContext()).getBetterQRCodeCameraID(); + if (qrcodeCamId >= 0) { + applicationInterface.setCameraIdPref(qrcodeCamId); + //userSwitchToCamera(qrcodeCamId, true); + } + } else if (this.is_qrcode()) { + this.functionalMode = FunctionalMode.PHOTO; + } else if (this.is_photo()) { if (this.isOnTimer()) { cancelTimer(); ((MainActivity)getContext()).setDecorFitsSystemWindows(true); - this.is_video = true; + this.functionalMode = FunctionalMode.VIDEO; } else if (this.phase == PHASE_TAKING_PHOTO) { // wait until photo taken if (MyDebug.LOG) Log.d(TAG, "wait until photo taken"); } else { ((MainActivity)getContext()).setDecorFitsSystemWindows(true); - this.is_video = true; + this.functionalMode = FunctionalMode.VIDEO; } } - if (is_video != old_is_video) { + if (is_qrcode() != old_is_qrcode) { + applicationInterface.setVideoPref(false); + this.reopenCamera(); + } + else if (is_video() != old_is_video) { setFocusPref(false); // first restore the saved focus for the new photo/video mode; don't do autofocus, as it'll be cancelled when restarting preview /*if( !is_video ) { // changing from video to photo mode @@ -4749,7 +4811,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if (change_user_pref) { // now save - applicationInterface.setVideoPref(is_video); + applicationInterface.setVideoPref(true); } if (!during_startup) { // if during startup, updateFlashForVideo() needs to always be explicitly called anyway @@ -4776,7 +4838,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // changing from photo to video mode setFocusPref(false); }*/ - if (is_video) { + if (is_video()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && applicationInterface.getRecordAudioPref()) { // check for audio permission now, rather than when user starts video recording // we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() @@ -4804,7 +4866,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private void setFocusPref(boolean auto_focus) { if (MyDebug.LOG) Log.d(TAG, "setFocusPref()"); - String focus_value = applicationInterface.getFocusPref(is_video); + String focus_value = applicationInterface.getFocusPref(is_video()); if (focus_value.length() > 0) { if (MyDebug.LOG) Log.d(TAG, "found existing focus_value: " + focus_value); @@ -4818,7 +4880,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "found no existing focus_value"); // here we set the default values for focus mode // note if updating default focus value for photo mode, also update MainActivityTest.setToDefault() - if( !updateFocus(is_video ? "focus_mode_continuous_video" : "focus_mode_continuous_picture", true, true, auto_focus) ) { + if( !updateFocus(is_video() ? "focus_mode_continuous_video" : "focus_mode_continuous_picture", true, true, auto_focus) ) { if( MyDebug.LOG ) Log.d(TAG, "continuous focus not supported, so fall back to first"); updateFocus(0, true, true, auto_focus); @@ -4839,12 +4901,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if (MyDebug.LOG) Log.d(TAG, "updateFocusForVideo()"); String old_focus_mode = null; - if (this.supported_focus_values != null && camera_controller != null && is_video) { + if (this.supported_focus_values != null && camera_controller != null && is_video()) { boolean focus_is_video = focusIsVideo(); if (MyDebug.LOG) { - Log.d(TAG, "focus_is_video: " + focus_is_video + " , is_video: " + is_video); + Log.d(TAG, "focus_is_video: " + focus_is_video + " , is_video: " + is_video()); } - if (focus_is_video != is_video) { + if (focus_is_video != is_video()) { if (MyDebug.LOG) Log.d(TAG, "need to change focus mode"); old_focus_mode = this.getCurrentFocusValue(); @@ -4862,7 +4924,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private void updateFlashForVideo() { if (MyDebug.LOG) Log.d(TAG, "updateFlashForVideo()"); - if (is_video) { + if (is_video()) { // check flash is not auto or on String current_flash = getCurrentFlashValue(); if (current_flash != null && !isFlashSupportedForVideo(current_flash)) { @@ -4926,7 +4988,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public void updateFlash(String flash_value) { if (MyDebug.LOG) Log.d(TAG, "updateFlash(): " + flash_value); - if (this.phase == PHASE_TAKING_PHOTO && !is_video) { + if (this.phase == PHASE_TAKING_PHOTO && !is_video()) { // just to be safe - risk of cancelling the autofocus before taking a photo, or otherwise messing things up if (MyDebug.LOG) Log.d(TAG, "currently taking a photo"); @@ -4967,7 +5029,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // don't bother setting done to false as we shouldn't have two torches in a row... } - if (is_video) { + if (is_video()) { // check supported for video String new_flash_value = supported_flash_values.get(new_flash_index); if (!isFlashSupportedForVideo(new_flash_value)) { @@ -5137,7 +5199,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if (save) { // now save - applicationInterface.setFocusPref(focus_value, is_video); + applicationInterface.setFocusPref(focus_value, is_video()); } } } @@ -5187,7 +5249,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu String focus_value = current_focus_index != -1 ? supported_focus_values.get(current_focus_index) : null; if (MyDebug.LOG) Log.d(TAG, "focus_value is " + focus_value); - if (camera_controller != null && focus_value != null && focus_value.equals("focus_mode_continuous_picture") && !this.is_video) { + if (camera_controller != null && focus_value != null && focus_value.equals("focus_mode_continuous_picture") && !this.is_video()) { if (MyDebug.LOG) Log.d(TAG, "set continuous picture focus move callback"); camera_controller.setContinuousFocusMoveCallback(new CameraController.ContinuousFocusMoveCallback() { @@ -5272,7 +5334,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.phase = PHASE_NORMAL; return; } - if (is_video && continuous_fast_burst) { + if (is_video() && continuous_fast_burst) { Log.e(TAG, "continuous_fast_burst not supported for video mode"); this.phase = PHASE_NORMAL; return; @@ -5284,7 +5346,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } //if( !photo_snapshot && this.phase == PHASE_TAKING_PHOTO ) { //if( (is_video && is_video_recording && !photo_snapshot) || this.phase == PHASE_TAKING_PHOTO ) { - if (is_video && isVideoRecording() && !photo_snapshot) { + if (is_video() && isVideoRecording() && !photo_snapshot) { // user requested stop video if (!video_start_time_set || System.currentTimeMillis() - video_start_time < 500) { // if user presses to stop too quickly, we ignore @@ -5296,7 +5358,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu stopVideo(false); } return; - } else if ((!is_video || photo_snapshot) && this.phase == PHASE_TAKING_PHOTO) { + } else if ((!is_video() || photo_snapshot) && this.phase == PHASE_TAKING_PHOTO) { // user requested take photo while already taking photo if (MyDebug.LOG) Log.d(TAG, "already taking a photo"); @@ -5304,14 +5366,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu cancelRepeat(); showToast(take_photo_toast, R.string.cancelled_repeat_mode, true); } - else if( !is_video && camera_controller.getBurstType() == CameraController.BurstType.BURSTTYPE_FOCUS && camera_controller.isCapturingBurst() ) { + else if( !is_video() && camera_controller.getBurstType() == CameraController.BurstType.BURSTTYPE_FOCUS && camera_controller.isCapturingBurst() ) { camera_controller.stopFocusBracketingBurst(); showToast(take_photo_toast, R.string.cancelled_focus_bracketing, true); } return; } - if (!is_video || photo_snapshot) { + if (!is_video() || photo_snapshot) { // check it's okay to take a photo if (!applicationInterface.canTakeNewPhoto()) { if (MyDebug.LOG) @@ -5584,7 +5646,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if (MyDebug.LOG) Log.d(TAG, "takePicture"); //this.thumbnail_anim = false; - if (!is_video || photo_snapshot) + if (!is_video() || photo_snapshot) this.phase = PHASE_TAKING_PHOTO; else { if (phase == PHASE_TIMER) @@ -5599,7 +5661,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "camera not opened!"); this.phase = PHASE_NORMAL; applicationInterface.cameraInOperation(false, false); - if (is_video) + if (is_video()) applicationInterface.cameraInOperation(false, true); return; } @@ -5608,7 +5670,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "preview surface not yet available"); this.phase = PHASE_NORMAL; applicationInterface.cameraInOperation(false, false); - if (is_video) + if (is_video()) applicationInterface.cameraInOperation(false, true); return; } @@ -5623,17 +5685,17 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if (MyDebug.LOG) Log.d(TAG, "location data required, but not available"); showToast(null, R.string.location_not_available, true); - if( !is_video || photo_snapshot ) + if( !is_video() || photo_snapshot ) this.phase = PHASE_NORMAL; applicationInterface.cameraInOperation(false, false); - if (is_video) + if (is_video()) applicationInterface.cameraInOperation(false, true); return; } } } - if (is_video && !photo_snapshot) { + if (is_video() && !photo_snapshot) { if (MyDebug.LOG) Log.d(TAG, "start video recording"); startVideoRecording(max_filesize_restart); @@ -6465,7 +6527,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu camera_controller.setRotation(getImageVideoRotation()); boolean enable_sound = applicationInterface.getShutterSoundPref(); - if (is_video && isVideoRecording()) + if (is_video() && isVideoRecording()) enable_sound = false; // always disable shutter sound if we're taking a photo while recording video if (MyDebug.LOG) Log.d(TAG, "enable_sound? " + enable_sound); @@ -6564,7 +6626,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } else if (!this.is_preview_started) { if (MyDebug.LOG) Log.d(TAG, "preview not yet started"); - } else if (!(manual && this.is_video) && (this.isVideoRecording() || this.isTakingPhotoOrOnTimer())) { + } else if (!(manual && this.is_video()) && (this.isVideoRecording() || this.isTakingPhotoOrOnTimer())) { // if taking a video, we allow manual autofocuses // autofocus may cause problem if there is a video corruption problem, see testTakeVideoBitrate() on Nexus 7 at 30Mbs or 50Mbs, where the startup autofocus would cause a problem here if (MyDebug.LOG) @@ -6574,7 +6636,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // remove any previous request to switch back to continuous removePendingContinuousFocusReset(); } - if (manual && !is_video && camera_controller.focusIsContinuous() && supportedFocusValue("focus_mode_auto")) { + if (manual && !is_video() && camera_controller.focusIsContinuous() && supportedFocusValue("focus_mode_auto")) { if (MyDebug.LOG) Log.d(TAG, "switch from continuous to autofocus mode for touch focus"); camera_controller.setFocusValue("focus_mode_auto"); // switch to autofocus @@ -6755,8 +6817,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "starting the camera preview"); { if (MyDebug.LOG) - Log.d(TAG, "setRecordingHint: " + is_video); - camera_controller.setRecordingHint(this.is_video); + Log.d(TAG, "setRecordingHint: " + is_video()); + camera_controller.setRecordingHint(this.is_video()); } setPreviewFps(); try { @@ -7057,7 +7119,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public boolean isVideoHighSpeed() { if (MyDebug.LOG) Log.d(TAG, "isVideoHighSpeed"); - return is_video && video_high_speed; + return is_video() && video_high_speed; } public boolean canDisableShutterSound() { @@ -8583,8 +8645,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } + public boolean isQRCode() { + return functionalMode == FunctionalMode.QRCODE; + } public boolean isVideo() { - return is_video; + return functionalMode == FunctionalMode.VIDEO; + } + public boolean isPhoto() { + return functionalMode == FunctionalMode.PHOTO; } public boolean isVideoRecording() { diff --git a/app/src/main/java/net/sourceforge/opencamera/qr/QrImageAnalyzer.kt b/app/src/main/java/net/sourceforge/opencamera/qr/QrImageAnalyzer.kt new file mode 100644 index 0000000000000000000000000000000000000000..47273a370941e705bc7dba91840ac53cbe73a5a2 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/qr/QrImageAnalyzer.kt @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.qr + +import android.app.Activity +import android.app.KeyguardManager +import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Build +import android.text.method.LinkMovementMethod +import android.view.textclassifier.TextClassificationManager +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.LinearLayoutCompat.LayoutParams +import androidx.cardview.widget.CardView +import androidx.core.graphics.drawable.DrawableCompat +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.button.MaterialButton +import com.google.zxing.MultiFormatReader +import com.google.zxing.Result +import foundation.e.camera.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.sourceforge.opencamera.ext.getThemeColor +import net.sourceforge.opencamera.ext.px +import kotlin.reflect.cast + +class QrImageAnalyzer(private val activity: Activity, private val scope: CoroutineScope) { + + // Views + private val bottomSheetDialog by lazy { + BottomSheetDialog(activity).apply { + setContentView(R.layout.qr_bottom_sheet_dialog) + } + } + + private val bottomSheetDialogCardView by lazy { + bottomSheetDialog.findViewById(R.id.cardView)!! + } + private val bottomSheetDialogTitle by lazy { + bottomSheetDialog.findViewById(R.id.title)!! + } + private val bottomSheetDialogData by lazy { + bottomSheetDialog.findViewById(R.id.data)!! + } + private val bottomSheetDialogIcon by lazy { + bottomSheetDialog.findViewById(R.id.icon)!! + } + private val bottomSheetDialogCopy by lazy { + bottomSheetDialog.findViewById(R.id.copy)!! + } + private val bottomSheetDialogShare by lazy { + bottomSheetDialog.findViewById(R.id.share)!! + } + private val bottomSheetDialogActionsLayout by lazy { + bottomSheetDialog.findViewById(R.id.actionsLayout)!! + } + + // System services + private val clipboardManager by lazy { activity.getSystemService(ClipboardManager::class.java) } + private val keyguardManager by lazy { activity.getSystemService(KeyguardManager::class.java) } + private val textClassificationManager by lazy { + activity.getSystemService(TextClassificationManager::class.java) + } + + // QR + private val reader by lazy { MultiFormatReader() } + + private val qrTextClassifier by lazy { + QrTextClassifier(activity, textClassificationManager.textClassifier) + } + + public fun showQrDialog(result: Result) { + scope.launch(Dispatchers.Main) { + if (bottomSheetDialog.isShowing) { + return@launch + } + + val text = result.text ?: return@launch + bottomSheetDialogData.text = text + + // Classify message + val textClassification = withContext(Dispatchers.IO) { + qrTextClassifier.classifyText(result) + } + + bottomSheetDialogData.text = textClassification.text + bottomSheetDialogActionsLayout.removeAllViews() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + textClassification.actions.isNotEmpty() + ) { + with(textClassification.actions[0]) { + bottomSheetDialogCardView.setOnClickListener { + try { + actionIntent.send() + } catch (e: PendingIntent.CanceledException) { + Toast.makeText( + activity, + R.string.qr_no_app_available_for_action, + Toast.LENGTH_SHORT + ).show() + } + } + bottomSheetDialogCardView.contentDescription = contentDescription + bottomSheetDialogData.movementMethod = null + bottomSheetDialogTitle.text = title + bottomSheetDialogIcon.setImageIcon(icon) + } + for (action in textClassification.actions.drop(1)) { + bottomSheetDialogActionsLayout.addView(inflateButton().apply { + setOnClickListener { + try { + action.actionIntent.send() + } catch (e: PendingIntent.CanceledException) { + Toast.makeText( + activity, + R.string.qr_no_app_available_for_action, + Toast.LENGTH_SHORT + ).show() + } + } + contentDescription = action.contentDescription + this.text = action.title + withContext(Dispatchers.IO) { + val drawable = action.icon.loadDrawable(activity)!! + drawable.setBounds(0, 0, 15.px, 15.px) + withContext(Dispatchers.Main) { + setCompoundDrawables( + drawable, null, null, null + ) + } + } + }) + } + } else { + bottomSheetDialogCardView.setOnClickListener {} + bottomSheetDialogTitle.text = activity.resources.getText(R.string.qr_text) + bottomSheetDialogIcon.setImageDrawable(AppCompatResources.getDrawable( + activity, R.drawable.ic_text_snippet + )?.let { + DrawableCompat.wrap(it.mutate()).apply { + DrawableCompat.setTint( + this, activity.getThemeColor( + com.google.android.material.R.attr.colorOnBackground + ) + ) + } + }) + } + + // Make links clickable if not on locked keyguard + bottomSheetDialogData.movementMethod = + if (!keyguardManager.isKeyguardLocked) LinkMovementMethod.getInstance() + else null + + // Set buttons + bottomSheetDialogCopy.setOnClickListener { + clipboardManager.setPrimaryClip( + ClipData.newPlainText( + "", text + ) + ) + } + + bottomSheetDialogShare.setOnClickListener { + activity.startActivity( + Intent.createChooser( + Intent().apply { + action = Intent.ACTION_SEND + type = ClipDescription.MIMETYPE_TEXT_PLAIN + putExtra( + Intent.EXTRA_TEXT, result.text + ) + }, + activity.getString(R.string.abc_shareactionprovider_share_with) + ) + ) + } + + // Show dialog + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + + bottomSheetDialog.show() + + bottomSheetDialog.setOnDismissListener { + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + + } + } + + private fun inflateButton() = MaterialButton::class.cast( + activity.layoutInflater.inflate( + R.layout.qr_bottom_sheet_action_button, + bottomSheetDialogActionsLayout, + false + ) + ).apply { + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } + +} diff --git a/app/src/main/java/net/sourceforge/opencamera/qr/QrTextClassifier.kt b/app/src/main/java/net/sourceforge/opencamera/qr/QrTextClassifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..89fc85169bfeaf92be14a2df4e524c4bb3fdc0e2 --- /dev/null +++ b/app/src/main/java/net/sourceforge/opencamera/qr/QrTextClassifier.kt @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.sourceforge.opencamera.qr + +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.net.wifi.WifiManager +import android.os.Build +import android.os.LocaleList +import android.provider.Settings +import android.text.SpannableString +import android.view.textclassifier.TextClassification +import android.view.textclassifier.TextClassifier +import com.google.zxing.Result +import com.google.zxing.client.result.ResultParser +import com.google.zxing.client.result.URIParsedResult +import foundation.e.camera.R +import net.sourceforge.opencamera.ext.build +import net.sourceforge.opencamera.ext.createTextClassification +import kotlin.reflect.safeCast + +class QrTextClassifier( + private val context: Context, private val textClassifier: TextClassifier +) { + private val wifiManager by lazy { + runCatching { context.getSystemService(WifiManager::class.java) }.getOrNull() + } + + fun classifyText(result: Result): TextClassification { + // Try with ZXing parser + val parsedResult = ResultParser.parseResult(result) + parsedResult?.createTextClassification(context)?.let { + return it + } + + // We handle URIParsedResult here + val text = URIParsedResult::class.safeCast(parsedResult)?.uri ?: result.text + + // Try parsing it as a Uri + Uri.parse(text.toString()).let { uri -> + when (uri.scheme?.lowercase()) { + // Wi-Fi DPP + SCHEME_DPP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + wifiManager?.isEasyConnectSupported == true + ) { + return TextClassification.Builder() + .setText(context.getString(R.string.qr_dpp_description)) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .addAction( + RemoteAction::class.build( + context, + R.drawable.ic_network_wifi, + R.string.qr_dpp_title, + R.string.qr_dpp_description, + Intent(Settings.ACTION_PROCESS_WIFI_EASY_CONNECT_URI).apply { + data = uri + } + ) + ) + .build() + } + + SCHEME_FIDO -> return TextClassification.Builder() + .setText(context.getString(R.string.qr_fido_content_description)) + .setEntityType(TextClassifier.TYPE_OTHER, 1.0f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAction( + RemoteAction::class.build( + context, + R.drawable.ic_passkey, + R.string.qr_fido_title, + R.string.qr_fido_content_description, + Intent(Intent.ACTION_VIEW).apply { + data = uri + } + ) + ) + } + } + .build() + } + } + + // Let Android classify it + val spannableString = SpannableString(text) + return textClassifier.classifyText( + spannableString, 0, spannableString.length, LocaleList.getDefault() + ) + } + + companion object { + private const val SCHEME_DPP = "dpp" + private const val SCHEME_FIDO = "fido" + } +} diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java index 44fd83b21eda4699ef5e69dffd72f1a2cca5bfd3..929ce21a9940571ff435e588851969f524a5fe55 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -1217,6 +1217,7 @@ public class MainUI { int resource; int content_description; int switch_video_content_description; + // The switch order is camera -> video -> qrcode -> camera... if (main_activity.getPreview().isVideo()) { if (MyDebug.LOG) Log.d(TAG, "set icon to video " + main_activity.getPreview().isVideoRecording()); @@ -1224,23 +1225,48 @@ public class MainUI { ? R.drawable.ic_camera_video_recording : R.drawable.ic_camera_video; content_description = main_activity.getPreview().isVideoRecording() ? R.string.stop_video : R.string.start_video; + switch_video_content_description = R.string.switch_to_qrcode; + } else if (main_activity.getPreview().isQRCode()) { + if (MyDebug.LOG) + Log.d(TAG, "set icon to qrcode"); + resource = R.drawable.empty; + content_description = 0; switch_video_content_description = R.string.switch_to_photo; - } else { + } else { // Camera case if (MyDebug.LOG) Log.d(TAG, "set icon to photo"); resource = R.drawable.take_photo_selector; content_description = R.string.take_photo; switch_video_content_description = R.string.switch_to_video; } + view.setImageResource(resource); - view.setContentDescription(main_activity.getResources().getString(content_description)); + if (content_description==0) + view.setContentDescription(""); + else + view.setContentDescription(main_activity.getResources().getString(content_description)); view.setTag(resource); // for testing view = main_activity.findViewById(R.id.switch_video); view.setContentDescription(main_activity.getResources().getString(switch_video_content_description)); - resource = main_activity.getPreview().isVideo() ? R.drawable.ic_switch_camera : R.drawable.ic_switch_video; + + if (main_activity.getPreview().isVideo()) + resource = R.drawable.ic_switch_qrcode; + else if (main_activity.getPreview().isQRCode()) + resource = R.drawable.ic_switch_camera; + else // camera + resource = R.drawable.ic_switch_video; view.setImageResource(resource); view.setTag(resource); // for testing + + // Hide/Show gallery & switch camera icons. + if (main_activity.getPreview().isQRCode()) { + main_activity.findViewById(R.id.gallery).setVisibility(View.INVISIBLE); + main_activity.findViewById(R.id.switch_camera).setVisibility(View.INVISIBLE); + } else { + main_activity.findViewById(R.id.gallery).setVisibility(View.VISIBLE); + main_activity.findViewById(R.id.switch_camera).setVisibility(View.VISIBLE); + } } } @@ -2506,6 +2532,7 @@ public class MainUI { } public void setPopupIcon() { + if (MyDebug.LOG) Log.d(TAG, "setPopupIcon"); ImageButton popup = main_activity.findViewById(R.id.popup); diff --git a/app/src/main/res/drawable/empty.xml b/app/src/main/res/drawable/empty.xml new file mode 100644 index 0000000000000000000000000000000000000000..c886b6ada8d7c4b15850d3df87fcfedd5aa2f271 --- /dev/null +++ b/app/src/main/res/drawable/empty.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/ic_book.xml b/app/src/main/res/drawable/ic_book.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c6d45296cdc1d2197e7ac888b4de1437c10b84d --- /dev/null +++ b/app/src/main/res/drawable/ic_book.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_calendar_add_on.xml b/app/src/main/res/drawable/ic_calendar_add_on.xml new file mode 100644 index 0000000000000000000000000000000000000000..d4cfef3e2749a069dfd986c897fe7d2bc4a6d4e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_add_on.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_contact_phone.xml b/app/src/main/res/drawable/ic_contact_phone.xml new file mode 100644 index 0000000000000000000000000000000000000000..19d5ef77f62666e5c58354ff2ca5a31216bc2afd --- /dev/null +++ b/app/src/main/res/drawable/ic_contact_phone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 0000000000000000000000000000000000000000..66112fee750cdac9134d5ef3a642f50f1ec9c4f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_directions_car.xml b/app/src/main/res/drawable/ic_directions_car.xml new file mode 100644 index 0000000000000000000000000000000000000000..b39f5334369ba2f1f1e5f6b87fad8fdbeb2e33c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_car.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 0000000000000000000000000000000000000000..602c9d3bcaa6863984307da9cc89a308342c8999 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml new file mode 100644 index 0000000000000000000000000000000000000000..63a9bd89a2121f3451f91fd0f4f269764cb195fd --- /dev/null +++ b/app/src/main/res/drawable/ic_email.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_location_on.xml b/app/src/main/res/drawable/ic_location_on.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1db4bdbf86a4f80b8d9c76b59965aa0b36b1543 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_on.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_network_wifi.xml b/app/src/main/res/drawable/ic_network_wifi.xml new file mode 100644 index 0000000000000000000000000000000000000000..6db18e1a059ed70e5b8294346463e36171b8d077 --- /dev/null +++ b/app/src/main/res/drawable/ic_network_wifi.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_passkey.xml b/app/src/main/res/drawable/ic_passkey.xml new file mode 100644 index 0000000000000000000000000000000000000000..b3c3dbb54104f7b745562ce97c93564f3a5b81df --- /dev/null +++ b/app/src/main/res/drawable/ic_passkey.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 0000000000000000000000000000000000000000..ae7ecad9c7d03eb84259f7dc7a712df2d65809d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml new file mode 100644 index 0000000000000000000000000000000000000000..3cd15b2b032a40f7c3d3fb57721c8ab8501bf619 --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_sms.xml b/app/src/main/res/drawable/ic_sms.xml new file mode 100644 index 0000000000000000000000000000000000000000..b20d6f027ce45308417dc293e93737ade9f075b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_sms.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_switch_qrcode.xml b/app/src/main/res/drawable/ic_switch_qrcode.xml new file mode 100644 index 0000000000000000000000000000000000000000..8b31106f9e36fda2f032cb5ec488c28b2c19a1bb --- /dev/null +++ b/app/src/main/res/drawable/ic_switch_qrcode.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_text_snippet.xml b/app/src/main/res/drawable/ic_text_snippet.xml new file mode 100644 index 0000000000000000000000000000000000000000..137e2893e4022ce6eb4abc461d370dae22faf993 --- /dev/null +++ b/app/src/main/res/drawable/ic_text_snippet.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/qr_bottom_sheet_action_button_divider.xml b/app/src/main/res/drawable/qr_bottom_sheet_action_button_divider.xml new file mode 100644 index 0000000000000000000000000000000000000000..06402726f4c3bdce17ba4b2f33a620392cbae76d --- /dev/null +++ b/app/src/main/res/drawable/qr_bottom_sheet_action_button_divider.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/layout/qr_bottom_sheet_action_button.xml b/app/src/main/res/layout/qr_bottom_sheet_action_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..6154163e10d0237aeb4bc7343f77a0588aacf37b --- /dev/null +++ b/app/src/main/res/layout/qr_bottom_sheet_action_button.xml @@ -0,0 +1,20 @@ + + + diff --git a/app/src/main/res/layout/qr_bottom_sheet_dialog.xml b/app/src/main/res/layout/qr_bottom_sheet_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0b49148969237cffa306a40b5fb8ac510e0a1b7 --- /dev/null +++ b/app/src/main/res/layout/qr_bottom_sheet_dialog.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a48df3018ede45ecab31a4d80991932fe29724df..4b806de319a3d6140db108ea3791616ef8bb834f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -932,4 +932,35 @@ X-Bty Extension : Bokeh X-Bokeh + QR Code + Ajouter un contact + Ajouter un contact + Ajouter des évènements au calendrier + Ajouter cet évènement au calendrier + Ouvrir cette localisation + Ouvrir cette localisation + Envoyer un nouveau couriel + Composer un nouveau courriel pour les courriels spécifiés + Regarder cet ISBN + Recherchez cet ISBN sur isbnsearch.org + Rechercher un produit + Consulter le code-barres de l\'ID de ce produit + Envoyer un nouveau SMS + Envoyer un nouveau SMS aux destinataires spécifiés + Appeler le numéro de téléphone + Appeler le numéro de téléphone + Rechercher VIN + Recherche du numéro d\'identification du véhicule (VIN) + Se connecter à ce réseau Wi-Fi + Ajouter ce réseau Wi-Fi à la liste des réseaux connus et y connecter l\'appareil + Icône + Partager + Copier vers le presse papier + "Pas d'application disponible pour prendre en compte cette action " + Texte + Configurer cet appareil + Wi-Fi Easy Connect™ (DPP) + Manipuler ce code QR FIDO + Utiliser le mot de passe + Partager avec \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 013b4e094a98058379b2e381b90d24e779c6c36d..761c80c436952c7f5c36f174d2ad80b9e86538df 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,4 +17,12 @@ @color/e_disabled_color_light @color/e_disabled_color_dark #E5000000 + + + #FFFFFF + #E5000000 + #8BC34A + #3F51B5 + #CB000000 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec96043cd3e21ba901b4b6812642557e65fef1dd..a9737d854dc47fec6f95a067294e45ed47623354 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1101,4 +1101,35 @@ Skip LENS + QRCode + Add contact + Add contact + Add event to calendar + Add this event to the calendar + Open this location + Open this location + Send a new email + Compose a new email to the specified emails + Lookup this ISBN + Search this ISBN on isbnsearch.org + Lookup product + Lookup this product ID barcode + Send a new SMS + Send a new SMS to the specified recipients + Call phone number + Call the phone number + Lookup VIN + Lookup this Vehicle Identification Number (VIN) + Connect to this Wi-Fi network + Add this Wi-Fi network to the list of known networks and connect the device to it + Icon + Share + Copy to clipboard + No app available to handle this action + Text + Configure this device + Wi-Fi Easy Connect™ (DPP) + Handle this FIDO QR code + Use passkey + Share with