Skip to content

Commit

Permalink
#9: fixed issue with notification permission opt-in screen in instrum…
Browse files Browse the repository at this point in the history
…ented test
  • Loading branch information
arburk committed Jan 28, 2024
1 parent ee85a45 commit cb542e0
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.github.arburk.vscp.app

import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityOptionsCompat
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers
import de.mannodermaus.junit5.ActivityScenarioExtension
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

Expand All @@ -22,6 +26,27 @@ class MainActivityInstrumentedTest {
@RegisterExtension
val scenarioExtension = ActivityScenarioExtension.launch<MainActivity>()

@BeforeEach
fun setUp(scenario: ActivityScenario<MainActivity>) {
// Avoid system interaction which would block the app asking for permission by adding minimum disruptive channel
scenario.onActivity {
it.permissionActivity = MockedActivityResultLauncher()
}
}

class MockedActivityResultLauncher : ActivityResultLauncher<String> () {
override fun launch(p0: String?, p1: ActivityOptionsCompat?) { // do nothing
}

override fun unregister() { // do nothing
}

override fun getContract(): ActivityResultContract<String, *> {
throw NotImplementedError("mock only")
}

}

@Test
fun navigateToTimer(scenario: ActivityScenario<MainActivity>) {
// Context of the app under test.
Expand All @@ -41,4 +66,4 @@ class MainActivityInstrumentedTest {
}
}

}
}
67 changes: 10 additions & 57 deletions app/src/main/java/com/github/arburk/vscp/app/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
package com.github.arburk.vscp.app

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.withStarted
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import com.github.arburk.vscp.app.activity.PokerTimer
import com.github.arburk.vscp.app.common.PreferenceManagerWrapper
import com.github.arburk.vscp.app.databinding.ActivityMainBinding
import com.github.arburk.vscp.app.service.NotificationManagerWrapper
import com.github.arburk.vscp.app.service.TimerService
import com.github.arburk.vscp.app.settings.AppSettingsActivity
import kotlin.system.exitProcess
Expand All @@ -40,6 +33,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
private lateinit var _timerService: TimerService
lateinit var permissionActivity : ActivityResultLauncher<String>

val timerService: TimerService get() = _timerService

Expand All @@ -60,8 +54,8 @@ class MainActivity : AppCompatActivity() {
Intent(this, TimerService::class.java).also { intent ->
bindService(intent, timerServiceConnection, Context.BIND_AUTO_CREATE)
}
createNotificationChannel()
deepNavigationHandler(navController)
registerNotificationService()
}

private fun deepNavigationHandler(navController: NavController) {
Expand Down Expand Up @@ -131,53 +125,12 @@ class MainActivity : AppCompatActivity() {
unbindService(timerServiceConnection)
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return // Skip for lower versions

getString(R.string.notification_channel_id).also {
if(notificationChannelMissing(it)) {
// After notification channel creation, you cannot change the notification behaviors programmatically.
// The user has complete control at that point so it is useless to recreate it over and over again
executeChannelCreation(it)
private fun registerNotificationService() {
NotificationManagerWrapper().also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && it.isLackOfNotificationPermission(this)) {
// Create activity to ask for permission to post notifications
permissionActivity = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isLackOfNotificationPermission()) {
// Ask for permission to post notifications
registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun notificationChannelMissing(channelId: String): Boolean {
return ContextCompat.getSystemService(this, NotificationManager::class.java)!!
.getNotificationChannel(channelId) == null
}

@RequiresApi(Build.VERSION_CODES.O)
private fun executeChannelCreation(channelId: String) {
NotificationChannel(channelId, getString(R.string.channel_name), NotificationManager.IMPORTANCE_HIGH)
.apply {
description = getString(R.string.channel_description)
enableLights(true)
lightColor = Color.RED
setSound(
PreferenceManagerWrapper.getChannelNotificationSound(this@MainActivity),
AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
)
}.also {
// Register the channel with the system. You can't change the importance
// or other notification behaviors after this.
ContextCompat.getSystemService(this, NotificationManager::class.java)!!
.createNotificationChannel(it)
}
Log.v("MainActivity", "$channelId created")
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun isLackOfNotificationPermission() =
(ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED)

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.arburk.vscp.app.activity

import android.Manifest
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
Expand All @@ -13,6 +15,7 @@ import com.github.arburk.vscp.app.MainActivity
import com.github.arburk.vscp.app.R
import com.github.arburk.vscp.app.databinding.TimerBinding
import com.github.arburk.vscp.app.model.Blind
import com.github.arburk.vscp.app.service.NotificationManagerWrapper
import com.github.arburk.vscp.app.service.TimerService

class PokerTimer : Fragment() {
Expand All @@ -28,9 +31,21 @@ class PokerTimer : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = TimerBinding.inflate(inflater, container, false)
timerService = (activity as MainActivity).timerService
launchPermissionActivity()
return binding.root
}

private fun launchPermissionActivity() {
NotificationManagerWrapper().also {nmw ->
(this.requireActivity() as MainActivity).also { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && nmw.isLackOfNotificationPermission(activity)) {
nmw.createNotificationChannel(activity)
activity.permissionActivity.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.v("PokerTimer", "onViewCreated")
super.onViewCreated(view, savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.github.arburk.vscp.app.service

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.AudioAttributes
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.github.arburk.vscp.app.R
import com.github.arburk.vscp.app.common.PreferenceManagerWrapper

class NotificationManagerWrapper {

@VisibleForTesting
internal lateinit var mockedManager: NotificationManager

fun get(ctx: Context): NotificationManager? {
if (this::mockedManager.isInitialized) {
return mockedManager
}

return ContextCompat.getSystemService(ctx, NotificationManager::class.java)
}

fun createNotificationChannel(ctx: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return // Skip for lower versions

ctx.getString(R.string.notification_channel_id).also {
if(notificationChannelMissing(ctx, it)) {
// After notification channel creation, you cannot change the notification behaviors programmatically.
// The user has complete control at that point so it is useless to recreate it over and over again
executeChannelCreation(ctx, it)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun executeChannelCreation(ctx: Context, channelId: String) {
NotificationChannel(channelId, ctx.getString(R.string.channel_name), NotificationManager.IMPORTANCE_HIGH)
.apply {
description = ctx.getString(R.string.channel_description)
enableLights(true)
lightColor = Color.RED
setSound(
PreferenceManagerWrapper.getChannelNotificationSound(ctx),
AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
)
}.also {
// Register the channel with the system. You can't change the importance
// or other notification behaviors after this.
get(ctx)!!.createNotificationChannel(it)
}
Log.v("MainActivity", "$channelId created")
}

@RequiresApi(Build.VERSION_CODES.O)
private fun notificationChannelMissing(ctx: Context, channelId: String): Boolean {
return get(ctx)!!.getNotificationChannel(channelId) == null
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun isLackOfNotificationPermission(ctx: Context) =
(ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED)

}

0 comments on commit cb542e0

Please sign in to comment.