Skip to content

Commit

Permalink
[STAD-525] Upgrade targetVersion to 33, add permission notification (#65
Browse files Browse the repository at this point in the history
)

* [RFR-525] Upgrade targetVersion to 33

* [RFR-525] Upgrade buildToolVersion to 34.0.0

* Fix text color on terms page

* [STAD-526] Request notification permissions

* Upgrade utils to 3.5.1 (targetversion 33)

* Upgrade androidx.preferences to 1.2.1

* Upgrade energy-settings to 3.3.5 (targetversion 33)

* Upgrade SDK to 7.8.2

* Upgrade camera_service to 4.2.1 (targetVersion 33)
  • Loading branch information
hb0 authored Aug 23, 2023
1 parent aa27691 commit d7cef1d
Show file tree
Hide file tree
Showing 17 changed files with 496 additions and 98 deletions.
2 changes: 1 addition & 1 deletion backend
Submodule backend updated 1 files
+6 −6 build.gradle
18 changes: 9 additions & 9 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,27 @@ ext {
*/

// Cyface dependencies
cyfaceAndroidBackendVersion = "7.8.1" // Also update submodule commit ref
cyfaceUtilsVersion = "3.5.0"
cyfaceEnergySettingsVersion = "3.3.4" // Also update submodule commit ref
cyfaceCameraServiceVersion = "4.2.0" // Also update submodule commit ref
cyfaceAndroidBackendVersion = "7.8.2" // Also update submodule commit ref
cyfaceUtilsVersion = "3.5.1"
cyfaceEnergySettingsVersion = "3.3.5" // Also update submodule commit ref
cyfaceCameraServiceVersion = "4.2.1" // 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`
cyfaceSerializationVersion = "3.0.0" // Keep im sync with version in submodule `backend`
cyfaceUploaderVersion = "1.0.0"

// Android SDK versions
minSdkVersion = 21 // device support
targetSdkVersion = 31 // behavioral changes, follow migration guide & test the app against this
targetSdkVersion = 33 // behavioral changes, follow migration guide & test the app against this
compileSdkVersion = 33 // allows newest APIs to be used and to see deprecations, use latest
buildToolsVersion = '33.0.0' // optional, if defined, use latest (SDK Manager > SDK Tools)
buildToolsVersion = '34.0.0' // optional, if defined, use latest (SDK Manager > SDK Tools)

// Android dependencies
androidxAnnotationVersion = '1.6.0'
androidxAppCompatVersion = "1.6.1"
androidPreferencesVersion = '1.2.0'
androidPreferencesVersion = '1.2.1'
localbroadcastmanagerVersion = '1.1.0'
exifinterfaceVersion = "1.3.6" // Required by Camera Service submodule
roomVersion = "2.5.1"
roomVersion = "2.5.2"
lifecycleVersion = "2.6.1"
navigationVersion = "2.5.3"
okHttpVersion = "4.11.0"
Expand Down
2 changes: 1 addition & 1 deletion camera_service
2 changes: 1 addition & 1 deletion energy_settings
Submodule energy_settings updated 1 files
+5 −5 build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class CapturingFragment : Fragment(), ConnectionStatusListener {
} else {
Toast.makeText(
context,
"Location permission repeatedly denies",
requireContext().getString(de.cyface.app.utils.R.string.missing_location_permissions_toast),
Toast.LENGTH_LONG
).show()
// Close Cyface if permission has not been granted.
Expand Down
182 changes: 153 additions & 29 deletions ui/cyface/src/main/kotlin/de/cyface/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,23 @@ import android.accounts.AccountManagerFuture
import android.accounts.AuthenticatorException
import android.accounts.OperationCanceledException
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.MainThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
Expand Down Expand Up @@ -78,6 +84,7 @@ import net.openid.appauth.TokenResponse
import java.io.IOException
import java.lang.ref.WeakReference


/**
* The base `Activity` for the actual Cyface measurement client. It's called by the [TermsOfUseActivity]
* class.
Expand All @@ -88,7 +95,7 @@ import java.lang.ref.WeakReference
*
* @author Klemens Muthmann
* @author Armin Schnabel
* @version 4.0.0
* @version 4.1.0
* @since 1.0.0
*/
class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider {
Expand Down Expand Up @@ -123,6 +130,16 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider
*/
private lateinit var cameraPreferences: CameraPreferences

/**
* The launcher to call after a permission request returns.
*/
private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>

/**
* The dialog which inform the user about missing permissions.
*/
private var permissionDialog: AlertDialog? = null

/**
* The authorization.
*/
Expand Down Expand Up @@ -165,31 +182,12 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider
appPreferences = AppPreferences(this)
cameraPreferences = CameraPreferences(this)

// Location permissions are requested by MainFragment which needs to react to results

// If camera service is requested, check needed permissions
val cameraEnabled = cameraPreferences.getCameraEnabled()
val permissionsMissing = ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED /*
* No permissions needed as we write the data to the app specific directory.
* || ContextCompat.checkSelfPermission(this,
* Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
* || ContextCompat.checkSelfPermission(this,
* Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
*/
if (cameraEnabled && permissionsMissing) {
ActivityCompat.requestPermissions(
this, arrayOf(
Manifest.permission.CAMERA /*
* , Manifest.permission.WRITE_EXTERNAL_STORAGE,
* Manifest.permission.READ_EXTERNAL_STORAGE
*/
),
de.cyface.camera_service.Constants.PERMISSION_REQUEST_CAMERA_AND_STORAGE_PERMISSION
)
}
// Location permissions are requested by CapturingFragment/Map to react to results.
permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
// Handled by onResume: showMissingPermissions()
}
requestMissingPermissions(cameraPreferences)

// Start DataCapturingService and CameraService
val sensorFrequency = appPreferences.getSensorFrequency()
Expand Down Expand Up @@ -306,6 +304,14 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider
showGnssWarningDialog(this)
showEnergySaferWarningDialog(this)
showRestrictedBackgroundProcessingWarningDialog(this)
// Show dialog if permissions are missing (e.g. dismissed too ofter)
permissionDialog?.dismiss() // reset previous to show current permission state
val cameraMissing = cameraPermissionMissing(this, cameraPreferences)
val notificationMissing = notificationPermissionMissing(this)
val locationMissing = !granted(this, Manifest.permission.ACCESS_FINE_LOCATION)
if (cameraMissing || notificationMissing || locationMissing) {
showMissingPermissions(cameraMissing, notificationMissing, locationMissing)
}
super.onResume()
}

Expand All @@ -328,14 +334,132 @@ class MainActivity : AppCompatActivity(), ServiceProvider, CameraServiceProvider
auth.dispose()
}

private fun showMissingPermissions(
cameraMissing: @JvmSuppressWildcards Boolean,
notificationMissing: @JvmSuppressWildcards Boolean,
locationMissing: @JvmSuppressWildcards Boolean
) {
if (cameraMissing || notificationMissing || locationMissing) {
val cameraString = this.getString(de.cyface.app.utils.R.string.camera)
val notificationString =
this.getString(de.cyface.app.utils.R.string.notification)
val locationString = this.getString(de.cyface.app.utils.R.string.location)
val missing = mutableListOf<String>()
if (cameraMissing) missing.add(cameraString)
if (notificationMissing) missing.add(notificationString)
if (locationMissing) missing.add(locationString)

permissionDialog = AlertDialog.Builder(this)
.setTitle(this.getString(de.cyface.app.utils.R.string.missing_permissions))
.setMessage(
this.getString(
de.cyface.app.utils.R.string.missing_permissions_info,
missing.toCustomString()
)
)
.setPositiveButton(de.cyface.app.utils.R.string.change_permissions) { dialog, _ ->
dialog.dismiss()
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
}
.setNegativeButton(de.cyface.app.utils.R.string.close_app) { _, _ ->
finish()
}
.setCancelable(false)
.show()
}
}

/**
* @return a string in the format: "item1, item2 and item3"
*/
private fun List<String>.toCustomString(): String {
val and = applicationContext.getString(de.cyface.app.utils.R.string.and)
return when (size) {
1 -> this[0]
2 -> "${this[0]} $and ${this[1]}"
else -> "${this.dropLast(1).joinToString(", ")} $and ${this.last()}"
}
}

/**
* Checks and requests missing permissions.
*
* @param cameraPreferences The camera preferences to check if camera is enabled.
*/
private fun requestMissingPermissions(cameraPreferences: CameraPreferences) {
// Without notification permissions the capturing notification is not shown on Android >= 13
// But capturing still works.
val permissionsMissing = missingPermission(this, cameraPreferences)
if (permissionsMissing) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val cameraEnabled = cameraPreferences.getCameraEnabled()
val permissions = if (cameraEnabled) arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.ACCESS_FINE_LOCATION
) else arrayOf(
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.ACCESS_FINE_LOCATION
)
permissionLauncher.launch(permissions)
} else {
val permissions = arrayOf(Manifest.permission.CAMERA)
permissionLauncher.launch(permissions)
}
}
}

/**
* Checks if permissions are missing.
*
* @param context The context to check for.
* @param cameraPreferences The camera preferences to check if camera is enable.
* @return `true` if permissions are missing.
*/
private fun missingPermission(context: Context, cameraPreferences: CameraPreferences): Boolean {
val cameraPermissionMissing = cameraPermissionMissing(context, cameraPreferences)
val notificationPermissionMissing = notificationPermissionMissing(context)
val locationPermissionMissing = !granted(context, Manifest.permission.ACCESS_FINE_LOCATION)
return cameraPermissionMissing || notificationPermissionMissing || locationPermissionMissing
}

private fun notificationPermissionMissing(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
!granted(context, Manifest.permission.POST_NOTIFICATIONS)
} else {
false
}
}

private fun cameraPermissionMissing(
context: Context,
cameraPreferences: CameraPreferences
): Boolean {
val cameraEnabled = cameraPreferences.getCameraEnabled()
return if (cameraEnabled) !granted(context, Manifest.permission.CAMERA) else false
}

/**
* Determine whether you have been granted a particular permission.
*
* @param permission The permission to check.
* @return `true` if the permission was already granted.
*/
private fun granted(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PERMISSION_GRANTED
}

/**
* Starts the synchronization.
*
* Creates an [Account] if there is none as an account is required
* for synchronization. If there was no account the synchronization is started when the async account
* creation future returns to ensure the account is available at that point.
*/
fun startSynchronization() {
private fun startSynchronization() {
val accountManager = AccountManager.get(this.applicationContext)
val validAccountExists = accountWithTokenExists(accountManager)
if (validAccountExists) {
Expand Down
1 change: 1 addition & 0 deletions ui/cyface/src/main/res/layout/activity_terms_of_use.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
android:layout_marginBottom="5sp"
android:enabled="true"
android:backgroundTint="@color/green_700"
android:textColor="@color/white"
android:text="@string/accept_terms" />

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class CapturingFragment : Fragment(), ConnectionStatusListener {
} else {
Toast.makeText(
context,
"Location permission repeatedly denies",
requireContext().getString(de.cyface.app.utils.R.string.missing_location_permissions_toast),
Toast.LENGTH_LONG
).show()
// Close Cyface if permission has not been granted.
Expand Down
Loading

0 comments on commit d7cef1d

Please sign in to comment.