From 04ef1634af27c72fb66cb63d5d4d15065db94fa6 Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 9 Aug 2023 19:45:00 +0200 Subject: [PATCH] [LEIP-130] Call Digural API on image capture (#63) * Refactor background process to take in some kind of hook class for custom behaviour. * Integrate updated camera servic into Cyface app and include code to trigger digural * Add trigger call to DiGuRaL API * Add UI Field to set DiGuRaL server address * [LEIP-137] Fix digural url field in settings fragment * Dont inject digural address statically * Upgrade camera service to 4.2.0 * Cleanup and documentation * Fix build --------- Co-authored-by: Klemens Muthmann --- .github/workflows/gradle_build.yml | 4 +- build.gradle | 4 +- camera_service | 2 +- ui/cyface/build.gradle | 1 + ui/cyface/src/main/AndroidManifest.xml | 1 + .../app/ui/button/DataCapturingButton.java | 26 +++- .../de/cyface/app/CameraServiceProvider.kt | 2 +- .../kotlin/de/cyface/app/CapturingFragment.kt | 2 +- .../main/kotlin/de/cyface/app/MainActivity.kt | 15 +- .../cyface/app/button/UnInterestedListener.kt | 37 +++++ .../app/notification/CameraEventHandler.kt | 139 ++---------------- ui/digural/build.gradle | 14 +- ui/digural/src/main/AndroidManifest.xml | 1 + .../ui/button/DataCapturingButton.java | 18 ++- .../app/digural/CameraServiceProvider.kt | 2 +- .../cyface/app/digural/CapturingFragment.kt | 2 +- .../de/cyface/app/digural/MainActivity.kt | 10 +- .../button/ExternalCameraController.kt | 88 +++++++++++ .../app/digural/capturing/DiGuRaLApi.kt | 45 ++++++ .../capturing/settings/CustomPreferences.kt | 55 +++++++ .../settings/DiguralUrlChangeHandler.kt | 65 ++++++++ .../capturing/settings/SettingsFragment.kt | 18 ++- .../capturing/settings/SettingsViewModel.kt | 19 ++- .../settings/SettingsViewModelFactory.kt | 5 +- .../notification/CameraEventHandler.kt | 133 ++--------------- .../main/res/drawable/baseline_save_24.xml | 5 + .../src/main/res/layout/fragment_settings.xml | 30 +++- ui/digural/src/main/res/values-de/strings.xml | 2 + ui/digural/src/main/res/values-it/strings.xml | 2 + ui/digural/src/main/res/values/strings.xml | 2 + ui/r4r/src/main/AndroidManifest.xml | 1 + utils/build.gradle | 6 + 32 files changed, 470 insertions(+), 286 deletions(-) create mode 100644 ui/cyface/src/main/kotlin/de/cyface/app/button/UnInterestedListener.kt create mode 100644 ui/digural/src/main/kotlin/de/cyface/app/digural/button/ExternalCameraController.kt create mode 100644 ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/DiGuRaLApi.kt create mode 100644 ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/CustomPreferences.kt create mode 100644 ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/DiguralUrlChangeHandler.kt create mode 100644 ui/digural/src/main/res/drawable/baseline_save_24.xml diff --git a/.github/workflows/gradle_build.yml b/.github/workflows/gradle_build.yml index 8d7d86b7..444582a5 100644 --- a/.github/workflows/gradle_build.yml +++ b/.github/workflows/gradle_build.yml @@ -59,6 +59,6 @@ jobs: # Executing build here on Ubuntu stack (1/10th costs of MacOS stack) # Not using "gradle build" as we don't want to run the tests of all dependencies (e.g. backend) - name: Assemble with Gradle - run: ./gradlew :ui:cyface:assembleDebug :ui:r4r:assembleDebug + run: ./gradlew :ui:cyface:assembleDebug :ui:r4r:assembleDebug :ui:digural:assembleDebug - name: Test with Gradle - run: ./gradlew :ui:cyface:testDebugUnitTest :ui:r4r:testDebugUnitTest + run: ./gradlew :ui:cyface:testDebugUnitTest :ui:r4r:testDebugUnitTest :ui:digural:testDebugUnitTest diff --git a/build.gradle b/build.gradle index 1360c988..b0f6af26 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { cyfaceAndroidBackendVersion = "7.8.1" // Also update submodule commit ref cyfaceUtilsVersion = "3.5.0" cyfaceEnergySettingsVersion = "3.3.4" // Also update submodule commit ref - cyfaceCameraServiceVersion = "4.1.12" // Also update submodule commit ref + cyfaceCameraServiceVersion = "4.2.0" // Also update submodule commit ref // Maybe keep this in sync with the serialization library version used in `uploader` lib cyfaceSerializationVersion = "2.3.7" // Keep im sync with version in submodule `backend` cyfaceUploaderVersion = "1.0.0" @@ -88,6 +88,8 @@ ext { chartVersion = "v3.1.0" // Can't be a SDK dependency as `appAuthRedirectScheme` needs to be defined at build-time appAuthVersion = "0.11.1" // Can't move to uploader lib because of dependency issues + retrofitVersion = "2.9.0" // Used by digural UI + // Testing junitVersion = "1.1.5" diff --git a/camera_service b/camera_service index d02b953b..f4baf12a 160000 --- a/camera_service +++ b/camera_service @@ -1 +1 @@ -Subproject commit d02b953b8b2290235aa4493305d223be09f9b6ab +Subproject commit f4baf12ab2f2f7b5826ef69520f40a77c410f71a diff --git a/ui/cyface/build.gradle b/ui/cyface/build.gradle index 1fa2b748..d65875b2 100644 --- a/ui/cyface/build.gradle +++ b/ui/cyface/build.gradle @@ -29,6 +29,7 @@ apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'io.sentry.android.gradle' // Exception tracking when using proguard apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'androidx.navigation.safeargs.kotlin' // recommended to navigate between fragments +apply plugin: 'kotlin-parcelize' buildscript { repositories { diff --git a/ui/cyface/src/main/AndroidManifest.xml b/ui/cyface/src/main/AndroidManifest.xml index 06ace4fc..1c60dd32 100644 --- a/ui/cyface/src/main/AndroidManifest.xml +++ b/ui/cyface/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22" /> + diff --git a/ui/cyface/src/main/java/de/cyface/app/ui/button/DataCapturingButton.java b/ui/cyface/src/main/java/de/cyface/app/ui/button/DataCapturingButton.java index d41fb913..7f637739 100644 --- a/ui/cyface/src/main/java/de/cyface/app/ui/button/DataCapturingButton.java +++ b/ui/cyface/src/main/java/de/cyface/app/ui/button/DataCapturingButton.java @@ -61,11 +61,12 @@ import de.cyface.app.R; import de.cyface.app.button.AbstractButton; import de.cyface.app.button.ButtonListener; +import de.cyface.app.notification.CameraEventHandler; import de.cyface.app.utils.CalibrationDialogListener; import de.cyface.app.utils.Map; -import de.cyface.camera_service.CameraListener; +import de.cyface.camera_service.background.camera.CameraListener; import de.cyface.camera_service.CameraPreferences; -import de.cyface.camera_service.CameraService; +import de.cyface.camera_service.foreground.CameraService; import de.cyface.camera_service.Constants; import de.cyface.camera_service.UIListener; import de.cyface.datacapturing.CyfaceDataCapturingService; @@ -93,6 +94,10 @@ import de.cyface.utils.Validate; import io.sentry.Sentry; +// TODO: This class has overstretched its intended scope by several orders of magnitude by now. +// The initial idea was to have this contain all the UI code for the button triggering data +// capturing and providing a ViewModel as soon as too much business logic is in here. This is the +// case now. All this stuff should be moved to a or multiple business logic classes soon. /** * The button listener for the button to start and stop the data capturing service. * @@ -699,15 +704,24 @@ private void startCameraService(final long measurementId) final var staticExposureTime = cameraPreferences.getStaticExposureTime(); final var exposureValueIso100 = cameraPreferences.getStaticExposureValue(); - cameraService.start(measurementId, videoModeSelected, rawModeSelected, staticFocusSelected, - staticFocusDistance, staticExposureTimeSelected, staticExposureTime, exposureValueIso100, - distanceBasedTriggeringSelected, triggeringDistance, + cameraService.start( + measurementId, + videoModeSelected, + rawModeSelected, + staticFocusSelected, + staticFocusDistance, + staticExposureTimeSelected, + staticExposureTime, + exposureValueIso100, + distanceBasedTriggeringSelected, + triggeringDistance, new StartUpFinishedHandler(de.cyface.camera_service.MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED) { @Override public void startUpFinished(final long measurementIdentifier) { Log.v(Constants.TAG, "startCameraService: CameraService startUpFinished"); } - }); + } + ); } /** diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/CameraServiceProvider.kt b/ui/cyface/src/main/kotlin/de/cyface/app/CameraServiceProvider.kt index bc4e463e..e89917cb 100644 --- a/ui/cyface/src/main/kotlin/de/cyface/app/CameraServiceProvider.kt +++ b/ui/cyface/src/main/kotlin/de/cyface/app/CameraServiceProvider.kt @@ -18,7 +18,7 @@ */ package de.cyface.app -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.foreground.CameraService /** * Interface which defines the dependencies implemented by the `MainActivity` to be accessible from diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/CapturingFragment.kt b/ui/cyface/src/main/kotlin/de/cyface/app/CapturingFragment.kt index 30cea6af..08404236 100644 --- a/ui/cyface/src/main/kotlin/de/cyface/app/CapturingFragment.kt +++ b/ui/cyface/src/main/kotlin/de/cyface/app/CapturingFragment.kt @@ -39,7 +39,7 @@ import de.cyface.app.dialog.ModalityDialog import de.cyface.app.ui.button.DataCapturingButton import de.cyface.app.utils.Map import de.cyface.app.utils.ServiceProvider -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.foreground.CameraService import de.cyface.datacapturing.CyfaceDataCapturingService import de.cyface.datacapturing.persistence.CapturingPersistenceBehaviour import de.cyface.persistence.DefaultPersistenceLayer diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/MainActivity.kt b/ui/cyface/src/main/kotlin/de/cyface/app/MainActivity.kt index 3d553c9f..12a294e6 100644 --- a/ui/cyface/src/main/kotlin/de/cyface/app/MainActivity.kt +++ b/ui/cyface/src/main/kotlin/de/cyface/app/MainActivity.kt @@ -27,11 +27,9 @@ import android.accounts.OperationCanceledException import android.app.Activity import android.content.Intent import android.content.pm.PackageManager -import android.location.Location import android.os.Bundle import android.os.Handler import android.os.Message -import android.os.Parcel import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -45,6 +43,7 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import de.cyface.app.auth.LoginActivity +import de.cyface.app.button.UnInterestedListener import de.cyface.app.databinding.ActivityMainBinding import de.cyface.app.notification.CameraEventHandler import de.cyface.app.notification.DataCapturingEventHandler @@ -52,9 +51,9 @@ import de.cyface.app.utils.Constants import de.cyface.app.utils.Constants.ACCOUNT_TYPE import de.cyface.app.utils.Constants.AUTHORITY import de.cyface.app.utils.ServiceProvider -import de.cyface.camera_service.CameraListener import de.cyface.camera_service.CameraPreferences -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.background.camera.CameraListener +import de.cyface.camera_service.foreground.CameraService import de.cyface.datacapturing.CyfaceDataCapturingService import de.cyface.datacapturing.DataCapturingListener import de.cyface.datacapturing.exception.SetupException @@ -148,7 +147,8 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider override fun onCapturingStopped() {} } - private val unInterestedCameraListener: CameraListener = object : CameraListener { + private val unInterestedCameraListener: CameraListener = object : + CameraListener { override fun onNewPictureAcquired(picturesCaptured: Int) {} override fun onNewVideoStarted() {} override fun onVideoStopped() {} @@ -211,7 +211,8 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider cameraService = CameraService( this.applicationContext, CameraEventHandler(), - unInterestedCameraListener // here was the capturing button but it registers itself, too + unInterestedCameraListener, // here was the capturing button but it registers itself, too + UnInterestedListener() ) } catch (e: SetupException) { throw IllegalStateException(e) @@ -541,4 +542,4 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider return existingAccounts.isNotEmpty() } } -} \ No newline at end of file +} diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/button/UnInterestedListener.kt b/ui/cyface/src/main/kotlin/de/cyface/app/button/UnInterestedListener.kt new file mode 100644 index 00000000..840700a3 --- /dev/null +++ b/ui/cyface/src/main/kotlin/de/cyface/app/button/UnInterestedListener.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface App for Android. + * + * The Cyface App for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface App for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface App for Android. If not, see . + */ +package de.cyface.app.button + +import android.content.Context +import android.location.Location +import de.cyface.camera_service.background.ParcelableCapturingProcessListener +import kotlinx.parcelize.Parcelize + +@Parcelize +class UnInterestedListener : ParcelableCapturingProcessListener { + + override fun contextBasedInitialization(context: Context) {} + override fun onCameraAccessLost() {} + override fun onPictureCaptured() {} + override fun onRecordingStarted() {} + override fun onRecordingStopped() {} + override fun onCameraError(reason: String) {} + override fun onAboutToCapture(measurementId: Long, location: Location?) {} + override fun shallStop() {} +} \ No newline at end of file diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/notification/CameraEventHandler.kt b/ui/cyface/src/main/kotlin/de/cyface/app/notification/CameraEventHandler.kt index 25331617..99d6e59c 100644 --- a/ui/cyface/src/main/kotlin/de/cyface/app/notification/CameraEventHandler.kt +++ b/ui/cyface/src/main/kotlin/de/cyface/app/notification/CameraEventHandler.kt @@ -27,90 +27,39 @@ import android.content.Intent import android.graphics.Color import android.media.RingtoneManager import android.os.Build -import android.os.Parcel -import android.os.Parcelable.Creator -import android.util.Log import androidx.core.app.NotificationCompat import de.cyface.app.MainActivity import de.cyface.app.R -import de.cyface.app.utils.Constants.TAG import de.cyface.app.utils.SharedConstants.CAMERA_ACCESS_LOST_NOTIFICATION_ID import de.cyface.app.utils.SharedConstants.NOTIFICATION_CHANNEL_ID_RUNNING import de.cyface.app.utils.SharedConstants.NOTIFICATION_CHANNEL_ID_WARNING import de.cyface.app.utils.SharedConstants.PICTURE_CAPTURING_DECREASED_NOTIFICATION_ID -import de.cyface.app.utils.SharedConstants.SPACE_WARNING_NOTIFICATION_ID -import de.cyface.camera_service.BackgroundService -import de.cyface.camera_service.EventHandlingStrategy +import de.cyface.camera_service.background.BackgroundService +import de.cyface.camera_service.foreground.NotificationStrategy import de.cyface.utils.Validate +import kotlinx.parcelize.Parcelize /** - * A [EventHandlingStrategy] to respond to specified events triggered by the + * A [NotificationStrategy] to respond to specified events triggered by the * [BackgroundService]. * * @author Armin Schnabel * @version 1.2.2 * @since 1.0.0 */ -class CameraEventHandler : EventHandlingStrategy { - constructor() { - // Nothing to do here. - } +@Parcelize +class CameraEventHandler : + NotificationStrategy { /** - * Constructor as required by `Parcelable` implementation. - * - * @param in A `Parcel` that is a serialized version of a [CameraEventHandler]. - */ - @Suppress("UNUSED_PARAMETER") - private constructor(`in`: Parcel) { - // Nothing to do here. - } - - override fun handleSpaceWarning(backgroundService: BackgroundService) { - showSpaceWarningNotification(backgroundService.applicationContext) - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleSpaceWarning() - CS capturing stopped.") - } - - override fun handleCameraAccessLostWarning(backgroundService: BackgroundService) { - showCameraAccessLostNotification(backgroundService.applicationContext) - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleCameraAccessLostWarning() triggered - CS capturing stopped.") - } - - override fun handleCameraErrorWarning( - backgroundService: BackgroundService, - reason: String - ) { - showCameraErrorNotification(backgroundService.applicationContext, reason) - - // The CameraStateHandle throws a hard exception for play store statistics but - // we try to stop this service here gracefully anyway - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleCameraErrorWarning() triggered - CS capturing stopped.") - } - - override fun handlePictureCapturingDecrease(backgroundService: BackgroundService) { - Log.i(TAG, "handlePictureCapturingDecrease() triggered. Showing notification.") - showPictureCapturingDecreasedNotification(backgroundService.applicationContext) - } - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) {} - - /** - * A [Notification] shown when the [BackgroundService] triggered the low space event. + * A [Notification] shown when the [BackgroundService] triggered the 'camera error' event. * * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showSpaceWarningNotification(context: Context) { + override fun showCameraErrorNotification(context: Context, reason: String) { + + // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) val onClickPendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity( @@ -135,54 +84,6 @@ class CameraEventHandler : EventHandlingStrategy { NotificationManager.IMPORTANCE_HIGH, true, Color.RED, true ) } - // TODO: see if we not create two of those warnings (DCS and CS) - val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder( - context, - NOTIFICATION_CHANNEL_ID_WARNING - ).setContentIntent(onClickPendingIntent) - .setSmallIcon(R.drawable.ic_logo_only_c) - .setContentTitle(context.getString(de.cyface.app.utils.R.string.notification_title_capturing_stopped)) - .setContentText(context.getString(de.cyface.app.utils.R.string.error_message_capturing_canceled_no_space)) - .setOngoing(false).setWhen(System.currentTimeMillis()).setPriority(2) - .setAutoCancel(true) - .setVibrate(longArrayOf(500, 1500)) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)) - notificationManager.notify(SPACE_WARNING_NOTIFICATION_ID, notificationBuilder.build()) - } - - /** - * A [Notification] shown when the [BackgroundService] triggered the 'camera error' event. - * - * @param context The context if the service used to show the [Notification]. It stays even - * when the service is stopped as long as a unique id is used. - */ - private fun showCameraErrorNotification(context: Context, reason: String) { - - // Open Activity when the notification is clicked - val onClickIntent = Intent(context, MainActivity::class.java) - val onClickPendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getActivity( - context, 0, onClickIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } else { - // Ignore warning: immutable flag only available in API >= 23, see above - PendingIntent.getActivity( - context, 0, onClickIntent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - val notificationManager = context - .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - Validate.notNull(notificationManager) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - createNotificationChannelIfNotExists( - context, NOTIFICATION_CHANNEL_ID_WARNING, - context.getString(de.cyface.app.utils.R.string.notification_channel_name_warning), - context.getString(de.cyface.app.utils.R.string.notification_channel_description_warning), - NotificationManager.IMPORTANCE_HIGH, true, Color.RED, true - ) - } val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder( context, NOTIFICATION_CHANNEL_ID_WARNING @@ -203,7 +104,7 @@ class CameraEventHandler : EventHandlingStrategy { * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showCameraAccessLostNotification(context: Context) { + override fun showCameraAccessLostNotification(context: Context) { // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) @@ -253,7 +154,7 @@ class CameraEventHandler : EventHandlingStrategy { * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showPictureCapturingDecreasedNotification(context: Context) { + override fun showPictureCapturingDecreasedNotification(context: Context) { // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) @@ -345,20 +246,6 @@ class CameraEventHandler : EventHandlingStrategy { } companion object { - /** - * The `Parcelable` creator as required by the Android Parcelable specification. - */ - @Suppress("unused") - @JvmField - val CREATOR: Creator = object : Creator { - override fun createFromParcel(`in`: Parcel): CameraEventHandler { - return CameraEventHandler(`in`) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } /** * Since Android 8 it is necessary to create a new notification channel for a foreground service notification. To diff --git a/ui/digural/build.gradle b/ui/digural/build.gradle index 790d76b6..90d344fa 100644 --- a/ui/digural/build.gradle +++ b/ui/digural/build.gradle @@ -29,6 +29,7 @@ apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'io.sentry.android.gradle' // Exception tracking when using proguard apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'androidx.navigation.safeargs.kotlin' // recommended to navigate between fragments +apply plugin: 'kotlin-parcelize' // Generate Parcelable in Kotlin from Annotation buildscript { repositories { @@ -114,7 +115,7 @@ android { buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.staging_oauth_discovery')}\"" buildConfigField "String", "testLogin", "\"${project.findProperty('cyface.staging_user')}\"" buildConfigField "String", "testPassword", "\"${project.findProperty('cyface.staging_password')}\"" - manifestPlaceholders = [usesCleartextTraffic:"false"] + manifestPlaceholders = [usesCleartextTraffic:"true"] // external Digural camera API uses http // MOCK-API - only supports login - used by UI test on CI //buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.demo_api')}\"" @@ -132,7 +133,7 @@ android { buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.api')}\"" buildConfigField "String", "incentivesServer", "\"${project.findProperty('cyface.incentives_api')}\"" buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.oauth_discovery')}\"" - manifestPlaceholders = [usesCleartextTraffic:"false"] + manifestPlaceholders = [usesCleartextTraffic:"true"] // external Digural camera API uses http } } @@ -229,6 +230,15 @@ dependencies { implementation project(':energy_settings') implementation project(':camera_service') implementation project(':utils') + // Retrofit + implementation "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofitVersion" + // Retrofit with Scalar Converter + implementation "com.squareup.retrofit2:converter-gson:$rootProject.ext.retrofitVersion" + implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.ext.okHttpVersion" + // Coroutines + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android' + // Dependencies for instrumentation tests // Resolve conflicts between main and test APK (which is used in the integration-test module): diff --git a/ui/digural/src/main/AndroidManifest.xml b/ui/digural/src/main/AndroidManifest.xml index 3949d917..fc4fbc49 100644 --- a/ui/digural/src/main/AndroidManifest.xml +++ b/ui/digural/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22" /> + diff --git a/ui/digural/src/main/java/de/cyface/app/digural/ui/button/DataCapturingButton.java b/ui/digural/src/main/java/de/cyface/app/digural/ui/button/DataCapturingButton.java index 42229eb5..4d80740f 100644 --- a/ui/digural/src/main/java/de/cyface/app/digural/ui/button/DataCapturingButton.java +++ b/ui/digural/src/main/java/de/cyface/app/digural/ui/button/DataCapturingButton.java @@ -63,11 +63,11 @@ import de.cyface.app.digural.button.ButtonListener; import de.cyface.app.utils.CalibrationDialogListener; import de.cyface.app.utils.Map; -import de.cyface.camera_service.CameraListener; import de.cyface.camera_service.CameraPreferences; -import de.cyface.camera_service.CameraService; import de.cyface.camera_service.Constants; import de.cyface.camera_service.UIListener; +import de.cyface.camera_service.background.camera.CameraListener; +import de.cyface.camera_service.foreground.CameraService; import de.cyface.datacapturing.CyfaceDataCapturingService; import de.cyface.datacapturing.DataCapturingListener; import de.cyface.datacapturing.DataCapturingService; @@ -688,9 +688,17 @@ private void startCameraService(final long measurementId) final var staticExposureTime = cameraPreferences.getStaticExposureTime(); final var exposureValueIso100 = cameraPreferences.getStaticExposureValue(); - cameraService.start(measurementId, videoModeSelected, rawModeSelected, staticFocusSelected, - staticFocusDistance, staticExposureTimeSelected, staticExposureTime, exposureValueIso100, - distanceBasedTriggeringSelected, triggeringDistance, + cameraService.start( + measurementId, + videoModeSelected, + rawModeSelected, + staticFocusSelected, + staticFocusDistance, + staticExposureTimeSelected, + staticExposureTime, + exposureValueIso100, + distanceBasedTriggeringSelected, + triggeringDistance, new StartUpFinishedHandler(de.cyface.camera_service.MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED) { @Override public void startUpFinished(final long measurementIdentifier) { diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/CameraServiceProvider.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/CameraServiceProvider.kt index b64d0a7f..d44d3fce 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/CameraServiceProvider.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/CameraServiceProvider.kt @@ -18,7 +18,7 @@ */ package de.cyface.app.digural -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.foreground.CameraService /** * Interface which defines the dependencies implemented by the `MainActivity` to be accessible from diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/CapturingFragment.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/CapturingFragment.kt index d6c9072d..94dbb9b8 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/CapturingFragment.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/CapturingFragment.kt @@ -39,7 +39,7 @@ import de.cyface.app.digural.dialog.ModalityDialog import de.cyface.app.digural.ui.button.DataCapturingButton import de.cyface.app.utils.Map import de.cyface.app.utils.ServiceProvider -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.foreground.CameraService import de.cyface.datacapturing.CyfaceDataCapturingService import de.cyface.datacapturing.persistence.CapturingPersistenceBehaviour import de.cyface.persistence.DefaultPersistenceLayer diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/MainActivity.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/MainActivity.kt index 0e40cea1..dadf3ed5 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/MainActivity.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/MainActivity.kt @@ -43,6 +43,7 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import de.cyface.app.digural.auth.LoginActivity +import de.cyface.app.digural.button.ExternalCameraController import de.cyface.app.digural.databinding.ActivityMainBinding import de.cyface.app.digural.notification.CameraEventHandler import de.cyface.app.digural.notification.DataCapturingEventHandler @@ -50,9 +51,9 @@ import de.cyface.app.digural.utils.Constants import de.cyface.app.digural.utils.Constants.ACCOUNT_TYPE import de.cyface.app.digural.utils.Constants.AUTHORITY import de.cyface.app.utils.ServiceProvider -import de.cyface.camera_service.CameraListener import de.cyface.camera_service.CameraPreferences -import de.cyface.camera_service.CameraService +import de.cyface.camera_service.background.camera.CameraListener +import de.cyface.camera_service.foreground.CameraService import de.cyface.datacapturing.CyfaceDataCapturingService import de.cyface.datacapturing.DataCapturingListener import de.cyface.datacapturing.exception.SetupException @@ -146,7 +147,8 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider override fun onCapturingStopped() {} } - private val unInterestedCameraListener: CameraListener = object : CameraListener { + private val unInterestedCameraListener: CameraListener = object : + CameraListener { override fun onNewPictureAcquired(picturesCaptured: Int) {} override fun onNewVideoStarted() {} override fun onVideoStopped() {} @@ -201,6 +203,7 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider unInterestedListener, // here was the capturing button but it registers itself, too preferences.getSensorFrequency() ) + val deviceIdentifier = capturing.persistenceLayer.restoreOrCreateDeviceId() // Needs to be called after new CyfaceDataCapturingService() for the SDK to check and throw // a specific exception when the LOGIN_ACTIVITY was not set from the SDK using app. // startSynchronization() // This is done in onAuthorized() instead @@ -209,6 +212,7 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider this.applicationContext, CameraEventHandler(), unInterestedCameraListener, // here was the capturing button but it registers itself, too + ExternalCameraController(deviceIdentifier) ) } catch (e: SetupException) { throw IllegalStateException(e) diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/button/ExternalCameraController.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/button/ExternalCameraController.kt new file mode 100644 index 00000000..faeeb901 --- /dev/null +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/button/ExternalCameraController.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface App for Android. + * + * The Cyface App for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface App for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface App for Android. If not, see . + */ +package de.cyface.app.digural.button + +import android.content.Context +import android.location.Location +import android.util.Log +import de.cyface.app.digural.MainActivity.Companion.TAG +import de.cyface.app.digural.capturing.DiguralApi +import de.cyface.app.digural.capturing.settings.CustomPreferences +import de.cyface.camera_service.background.ParcelableCapturingProcessListener +import de.cyface.utils.Validate +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize + +/** + * Calls the API that triggers external cameras to trigger in sync with this smartphones camera. + * + * @author Klemens Muthmann + * @since 4.2.0 + * @constructor Create a new controller from the world wide unique device identifier of this device. + */ +@Parcelize +class ExternalCameraController(private val deviceId: String) : ParcelableCapturingProcessListener { + init { + Validate.notEmpty(deviceId) + } + + override fun contextBasedInitialization(context: Context) { + // FIXME: Known issue: After starting the capturing once, further preference changes + // are not affecting the preferences received here, even when this is always executed after + // starting a new BackgroundService. The SharedPreferences don't support multi-process env. + // We cannot inject the address preference in `CameraService` as this is a ui-specific + // preferences which the camera_service has no knowledge of. + // A possible solution would be to store this in Room/Sql, as this supports multi-process. + val address = CustomPreferences(context).getDiguralUrl() + DiguralApi.baseUrl = address + Log.d(TAG, "###########Setting digural address to: $address") + } + + override fun onCameraAccessLost() {} + override fun onPictureCaptured() {} + override fun onRecordingStarted() {} + override fun onRecordingStopped() {} + override fun onCameraError(reason: String) {} + override fun onAboutToCapture(measurementId: Long, location: Location?) { + if(location == null) { + return + } + + val payload = de.cyface.app.digural.capturing.Location( + deviceId, + measurementId, + location.latitude, + location.longitude, + location.time + ) + + runBlocking { + withContext(Dispatchers.IO) { + Log.d(TAG, "###########Sending Payload $payload to ${DiguralApi.baseUrl}") + DiguralApi.diguralService.trigger(payload) + } + } + } + + override fun shallStop() { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/DiGuRaLApi.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/DiGuRaLApi.kt new file mode 100644 index 00000000..b127ec9a --- /dev/null +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/DiGuRaLApi.kt @@ -0,0 +1,45 @@ +package de.cyface.app.digural.capturing + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST +import java.net.URL + +interface DiguralApiService { + @POST("Trigger") + suspend fun trigger(@Body location: Location) +} + +object DiguralApi { + + lateinit var baseUrl: URL + + private val retrofit: Retrofit.Builder + get() { + val logging = HttpLoggingInterceptor() + logging.setLevel(HttpLoggingInterceptor.Level.BASIC) + + val httpClient = OkHttpClient.Builder() + httpClient.addInterceptor(logging) + + return Retrofit.Builder() + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient.build()) + } + + val diguralService: DiguralApiService by lazy { + retrofit.baseUrl(baseUrl.toURI().resolve("./PanAiCam").toURL()) + retrofit.build().create(DiguralApiService::class.java) + } +} + +data class Location( + val deviceId: String, + val measurementId: Long, + val latitude: Double, + val longitude: Double, + val time: Long + ) \ No newline at end of file diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/CustomPreferences.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/CustomPreferences.kt new file mode 100644 index 00000000..0ebcbc71 --- /dev/null +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/CustomPreferences.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface App for Android. + * + * The Cyface App for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface App for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface App for Android. If not, see . + */ +package de.cyface.app.digural.capturing.settings + +import android.content.Context +import androidx.core.content.edit +import de.cyface.utils.AppPreferences +import java.net.URL + +/** + * This class is responsible for storing and retrieving preferences specific to the DiGuRaL project. + */ +class CustomPreferences(context: Context) : AppPreferences(context) { + + fun saveDiguralUrl(address: URL) { + preferences.edit { + putString(DIGURAL_SERVER_URL_SETTINGS_KEY, address.toExternalForm()) + apply() + } + } + + fun getDiguralUrl(): URL { + val addressString = preferences.getString( + DIGURAL_SERVER_URL_SETTINGS_KEY, + DEFAULT_URL + ) + return URL(addressString) + } + + companion object { + /** + * The settings key used to identify the settings storing the URL of the server to share + * camera trigger events with, e.g. to trigger an external camera. + */ + const val DIGURAL_SERVER_URL_SETTINGS_KEY = "de.cyface.digural.server" + + private const val DEFAULT_URL = "http://localhost:33552/" + } +} \ No newline at end of file diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/DiguralUrlChangeHandler.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/DiguralUrlChangeHandler.kt new file mode 100644 index 00000000..afa120ad --- /dev/null +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/DiguralUrlChangeHandler.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface App for Android. + * + * The Cyface App for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface App for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface App for Android. If not, see . + */ +package de.cyface.app.digural.capturing.settings + +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import com.google.android.material.textfield.TextInputEditText +import de.cyface.app.digural.R +import java.net.MalformedURLException +import java.net.URL + +/** + * Handles when the user changes the Digural Server Url. + * + * @author Armin Schnabel + * @version 1.0.0 + * @since 3.2.0 + */ +class DiguralUrlChangeHandler( + private val viewModel: SettingsViewModel, + private val fragment: SettingsFragment, + private val valueHolder: TextInputEditText +) : View.OnClickListener { + + override fun onClick(v: View?) { + val previousValue = viewModel.diguralServerUrl.value + val newValue = valueHolder.text + val context = fragment.requireContext() + if (newValue != null && newValue.toString() != previousValue?.toExternalForm()) { + try { + viewModel.setDiguralServerUrl(URL(newValue.toString())) + } catch (e: MalformedURLException) { + Toast.makeText(context, R.string.url_malformed_toast, Toast.LENGTH_LONG) + .show() + viewModel.setDiguralServerUrl(previousValue!!) + } + } + + // Clear focus + valueHolder.clearFocus() + + // Hide soft keyboard + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v!!.windowToken, 0) + } + +} \ No newline at end of file diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsFragment.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsFragment.kt index 8853865d..1b0dc209 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsFragment.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsFragment.kt @@ -37,10 +37,10 @@ import androidx.lifecycle.ViewModelProvider import de.cyface.app.digural.databinding.FragmentSettingsBinding import de.cyface.app.digural.dialog.ExposureTimeDialog import de.cyface.app.utils.ServiceProvider -import de.cyface.camera_service.CameraModeDialog import de.cyface.camera_service.CameraPreferences import de.cyface.camera_service.Constants import de.cyface.camera_service.Utils +import de.cyface.camera_service.background.camera.CameraModeDialog import de.cyface.datacapturing.CyfaceDataCapturingService import de.cyface.utils.AppPreferences import de.cyface.utils.Validate @@ -112,9 +112,10 @@ class SettingsFragment : Fragment() { // Initialize ViewModel val appPreferences = AppPreferences(requireContext().applicationContext) val cameraPreferences = CameraPreferences(requireContext().applicationContext) + val customPreferences = CustomPreferences(requireContext().applicationContext) viewModel = ViewModelProvider( this, - SettingsViewModelFactory(appPreferences, cameraPreferences) + SettingsViewModelFactory(appPreferences, cameraPreferences, customPreferences) )[SettingsViewModel::class.java] // Initialize CapturingService @@ -159,6 +160,13 @@ class SettingsFragment : Fragment() { this ) ) + binding.diguralServerAddressWrapper.setEndIconOnClickListener( + DiguralUrlChangeHandler( + viewModel, + this, + binding.diguralServerAddress + ) + ) // Observe view model and update UI viewModel.centerMap.observe(viewLifecycleOwner) { centerMapValue -> @@ -177,6 +185,12 @@ class SettingsFragment : Fragment() { binding.sensorFrequency.text = sensorFrequencyValue.toString() } } + viewModel.diguralServerUrl.observe(viewLifecycleOwner) { serverAddress -> + run { + Log.d(TAG, "updateView -> digural server address: $serverAddress") + binding.diguralServerAddress.setText(serverAddress.toExternalForm()) + } + } viewModel.cameraEnabled.observe(viewLifecycleOwner) { cameraEnabledValue -> run { binding.cameraSwitch.isChecked = cameraEnabledValue!! diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModel.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModel.kt index 266d2155..0011fadf 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModel.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import de.cyface.camera_service.CameraPreferences import de.cyface.utils.AppPreferences +import java.net.URL /** * This is the [ViewModel] for the [SettingsFragment]. @@ -39,14 +40,17 @@ import de.cyface.utils.AppPreferences * https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate * * @author Armin Schnabel + * @author Klemens Muthmann * @version 1.0.0 * @since 3.4.0 * @param appPreferences Persistence storage of the app preferences. * @param cameraPreferences Persistence storage of the camera preferences. + * @param customPreferences Persistence storage of the ui custom preferences. */ class SettingsViewModel( private val appPreferences: AppPreferences, - private val cameraPreferences: CameraPreferences + private val cameraPreferences: CameraPreferences, + private val customPreferences: CustomPreferences ) : ViewModel() { private val _centerMap = MutableLiveData() @@ -65,6 +69,9 @@ class SettingsViewModel( private val _staticExposureTime = MutableLiveData() private val _staticExposureValue = MutableLiveData() + /** custom settings **/ + private val _diguralServerUrl = MutableLiveData() + /** * {@code True} if the camera allows to control the sensors (focus, exposure, etc.) manually. */ @@ -85,6 +92,8 @@ class SettingsViewModel( _staticExposure.value = cameraPreferences.getStaticExposure() _staticExposureTime.value = cameraPreferences.getStaticExposureTime() _staticExposureValue.value = cameraPreferences.getStaticExposureValue() + /** custom settings **/ + _diguralServerUrl.value = customPreferences.getDiguralUrl() } val centerMap: LiveData = _centerMap @@ -103,6 +112,9 @@ class SettingsViewModel( val staticExposureTime: LiveData = _staticExposureTime val staticExposureValue: LiveData = _staticExposureValue + /** custom settings **/ + val diguralServerUrl: LiveData = _diguralServerUrl + fun setCenterMap(centerMap: Boolean) { appPreferences.saveCenterMap(centerMap) _centerMap.postValue(centerMap) @@ -168,5 +180,10 @@ class SettingsViewModel( cameraPreferences.saveStaticExposureValue(staticExposureValue) _staticExposureValue.postValue(staticExposureValue) } + + fun setDiguralServerUrl(address: URL) { + customPreferences.saveDiguralUrl(address) + _diguralServerUrl.postValue(address) + } } diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModelFactory.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModelFactory.kt index 51be4597..5a21ab53 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModelFactory.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/capturing/settings/SettingsViewModelFactory.kt @@ -36,13 +36,14 @@ import de.cyface.utils.AppPreferences */ class SettingsViewModelFactory( private val appPreferences: AppPreferences, - private val cameraPreferences: CameraPreferences + private val cameraPreferences: CameraPreferences, + private val customPreferences: CustomPreferences ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { @Suppress("UNCHECKED_CAST") - return SettingsViewModel(appPreferences, cameraPreferences) as T + return SettingsViewModel(appPreferences, cameraPreferences, customPreferences) as T } throw IllegalArgumentException("Unknown ViewModel class") } diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/notification/CameraEventHandler.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/notification/CameraEventHandler.kt index ed7ce617..47e56b47 100644 --- a/ui/digural/src/main/kotlin/de/cyface/app/digural/notification/CameraEventHandler.kt +++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/notification/CameraEventHandler.kt @@ -27,128 +27,29 @@ import android.content.Intent import android.graphics.Color import android.media.RingtoneManager import android.os.Build -import android.os.Parcel -import android.os.Parcelable.Creator -import android.util.Log import androidx.core.app.NotificationCompat import de.cyface.app.digural.MainActivity import de.cyface.app.digural.R -import de.cyface.app.digural.utils.Constants.TAG import de.cyface.app.utils.SharedConstants.CAMERA_ACCESS_LOST_NOTIFICATION_ID import de.cyface.app.utils.SharedConstants.NOTIFICATION_CHANNEL_ID_RUNNING import de.cyface.app.utils.SharedConstants.NOTIFICATION_CHANNEL_ID_WARNING import de.cyface.app.utils.SharedConstants.PICTURE_CAPTURING_DECREASED_NOTIFICATION_ID -import de.cyface.app.utils.SharedConstants.SPACE_WARNING_NOTIFICATION_ID -import de.cyface.camera_service.BackgroundService -import de.cyface.camera_service.EventHandlingStrategy +import de.cyface.camera_service.background.BackgroundService +import de.cyface.camera_service.foreground.NotificationStrategy import de.cyface.utils.Validate +import kotlinx.parcelize.Parcelize /** - * A [EventHandlingStrategy] to respond to specified events triggered by the + * A [NotificationStrategy] to respond to specified events triggered by the * [BackgroundService]. * * @author Armin Schnabel * @version 1.2.2 * @since 1.0.0 */ -class CameraEventHandler : EventHandlingStrategy { - constructor() { - // Nothing to do here. - } - - /** - * Constructor as required by `Parcelable` implementation. - * - * @param in A `Parcel` that is a serialized version of a [CameraEventHandler]. - */ - @Suppress("UNUSED_PARAMETER") - private constructor(`in`: Parcel) { - // Nothing to do here. - } - - override fun handleSpaceWarning(backgroundService: BackgroundService) { - showSpaceWarningNotification(backgroundService.applicationContext) - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleSpaceWarning() - CS capturing stopped.") - } - - override fun handleCameraAccessLostWarning(backgroundService: BackgroundService) { - showCameraAccessLostNotification(backgroundService.applicationContext) - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleCameraAccessLostWarning() triggered - CS capturing stopped.") - } - - override fun handleCameraErrorWarning( - backgroundService: BackgroundService, - reason: String - ) { - showCameraErrorNotification(backgroundService.applicationContext, reason) - - // The CameraStateHandle throws a hard exception for play store statistics but - // we try to stop this service here gracefully anyway - backgroundService.stopSelf() - backgroundService.sendStoppedItselfMessage() - Log.i(TAG, "handleCameraErrorWarning() triggered - CS capturing stopped.") - } - - override fun handlePictureCapturingDecrease(backgroundService: BackgroundService) { - Log.i(TAG, "handlePictureCapturingDecrease() triggered. Showing notification.") - showPictureCapturingDecreasedNotification(backgroundService.applicationContext) - } - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) {} - - /** - * A [Notification] shown when the [BackgroundService] triggered the low space event. - * - * @param context The context if the service used to show the [Notification]. It stays even - * when the service is stopped as long as a unique id is used. - */ - private fun showSpaceWarningNotification(context: Context) { - val onClickIntent = Intent(context, MainActivity::class.java) - val onClickPendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getActivity( - context, 0, onClickIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } else { - // Ignore warning: immutable flag only available in API >= 23, see above - PendingIntent.getActivity( - context, 0, onClickIntent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - val notificationManager = context - .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - Validate.notNull(notificationManager) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - createNotificationChannelIfNotExists( - context, NOTIFICATION_CHANNEL_ID_WARNING, - context.getString(de.cyface.app.utils.R.string.notification_channel_name_warning), - context.getString(de.cyface.app.utils.R.string.notification_channel_description_warning), - NotificationManager.IMPORTANCE_HIGH, true, Color.RED, true - ) - } - // TODO: see if we not create two of those warnings (DCS and CS) - val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder( - context, - NOTIFICATION_CHANNEL_ID_WARNING - ).setContentIntent(onClickPendingIntent) - .setSmallIcon(R.drawable.ic_logo_only_c) - .setContentTitle(context.getString(de.cyface.app.utils.R.string.notification_title_capturing_stopped)) - .setContentText(context.getString(de.cyface.app.utils.R.string.error_message_capturing_canceled_no_space)) - .setOngoing(false).setWhen(System.currentTimeMillis()).setPriority(2) - .setAutoCancel(true) - .setVibrate(longArrayOf(500, 1500)) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)) - notificationManager.notify(SPACE_WARNING_NOTIFICATION_ID, notificationBuilder.build()) - } +@Parcelize +class CameraEventHandler : + NotificationStrategy { /** * A [Notification] shown when the [BackgroundService] triggered the 'camera error' event. @@ -156,7 +57,7 @@ class CameraEventHandler : EventHandlingStrategy { * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showCameraErrorNotification(context: Context, reason: String) { + override fun showCameraErrorNotification(context: Context, reason: String) { // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) @@ -203,7 +104,7 @@ class CameraEventHandler : EventHandlingStrategy { * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showCameraAccessLostNotification(context: Context) { + override fun showCameraAccessLostNotification(context: Context) { // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) @@ -253,7 +154,7 @@ class CameraEventHandler : EventHandlingStrategy { * @param context The context if the service used to show the [Notification]. It stays even * when the service is stopped as long as a unique id is used. */ - private fun showPictureCapturingDecreasedNotification(context: Context) { + override fun showPictureCapturingDecreasedNotification(context: Context) { // Open Activity when the notification is clicked val onClickIntent = Intent(context, MainActivity::class.java) @@ -345,20 +246,6 @@ class CameraEventHandler : EventHandlingStrategy { } companion object { - /** - * The `Parcelable` creator as required by the Android Parcelable specification. - */ - @Suppress("unused") - @JvmField - val CREATOR: Creator = object : Creator { - override fun createFromParcel(`in`: Parcel): CameraEventHandler { - return CameraEventHandler(`in`) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } /** * Since Android 8 it is necessary to create a new notification channel for a foreground service notification. To diff --git a/ui/digural/src/main/res/drawable/baseline_save_24.xml b/ui/digural/src/main/res/drawable/baseline_save_24.xml new file mode 100644 index 00000000..82070aa2 --- /dev/null +++ b/ui/digural/src/main/res/drawable/baseline_save_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/ui/digural/src/main/res/layout/fragment_settings.xml b/ui/digural/src/main/res/layout/fragment_settings.xml index 9196359f..f1caad9a 100644 --- a/ui/digural/src/main/res/layout/fragment_settings.xml +++ b/ui/digural/src/main/res/layout/fragment_settings.xml @@ -527,6 +527,33 @@ + + + + + + + + + @@ -584,6 +611,7 @@ android:paddingEnd="4pt" android:paddingRight="4pt" android:text="@string/settings_sensor_frequency_unit" /> + diff --git a/ui/digural/src/main/res/values-de/strings.xml b/ui/digural/src/main/res/values-de/strings.xml index d387b990..e817df17 100644 --- a/ui/digural/src/main/res/values-de/strings.xml +++ b/ui/digural/src/main/res/values-de/strings.xml @@ -25,6 +25,8 @@ Fehler beim Starten der Bluetooth Komponente Kamera Fokus Distanz Sensorfrequenz + Digural Server Adresse + Fehlerhaftes Adress-Format. Ă„nderung wurde verworfen. Kamera Status diff --git a/ui/digural/src/main/res/values-it/strings.xml b/ui/digural/src/main/res/values-it/strings.xml index f652a83e..c65e74dc 100644 --- a/ui/digural/src/main/res/values-it/strings.xml +++ b/ui/digural/src/main/res/values-it/strings.xml @@ -25,6 +25,8 @@ Error starting the bluetooth component. Camera focus distance Sensor frequency + Digural server address + Invalid address format. The change was ignored. Camera Status diff --git a/ui/digural/src/main/res/values/strings.xml b/ui/digural/src/main/res/values/strings.xml index ca309c30..809630c8 100644 --- a/ui/digural/src/main/res/values/strings.xml +++ b/ui/digural/src/main/res/values/strings.xml @@ -27,6 +27,8 @@ Camera focus distance Sensor frequency Hz + Digural server address + Invalid address format. The change was ignored. Camera Status diff --git a/ui/r4r/src/main/AndroidManifest.xml b/ui/r4r/src/main/AndroidManifest.xml index a6e2cd5f..e95549e5 100644 --- a/ui/r4r/src/main/AndroidManifest.xml +++ b/ui/r4r/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22" /> + diff --git a/utils/build.gradle b/utils/build.gradle index c73ae5cf..0919b2d1 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -51,6 +51,12 @@ android { testInstrumentationRunner rootProject.ext.testInstrumentationRunner + // Placeholders for AndroidManifest.xml + manifestPlaceholders = [ + // Dummy schema, required by the AppAuth dependency. + // Replace this in the app which in integrated the SDK. + 'appAuthRedirectScheme': 'com.example.PLACEHOLDER' + ] // Ensure that the backend's "CyfaceFull" flavor is always used missingDimensionStrategy 'project', 'cyface'