Skip to content

Commit

Permalink
Implement bluetooth permission explanation ui and some other improvem…
Browse files Browse the repository at this point in the history
…ents

~ Make UI Scrollable
~ Removed not required permissions
  • Loading branch information
NiroDeveloper committed May 17, 2024
1 parent 9dadd14 commit d6e7146
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 54 deletions.
11 changes: 5 additions & 6 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE"/>

<!-- TODO: Check what is really needed here -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="33" />

<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>

Expand Down Expand Up @@ -45,6 +40,10 @@
<activity
android:name=".ui.activities.ErrorActivity"
android:taskAffinity="" />

<activity
android:name=".ui.activities.BluetoothPermissionActivity"
android:taskAffinity="" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package dev.niro.cameraremote.bluetooth

import android.app.Activity
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import dev.niro.cameraremote.R
import dev.niro.cameraremote.bluetooth.helper.BluetoothPermission
import dev.niro.cameraremote.bluetooth.helper.sendKeyboardPress
import dev.niro.cameraremote.interfaces.IConnectionStateCallback
import dev.niro.cameraremote.interfaces.IUserInterfaceCallback
import dev.niro.cameraremote.ui.activities.BluetoothPermissionActivity

object BluetoothController {

Expand Down Expand Up @@ -51,11 +54,11 @@ object BluetoothController {
}
}

fun handleBluetooth(context: Context, permissionLauncher: ActivityResultLauncher<String>) {
if (BluetoothPermission.hasBluetoothPermission(context)) {
fun handleBluetooth(activity: Activity, permissionLauncher: ActivityResultLauncher<String>) {
if (BluetoothPermission.hasBluetoothPermission(activity)) {
val localBluetoothCallback = bluetoothCallback
if (localBluetoothCallback == null) {
registerBluetoothService(context)
registerBluetoothService(activity)
return
}

Expand All @@ -70,9 +73,14 @@ object BluetoothController {
return
}

// TODO: Show bluetooth explanation ui
// https://developer.android.com/training/permissions/requesting?hl=de#explain
if (BluetoothPermission.shouldShowPermissionDescription(activity)) {
val bluetoothPermissionIntent = Intent(activity, BluetoothPermissionActivity::class.java)
activity.startActivity(bluetoothPermissionIntent)

return
}

Log.i(null, "Requesting bluetooth permission")
BluetoothPermission.requestBluetoothPermission(permissionLauncher)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class BluetoothServiceCallback(private val connectionStateListener: IConnectionS
val devices = connectHidDevice.getDevicesMatchingConnectionStates(connectionStates)

if (devices.isEmpty()) {
Log.e(null, "No devices found")
connectionStateListener.onConnectionError(R.string.error_no_devices_found)

return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package dev.niro.cameraremote.bluetooth.helper

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import dev.niro.cameraremote.bluetooth.BluetoothController

object BluetoothPermission {

Expand All @@ -19,12 +20,8 @@ object BluetoothPermission {
return permissionResult == PackageManager.PERMISSION_GRANTED
}

fun buildPermissionLauncher(activity: ComponentActivity): ActivityResultLauncher<String> {
return activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted ->
if (permissionGranted) {
BluetoothController.registerBluetoothService(activity)
}
}
fun buildPermissionLauncher(activity: ComponentActivity, callback: (Boolean) -> Unit): ActivityResultLauncher<String> {
return activity.registerForActivityResult(ActivityResultContracts.RequestPermission(), callback)
}

fun requestBluetoothPermission(permissionLauncher: ActivityResultLauncher<String>) {
Expand All @@ -33,11 +30,21 @@ object BluetoothPermission {
permissionLauncher.launch(bluetoothPermission)
}

/**
* Requirement from android.
* https://developer.android.com/training/permissions/requesting?hl=de#explain
*/
fun shouldShowPermissionDescription(activity: Activity): Boolean {
val bluetoothPermission = getBluetoothPermissionName()

return ActivityCompat.shouldShowRequestPermissionRationale(activity, bluetoothPermission)
}

private fun getBluetoothPermissionName(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Manifest.permission.BLUETOOTH_CONNECT
} else {
Manifest.permission.BLUETOOTH_ADMIN
Manifest.permission.BLUETOOTH
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package dev.niro.cameraremote.ui.activities

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.Text
import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import dev.niro.cameraremote.R
import dev.niro.cameraremote.bluetooth.helper.BluetoothPermission

class BluetoothPermissionActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Must be created at activity startup, otherwise the app will crash.
val permissionLauncher = BluetoothPermission.buildPermissionLauncher(this) { permissionGranted ->
Log.d(null, "Bluetooth permission granted: $permissionGranted")

if (permissionGranted) {
finish()
} else {
val errorIntent = Intent(this, ErrorActivity::class.java)
errorIntent.putExtra("messageId", R.string.error_bluetooth_permission_denied)
this.startActivity(errorIntent)
}
}

setContent {
BluetoothPermissionLayout {
Log.i(null, "Requesting bluetooth permission")

BluetoothPermission.requestBluetoothPermission(permissionLauncher)
}
}
}
}


@OptIn(ExperimentalHorologistApi::class)
@Preview(device = WearDevices.RECT, showSystemUi = true)
@Preview(device = WearDevices.SQUARE, showSystemUi = true)
@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
@Preview(device = WearDevices.LARGE_ROUND, showSystemUi = true)
@Composable
fun BluetoothPermissionLayout(onOk: (() -> Unit)? = null) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
columnState = ScalingLazyColumnState(initialScrollPosition = ScalingLazyColumnState.ScrollPosition(0, 100))
) {
item {
Icon(
painter = painterResource(id = R.drawable.baseline_info_24),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
}

item {
Text(
text = stringResource(id = R.string.bluetooth_permission_description),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
}

item {
Button(
onClick = {
if (onOk != null) {
onOk()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(text = stringResource(id = R.string.ok))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
package dev.niro.cameraremote.ui.activities

import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand All @@ -25,8 +18,10 @@ import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.Text
import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import dev.niro.cameraremote.R
import kotlin.math.roundToInt

class ErrorActivity : ComponentActivity() {

Expand All @@ -37,50 +32,56 @@ class ErrorActivity : ComponentActivity() {
val errorMessage = resources.getString(errorId)

setContent {
ErrorLayout(errorMessage)
ErrorLayout(errorMessage) {
finish()
}
}
}

}

@OptIn(ExperimentalHorologistApi::class)
@Preview(device = WearDevices.RECT, showSystemUi = true)
@Preview(device = WearDevices.SQUARE, showSystemUi = true)
@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
@Preview(device = WearDevices.LARGE_ROUND, showSystemUi = true)
@Composable
fun ErrorLayout(message: String = "This is a very long error message that describes what happened.") {
val activity = LocalContext.current as Activity

BoxWithConstraints {
val horizontalPadding = (maxWidth.value * 0.1).roundToInt()
val verticalPadding = (maxHeight.value * 0.1).roundToInt()

Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = ScrollState(0))
.padding(horizontal = horizontalPadding.dp, vertical = verticalPadding.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
fun ErrorLayout(
message: String = "This is a very long error message that describes what happened.",
onOk: (() -> Unit)? = null
) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
columnState = ScalingLazyColumnState(initialScrollPosition = ScalingLazyColumnState.ScrollPosition(0, 100))
) {
item {
Icon(
painter = painterResource(id = R.drawable.baseline_error_24),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
}

item {
Text(
text = message,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(4.dp)
)
}

item {
Button(
onClick = { activity.finish() },
onClick = {
if (onOk != null) {
onOk()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(4.dp)
) {
Text(text = stringResource(id = R.string.ok))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.niro.cameraremote.ui.activities

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
Expand All @@ -19,7 +21,16 @@ class MainActivity : ComponentActivity() {
BluetoothController.init(this)

// Must be created at activity startup, otherwise the app will crash.
val permissionLauncher = BluetoothPermission.buildPermissionLauncher(this)
val permissionLauncher = BluetoothPermission.buildPermissionLauncher(this) { permissionGranted ->
Log.d(null, "Bluetooth permission granted: $permissionGranted")

if (permissionGranted) {
BluetoothController.registerBluetoothService(this)
} else {
val bluetoothPermissionIntent = Intent(this, BluetoothPermissionActivity::class.java)
this.startActivity(bluetoothPermissionIntent)
}
}

setContent {
AmbientAware {
Expand Down
Loading

0 comments on commit d6e7146

Please sign in to comment.