Skip to content

Commit

Permalink
Request bluetooth permission to pause on device disconnect
Browse files Browse the repository at this point in the history
Since Android S, a new permission is required to be able to receive the relevant broadcast events.
  • Loading branch information
Maxr1998 authored and nielsvanvelzen committed Sep 5, 2023
1 parent af9f2be commit d440213
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 1 deletion.
10 changes: 9 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30"
android:required="false" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT"
android:required="false"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/jellyfin/mobile/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.jellyfin.mobile.player.ui.PlayerFragment
import org.jellyfin.mobile.setup.ConnectFragment
import org.jellyfin.mobile.utils.AndroidVersion
import org.jellyfin.mobile.utils.BackPressInterceptor
import org.jellyfin.mobile.utils.BluetoothPermissionHelper
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.PermissionRequestHelper
import org.jellyfin.mobile.utils.SmartOrientationListener
Expand All @@ -41,6 +42,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
class MainActivity : AppCompatActivity() {
private val activityEventHandler: ActivityEventHandler = get()
val mainViewModel: MainViewModel by viewModel()
val bluetoothPermissionHelper: BluetoothPermissionHelper = BluetoothPermissionHelper(this, get())
val chromecast: IChromecast = Chromecast()
private val permissionRequestHelper: PermissionRequestHelper by inject()

Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/org/jellyfin/mobile/app/AppPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class AppPreferences(context: Context) {
}
}

var ignoreBluetoothPermission: Boolean
get() = sharedPreferences.getBoolean(Constants.PREF_IGNORE_BLUETOOTH_PERMISSION, false)
set(value) {
sharedPreferences.edit {
putBoolean(Constants.PREF_IGNORE_BLUETOOTH_PERMISSION, value)
}
}

var downloadMethod: Int?
get() = sharedPreferences.getInt(Constants.PREF_DOWNLOAD_METHOD, -1).takeIf { it >= 0 }
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ class NativeInterface(private val context: Context) : KoinComponent {
putExtra(EXTRA_IS_PAUSED, options.optBoolean(EXTRA_IS_PAUSED, true))
}
context.startService(intent)

// We may need to request bluetooth permission to react to bluetooth disconnect events
activityEventHandler.emit(ActivityEvent.RequestBluetoothPermission)
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sealed class ActivityEvent {
class OpenUrl(val uri: String) : ActivityEvent()
class DownloadFile(val uri: Uri, val title: String, val filename: String) : ActivityEvent()
class CastMessage(val action: String, val args: JSONArray) : ActivityEvent()
data object RequestBluetoothPermission : ActivityEvent()
object OpenSettings : ActivityEvent()
object SelectServer : ActivityEvent()
object ExitApp : ActivityEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ActivityEventHandler(
}
}

@Suppress("CyclomaticComplexMethod", "LongMethod")
private suspend fun MainActivity.handleEvent(event: ActivityEvent) {
when (event) {
is ActivityEvent.ChangeFullscreen -> {
Expand Down Expand Up @@ -88,6 +89,11 @@ class ActivityEventHandler(
},
)
}
ActivityEvent.RequestBluetoothPermission -> {
lifecycleScope.launch {
bluetoothPermissionHelper.requestBluetoothPermissionIfNecessary()
}
}
ActivityEvent.OpenSettings -> {
supportFragmentManager.addFragment<SettingsFragment>()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jellyfin.mobile.utils

import android.Manifest.permission.BLUETOOTH_CONNECT
import android.app.AlertDialog
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import kotlinx.coroutines.suspendCancellableCoroutine
import org.jellyfin.mobile.MainActivity
import org.jellyfin.mobile.R
import org.jellyfin.mobile.app.AppPreferences
import kotlin.coroutines.resume

class BluetoothPermissionHelper(
private val activity: MainActivity,
private val appPreferences: AppPreferences,
) {
/**
* This is used to prevent the dialog from showing multiple times in a single session (activity creation).
* Otherwise, the package manager and permission would need to be queried on every media event.
*/
private var wasDialogShowThisSession = false

@Suppress("ComplexCondition")
suspend fun requestBluetoothPermissionIfNecessary() {
// Check conditions by increasing complexity
if (
!AndroidVersion.isAtLeastS ||
wasDialogShowThisSession ||
activity.checkSelfPermission(BLUETOOTH_CONNECT) == PERMISSION_GRANTED ||
appPreferences.ignoreBluetoothPermission ||
!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
) {
return
}

wasDialogShowThisSession = true

val shouldRequestPermission = suspendCancellableCoroutine { continuation ->
AlertDialog.Builder(activity)
.setTitle(R.string.bluetooth_permission_title)
.setMessage(R.string.bluetooth_permission_message)
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
continuation.resume(false)
}
.setPositiveButton(R.string.bluetooth_permission_continue) { dialog, _ ->
dialog.dismiss()
continuation.resume(true)
}
.setOnCancelListener {
continuation.resume(false)
}
.show()
}

if (!shouldRequestPermission) {
appPreferences.ignoreBluetoothPermission = true
return
}

activity.requestPermission(BLUETOOTH_CONNECT) { requestPermissionsResult ->
if (requestPermissionsResult[BLUETOOTH_CONNECT] == PERMISSION_GRANTED) {
activity.toast(R.string.bluetooth_permission_granted)
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/java/org/jellyfin/mobile/utils/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ object Constants {
const val PREF_INSTANCE_URL = "pref_instance_url"
const val PREF_IGNORE_BATTERY_OPTIMIZATIONS = "pref_ignore_battery_optimizations"
const val PREF_IGNORE_WEBVIEW_CHECKS = "pref_ignore_webview_checks"
const val PREF_IGNORE_BLUETOOTH_PERMISSION = "pref_ignore_bluetooth_permission"
const val PREF_DOWNLOAD_METHOD = "pref_download_method"
const val PREF_MUSIC_NOTIFICATION_ALWAYS_DISMISSIBLE = "pref_music_notification_always_dismissible"
const val PREF_VIDEO_PLAYER_TYPE = "pref_video_player_type"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<string name="available_servers_title">Available servers</string>
<string name="button_use_different_server">Use a different server</string>

<string name="bluetooth_permission_title">Allow Bluetooth access?</string>
<string name="bluetooth_permission_message">To detect when wireless headphones or speakers are disconnected, the app requires Bluetooth access. On Android, this permission is described as access to nearby devices, and warns that Bluetooth devices can be used to infer the current location. However, the Jellyfin app will never use this permission to detect your location.</string>
<string name="bluetooth_permission_continue">Continue</string>
<string name="bluetooth_permission_granted">Bluetooth permission was granted</string>
<string name="battery_optimizations_message">Please disable battery optimizations for media playback while the screen is off.</string>
<string name="network_title">Allowed Network Types</string>
<string name="network_message">Do you want to allow the download to run over mobile data or roaming networks? Charges from your provider may apply.</string>
Expand Down

0 comments on commit d440213

Please sign in to comment.