Unit tests for NockNotificationManager

This commit is contained in:
Aidan Follestad 2018-12-01 17:36:14 -08:00
parent cf39207c08
commit 03c687def5
21 changed files with 333 additions and 163 deletions

View file

@ -35,6 +35,7 @@ dependencies {
implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
implementation 'com.jakewharton.timber:timber:' + versions.timber
testImplementation 'junit:junit:' + versions.junit
testImplementation 'org.mockito:mockito-core:' + versions.mockito
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin

View file

@ -6,7 +6,6 @@
package com.afollestad.nocknock
import android.app.Application
import android.util.Log
import com.afollestad.nocknock.di.AppComponent
import com.afollestad.nocknock.di.DaggerAppComponent
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
@ -18,19 +17,14 @@ import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
import com.afollestad.nocknock.utilities.Injector
import com.afollestad.nocknock.utilities.ext.systemService
import okhttp3.OkHttpClient
import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
class NockNockApp : Application(), Injector {
companion object {
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("NockNockApp", message)
}
}
}
private lateinit var appComponent: AppComponent
@Inject lateinit var nockNotificationManager: NockNotificationManager
@ -39,6 +33,10 @@ class NockNockApp : Application(), Injector {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
val okHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
val request = chain.request()

View file

@ -22,6 +22,7 @@ ext.versions = [
materialDialogs : '2.0.0-rc3',
rxkPrefs : '1.2.0',
timber : '4.7.1',
junit : '4.12',
mockito : '2.23.0',
mockitoKotlin : '2.0.0-RC1',

View file

@ -27,6 +27,7 @@ dependencies {
implementation 'com.google.dagger:dagger:' + versions.dagger
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
implementation 'com.jakewharton.timber:timber:' + versions.timber
testImplementation 'junit:junit:' + versions.junit
testImplementation 'org.mockito:mockito-core:' + versions.mockito
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin

View file

@ -7,13 +7,13 @@ package com.afollestad.nocknock.engine.db
import android.app.Application
import android.database.Cursor
import android.util.Log
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
import com.afollestad.nocknock.engine.BuildConfig
import com.afollestad.nocknock.utilities.ext.diffFrom
import timber.log.Timber.d as log
import timber.log.Timber.w as warn
import javax.inject.Inject
/** @author Aidan Follestad (@afollestad) */
@ -35,21 +35,6 @@ interface ServerModelStore {
/** @author Aidan Follestad (@afollestad) */
class RealServerModelStore @Inject constructor(app: Application) : ServerModelStore {
companion object {
private fun log(
message: String,
warning: Boolean = false
) {
if (BuildConfig.DEBUG) {
if (warning) {
Log.w("ServerModelStore", message)
} else {
Log.d("ServerModelStore", message)
}
}
}
}
private val dbHelper = ServerModelDbHelper(app)
override suspend fun get(id: Int?): List<ServerModel> {
@ -115,7 +100,7 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
val valuesDiff = oldValues.diffFrom(newValues)
if (valuesDiff.size() == 0) {
log("Nothing has changed - nothing to update!", warning = true)
warn("Nothing has changed - nothing to update!")
return 0
}

View file

@ -9,8 +9,6 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_BOOT_COMPLETED
import android.util.Log
import com.afollestad.nocknock.engine.BuildConfig
import com.afollestad.nocknock.utilities.ext.injector
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@ -18,18 +16,11 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import javax.inject.Inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
class BootReceiver : BroadcastReceiver() {
companion object {
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("BootReceiver", message)
}
}
}
@Inject lateinit var checkStatusManager: CheckStatusManager
override fun onReceive(

View file

@ -8,7 +8,6 @@ package com.afollestad.nocknock.engine.statuscheck
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Intent
import android.util.Log
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerStatus
import com.afollestad.nocknock.data.ServerStatus.CHECKING
@ -21,7 +20,6 @@ import com.afollestad.nocknock.data.isPending
import com.afollestad.nocknock.engine.BuildConfig.APPLICATION_ID
import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.utilities.BuildConfig
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.js.JavaScript
import kotlinx.coroutines.Dispatchers.IO
@ -32,6 +30,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.System.currentTimeMillis
import javax.inject.Inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad)*/
class CheckStatusJob : JobService() {
@ -41,12 +40,6 @@ class CheckStatusJob : JobService() {
const val ACTION_JOB_RUNNING = "$APPLICATION_ID.STATUS_JOB_RUNNING"
const val KEY_UPDATE_MODEL = "site_model"
const val KEY_SITE_ID = "site.id"
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("CheckStatusJob", message)
}
}
}
@Inject lateinit var modelStore: ServerModelStore

View file

@ -10,20 +10,19 @@ import android.app.job.JobInfo.NETWORK_TYPE_ANY
import android.app.job.JobScheduler
import android.app.job.JobScheduler.RESULT_SUCCESS
import android.os.PersistableBundle
import android.util.Log
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerStatus.ERROR
import com.afollestad.nocknock.data.ServerStatus.OK
import com.afollestad.nocknock.engine.R
import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
import com.afollestad.nocknock.utilities.BuildConfig
import com.afollestad.nocknock.utilities.providers.StringProvider
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.net.SocketTimeoutException
import javax.inject.Inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
data class CheckResult(
@ -56,14 +55,6 @@ class RealCheckStatusManager @Inject constructor(
private val siteStore: ServerModelStore
) : CheckStatusManager {
companion object {
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("CheckStatusManager", message)
}
}
}
override suspend fun ensureScheduledChecks() {
val sites = siteStore.get()
if (sites.isEmpty()) {

View file

@ -26,6 +26,7 @@ dependencies {
implementation 'com.google.dagger:dagger:' + versions.dagger
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
implementation 'com.jakewharton.timber:timber:' + versions.timber
testImplementation 'junit:junit:' + versions.junit
testImplementation 'org.mockito:mockito-core:' + versions.mockito
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin

View file

@ -5,10 +5,6 @@
*/
package com.afollestad.nocknock.notifications
import android.app.NotificationChannel
import android.content.Context
import android.os.Build.VERSION_CODES
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
/** @author Aidan Follestad (@afollestad) */
@ -25,14 +21,3 @@ enum class Channel(
importance = IMPORTANCE_DEFAULT
)
}
/** @author Aidan Follestad (@afollestad) */
@RequiresApi(VERSION_CODES.O)
fun Channel.toNotificationChannel(context: Context): NotificationChannel {
val titleText = context.getString(this.title)
val descriptionText = context.getString(this.description)
return NotificationChannel(this.id, titleText, this.importance)
.apply {
description = descriptionText
}
}

View file

@ -6,20 +6,19 @@
package com.afollestad.nocknock.notifications
import android.annotation.TargetApi
import android.app.Application
import android.app.NotificationManager
import android.os.Build.VERSION_CODES
import android.util.Log
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.notifications.Channel.CheckFailures
import com.afollestad.nocknock.utilities.providers.BitmapProvider
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.NotificationProvider
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.BASE_NOTIFICATION_REQUEST_CODE
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
import com.afollestad.nocknock.utilities.util.hasOreo
import javax.inject.Inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
interface NockNotificationManager {
@ -37,22 +36,15 @@ interface NockNotificationManager {
/** @author Aidan Follestad (@afollestad) */
class RealNockNotificationManager @Inject constructor(
private val app: Application,
@AppIconRes private val appIconRes: Int,
private val stockManager: NotificationManager,
private val bitmapProvider: BitmapProvider,
private val stringProvider: StringProvider,
private val intentProvider: IntentProvider
private val intentProvider: IntentProvider,
private val channelProvider: NotificationChannelProvider,
private val notificationProvider: NotificationProvider
) : NockNotificationManager {
companion object {
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("NockNotificationManager", message)
}
}
}
private var isAppOpen = false
override fun setIsAppOpen(open: Boolean) {
@ -73,15 +65,14 @@ class RealNockNotificationManager @Inject constructor(
log("Posting status notification for site ${model.id}...")
val intent = intentProvider.getPendingIntentForViewSite(model)
val newNotification = notification(app, CheckFailures) {
setContentTitle(model.name)
setContentText(stringProvider.get(R.string.something_wrong))
setContentIntent(intent)
setSmallIcon(R.drawable.ic_notification)
setLargeIcon(bitmapProvider.get(appIconRes))
setAutoCancel(true)
setDefaults(DEFAULT_VIBRATE)
}
val newNotification = notificationProvider.create(
channelId = CheckFailures.id,
title = model.name,
content = stringProvider.get(R.string.something_wrong),
intent = intent,
smallIcon = R.drawable.ic_notification,
largeIcon = bitmapProvider.get(appIconRes)
)
stockManager.notify(model.url, model.notificationId(), newNotification)
log("Posted status notification for site ${model.notificationId()}.")
@ -96,13 +87,13 @@ class RealNockNotificationManager @Inject constructor(
@TargetApi(VERSION_CODES.O)
private fun createChannel(channel: Channel) {
if (!hasOreo()) {
log("Not running Android O, channels won't be created.")
return
}
val notificationChannel = channel.toNotificationChannel(app)
stockManager.createNotificationChannel(notificationChannel)
val notificationChannel = channelProvider.create(
id = channel.id,
title = stringProvider.get(channel.title),
description = stringProvider.get(channel.description),
importance = channel.importance
)
notificationChannel?.let(stockManager::createNotificationChannel)
log("Created notification channel ${channel.id}")
}

View file

@ -1,24 +0,0 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.notifications
import android.app.Notification
import android.content.Context
import androidx.core.app.NotificationCompat
typealias NotificationBuilder = NotificationCompat.Builder
typealias NotificationConstructor = NotificationBuilder.() -> Unit
/** @author Aidan Follestad (@afollestad) */
fun notification(
context: Context,
channel: Channel,
builder: NotificationConstructor
): Notification {
val newNotification = NotificationCompat.Builder(context, channel.id)
builder(newNotification)
return newNotification.build()
}

View file

@ -1,17 +0,0 @@
package com.afollestad.nocknock.notifications;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View file

@ -0,0 +1,151 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.notifications
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.graphics.Bitmap
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.notifications.Channel.CheckFailures
import com.afollestad.nocknock.utilities.providers.BitmapProvider
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.NotificationProvider
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.BASE_NOTIFICATION_REQUEST_CODE
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import org.junit.Before
import org.junit.Test
class NockNotificationManagerTest {
private val appIconRes = 1024
private val somethingWentWrong = "something went wrong"
private val stockManager = mock<NotificationManager>()
private val appIcon = mock<Bitmap>()
private val bitmapProvider = mock<BitmapProvider> {
on { get(appIconRes) } doReturn appIcon
}
private val stringProvider = mock<StringProvider> {
on { get(R.string.something_wrong) } doReturn somethingWentWrong
}
private val intentProvider = mock<IntentProvider>()
private val channelProvider = mock<NotificationChannelProvider>()
private val notificationProvider = mock<NotificationProvider>()
private val manager = RealNockNotificationManager(
appIconRes,
stockManager,
bitmapProvider,
stringProvider,
intentProvider,
channelProvider,
notificationProvider
)
@Before fun setup() {
whenever(channelProvider.create(any(), any(), any(), any())).doAnswer { inv ->
val id = inv.getArgument<String>(0)
val title = inv.getArgument<String>(1)
val description = inv.getArgument<String>(2)
val important = inv.getArgument<Int>(3)
return@doAnswer mock<NotificationChannel> {
on { this.id } doReturn id
on { this.name } doReturn title
on { this.description } doReturn description
on { this.importance } doReturn important
}
}
}
@Test fun createChannels() {
whenever(stringProvider.get(any())).doReturn("")
val createdChannel = mock<NotificationChannel> {
on { this.id } doReturn CheckFailures.id
}
whenever(channelProvider.create(any(), any(), any(), any()))
.doReturn(createdChannel)
manager.createChannels()
val captor = argumentCaptor<NotificationChannel>()
verify(stockManager, times(1)).createNotificationChannel(captor.capture())
val channel = captor.allValues.single()
assertThat(channel.id).isEqualTo(CheckFailures.id)
verifyNoMoreInteractions(stockManager)
}
@Test fun postStatusNotification_appIsOpen() {
manager.setIsAppOpen(true)
manager.postStatusNotification(fakeModel())
verifyNoMoreInteractions(stockManager)
}
@Test fun postStatusNotification_appNotOpen() {
manager.setIsAppOpen(false)
val model = fakeModel()
val pendingIntent = mock<PendingIntent>()
whenever(intentProvider.getPendingIntentForViewSite(model))
.doReturn(pendingIntent)
val notification = mock<Notification>()
whenever(
notificationProvider.create(
CheckFailures.id,
model.name,
somethingWentWrong,
pendingIntent,
R.drawable.ic_notification,
appIcon
)
).doReturn(notification)
manager.postStatusNotification(model)
verify(stockManager).notify(
model.url,
BASE_NOTIFICATION_REQUEST_CODE + model.id,
notification
)
verifyNoMoreInteractions(stockManager)
}
@Test fun cancelStatusNotification() {
val model = fakeModel()
manager.cancelStatusNotification(model)
verify(stockManager).cancel(BASE_NOTIFICATION_REQUEST_CODE + model.id)
verifyNoMoreInteractions(stockManager)
}
@Test fun cancelStatusNotifications() {
manager.cancelStatusNotifications()
verify(stockManager).cancelAll()
verifyNoMoreInteractions(stockManager)
}
private fun fakeModel() = ServerModel(
id = 1,
url = "https://hello.com",
name = "Testing",
validationMode = STATUS_CODE
)
}

View file

@ -20,6 +20,7 @@ android {
dependencies {
implementation 'androidx.annotation:annotation:' + versions.androidx
implementation 'androidx.appcompat:appcompat:' + versions.androidx
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines

View file

@ -7,9 +7,15 @@ package com.afollestad.nocknock.utilities
import com.afollestad.nocknock.utilities.providers.BitmapProvider
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.NotificationProvider
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider
import com.afollestad.nocknock.utilities.providers.RealSdkProvider
import com.afollestad.nocknock.utilities.providers.RealStringProvider
import com.afollestad.nocknock.utilities.providers.SdkProvider
import com.afollestad.nocknock.utilities.providers.StringProvider
import dagger.Binds
import dagger.Module
@ -19,6 +25,12 @@ import javax.inject.Singleton
@Module
abstract class UtilitiesModule {
@Binds
@Singleton
abstract fun provideSdkProvider(
sdkProvider: RealSdkProvider
): SdkProvider
@Binds
@Singleton
abstract fun provideBitmapProvider(
@ -36,4 +48,16 @@ abstract class UtilitiesModule {
abstract fun provideIntentProvider(
intentProvider: RealIntentProvider
): IntentProvider
@Binds
@Singleton
abstract fun provideChannelProvider(
channelProvider: RealNotificationChannelProvider
): NotificationChannelProvider
@Binds
@Singleton
abstract fun provideNotificationProvider(
notificationProvider: RealNotificationProvider
): NotificationProvider
}

View file

@ -5,8 +5,6 @@
*/
package com.afollestad.nocknock.utilities.js
import android.util.Log
import com.afollestad.nocknock.utilities.BuildConfig
import org.mozilla.javascript.Context
import org.mozilla.javascript.EvaluatorException
import org.mozilla.javascript.Function
@ -62,9 +60,6 @@ object JavaScript {
}
}
log(
"Evaluated to $message ($success): $code"
)
return if (!success) message else null
} finally {
Context.exit()
@ -73,10 +68,4 @@ object JavaScript {
return e.message
}
}
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("JavaScript", message)
}
}
}

View file

@ -0,0 +1,45 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.providers
import android.annotation.TargetApi
import android.app.NotificationChannel
import android.os.Build.VERSION_CODES
import javax.inject.Inject
/** @author Aidan Follestad (@afollestad) */
interface NotificationChannelProvider {
/** @return null if the device doesn't have Android O. */
fun create(
id: String,
title: String,
description: String,
importance: Int
): NotificationChannel?
}
/** @author Aidan Follestad (@afollestad) */
class RealNotificationChannelProvider @Inject constructor(
private val sdkProvider: SdkProvider
) : NotificationChannelProvider {
@TargetApi(VERSION_CODES.O)
override fun create(
id: String,
title: String,
description: String,
importance: Int
): NotificationChannel? {
if (!sdkProvider.hasOreo()) {
return null
}
return NotificationChannel(id, title, importance)
.apply {
this.description = description
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.providers
import android.app.Application
import android.app.Notification
import android.app.PendingIntent
import android.graphics.Bitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
import javax.inject.Inject
/** @author Aidan Follestad (@afollestad) */
interface NotificationProvider {
fun create(
channelId: String,
title: String,
content: String,
intent: PendingIntent,
smallIcon: Int,
largeIcon: Bitmap
): Notification
}
/** @author Aidan Follestad (@afollestad) */
class RealNotificationProvider @Inject constructor(
private val app: Application
) : NotificationProvider {
override fun create(
channelId: String,
title: String,
content: String,
intent: PendingIntent,
smallIcon: Int,
largeIcon: Bitmap
): Notification {
return NotificationCompat.Builder(app, channelId)
.setContentTitle(title)
.setContentText(content)
.setContentIntent(intent)
.setSmallIcon(smallIcon)
.setLargeIcon(largeIcon)
.setAutoCancel(true)
.setDefaults(DEFAULT_VIBRATE)
.build()
}
}

View file

@ -0,0 +1,22 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.providers
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import javax.inject.Inject
/** @author Aidan Follestad (@afollestad) */
interface SdkProvider {
fun hasOreo(): Boolean
}
/** @author Aidan Follestad (@afollestad) */
class RealSdkProvider @Inject constructor() : SdkProvider {
override fun hasOreo() = SDK_INT >= O
}

View file

@ -1,11 +0,0 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.util
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
fun hasOreo() = SDK_INT >= O