Loading app/build.gradle +6 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,8 @@ android { } dependencies { def workManagerVersion = "2.5.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion" implementation "androidx.core:core-ktx:$rootProject.coreKtxVersion" Loading @@ -55,6 +57,10 @@ dependencies { implementation "androidx.activity:activity-ktx:$rootProject.activityVersion" implementation 'com.google.code.gson:gson:2.8.8' // WorkManager implementation("androidx.work:work-runtime:$workManagerVersion") implementation("androidx.work:work-runtime-ktx:$workManagerVersion") // RecyclerView implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion" Loading app/src/main/java/io/heckel/ntfy/data/ConnectionWorker.kt 0 → 100644 +107 −0 Original line number Diff line number Diff line package io.heckel.ntfy.data import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.google.gson.GsonBuilder import com.google.gson.JsonObject import io.heckel.ntfy.R import kotlinx.coroutines.* import java.net.HttpURLConnection import java.net.URL import kotlin.random.Random class ConnectionWorker(val ctx: Context, workerParams: WorkerParameters) : CoroutineWorker(ctx, workerParams) { private val gson = GsonBuilder().create() override suspend fun doWork(): Result { println("PHIL work started") while (isStopped) { openConnection(Random.nextLong(), "https://ntfy.sh/test/json") } println("PHIL work ended") // Indicate whether the work finished successfully with the Result return Result.success() } private fun openConnection(subscriptionId: Long, topicUrl: String) { println("Connecting to $topicUrl ...") val conn = (URL(topicUrl).openConnection() as HttpURLConnection).also { it.doInput = true it.readTimeout = READ_TIMEOUT } try { println("PHIL connected") val input = conn.inputStream.bufferedReader() while (isStopped) { val line = input.readLine() ?: break // Break if EOF is reached, i.e. readLine is null val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line val validNotification = !json.isJsonNull && !json.has("event") // No keepalive or open messages && json.has("message") if (validNotification) { val title = "ntfy.sh/test" val message = json.get("message").asString displayNotification(title, message) println("notification received: ${json.get("message").asString}") } } } catch (e: Exception) { println("Connection error: " + e) } finally { conn.disconnect() } println("Connection terminated: $topicUrl") } private fun displayNotification(title: String, message: String) { val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channelId = ctx.getString(R.string.notification_channel_id) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = ctx.getString(R.string.notification_channel_name) val descriptionText = ctx.getString(R.string.notification_channel_name) val importance = NotificationManager.IMPORTANCE_DEFAULT val channel = NotificationChannel(channelId, name, importance).apply { description = descriptionText } notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(ctx, channelId) .setSmallIcon(R.drawable.ntfy) .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() notificationManager.notify(Random.nextInt(), notification) } /** * Create the NotificationChannel, but only on API 26+ because * the NotificationChannel class is new and not in the support library */ private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channelId = ctx.getString(R.string.notification_channel_id) val name = ctx.getString(R.string.notification_channel_name) val descriptionText = ctx.getString(R.string.notification_channel_name) val importance = NotificationManager.IMPORTANCE_DEFAULT val channel = NotificationChannel(channelId, name, importance).apply { description = descriptionText } // Register the channel with the system val notificationManager: NotificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } } app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +20 −8 Original line number Diff line number Diff line Loading @@ -15,17 +15,18 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.recyclerview.widget.RecyclerView import androidx.work.* import com.google.gson.GsonBuilder import com.google.gson.JsonObject import io.heckel.ntfy.R import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Status import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.topicShortUrl import io.heckel.ntfy.data.* import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.TimeUnit import kotlin.random.Random const val SUBSCRIPTION_ID = "topic_id" class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { private val uniqueWorkName = "connectionWorker" private val subscriptionsViewModel by viewModels<SubscriptionsViewModel> { SubscriptionsViewModelFactory() } Loading Loading @@ -76,7 +77,8 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_action_source -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url)))) // startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url)))) enqueueConnectionWorker() true } R.id.menu_action_website -> { Loading @@ -101,6 +103,15 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { subscriptionsViewModel.add(subscription) } private fun enqueueConnectionWorker() { val workRequest = PeriodicWorkRequestBuilder<ConnectionWorker>(1, TimeUnit.MINUTES) .build() WorkManager .getInstance(this) .enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest) } private fun displayNotification(n: Notification) { val channelId = getString(R.string.notification_channel_id) val notification = NotificationCompat.Builder(this, channelId) Loading Loading @@ -132,3 +143,4 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { } } } Loading
app/build.gradle +6 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,8 @@ android { } dependencies { def workManagerVersion = "2.5.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion" implementation "androidx.core:core-ktx:$rootProject.coreKtxVersion" Loading @@ -55,6 +57,10 @@ dependencies { implementation "androidx.activity:activity-ktx:$rootProject.activityVersion" implementation 'com.google.code.gson:gson:2.8.8' // WorkManager implementation("androidx.work:work-runtime:$workManagerVersion") implementation("androidx.work:work-runtime-ktx:$workManagerVersion") // RecyclerView implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion" Loading
app/src/main/java/io/heckel/ntfy/data/ConnectionWorker.kt 0 → 100644 +107 −0 Original line number Diff line number Diff line package io.heckel.ntfy.data import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.google.gson.GsonBuilder import com.google.gson.JsonObject import io.heckel.ntfy.R import kotlinx.coroutines.* import java.net.HttpURLConnection import java.net.URL import kotlin.random.Random class ConnectionWorker(val ctx: Context, workerParams: WorkerParameters) : CoroutineWorker(ctx, workerParams) { private val gson = GsonBuilder().create() override suspend fun doWork(): Result { println("PHIL work started") while (isStopped) { openConnection(Random.nextLong(), "https://ntfy.sh/test/json") } println("PHIL work ended") // Indicate whether the work finished successfully with the Result return Result.success() } private fun openConnection(subscriptionId: Long, topicUrl: String) { println("Connecting to $topicUrl ...") val conn = (URL(topicUrl).openConnection() as HttpURLConnection).also { it.doInput = true it.readTimeout = READ_TIMEOUT } try { println("PHIL connected") val input = conn.inputStream.bufferedReader() while (isStopped) { val line = input.readLine() ?: break // Break if EOF is reached, i.e. readLine is null val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line val validNotification = !json.isJsonNull && !json.has("event") // No keepalive or open messages && json.has("message") if (validNotification) { val title = "ntfy.sh/test" val message = json.get("message").asString displayNotification(title, message) println("notification received: ${json.get("message").asString}") } } } catch (e: Exception) { println("Connection error: " + e) } finally { conn.disconnect() } println("Connection terminated: $topicUrl") } private fun displayNotification(title: String, message: String) { val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channelId = ctx.getString(R.string.notification_channel_id) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = ctx.getString(R.string.notification_channel_name) val descriptionText = ctx.getString(R.string.notification_channel_name) val importance = NotificationManager.IMPORTANCE_DEFAULT val channel = NotificationChannel(channelId, name, importance).apply { description = descriptionText } notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(ctx, channelId) .setSmallIcon(R.drawable.ntfy) .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() notificationManager.notify(Random.nextInt(), notification) } /** * Create the NotificationChannel, but only on API 26+ because * the NotificationChannel class is new and not in the support library */ private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channelId = ctx.getString(R.string.notification_channel_id) val name = ctx.getString(R.string.notification_channel_name) val descriptionText = ctx.getString(R.string.notification_channel_name) val importance = NotificationManager.IMPORTANCE_DEFAULT val channel = NotificationChannel(channelId, name, importance).apply { description = descriptionText } // Register the channel with the system val notificationManager: NotificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } }
app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +20 −8 Original line number Diff line number Diff line Loading @@ -15,17 +15,18 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.recyclerview.widget.RecyclerView import androidx.work.* import com.google.gson.GsonBuilder import com.google.gson.JsonObject import io.heckel.ntfy.R import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Status import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.topicShortUrl import io.heckel.ntfy.data.* import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.TimeUnit import kotlin.random.Random const val SUBSCRIPTION_ID = "topic_id" class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { private val uniqueWorkName = "connectionWorker" private val subscriptionsViewModel by viewModels<SubscriptionsViewModel> { SubscriptionsViewModelFactory() } Loading Loading @@ -76,7 +77,8 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_action_source -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url)))) // startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url)))) enqueueConnectionWorker() true } R.id.menu_action_website -> { Loading @@ -101,6 +103,15 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { subscriptionsViewModel.add(subscription) } private fun enqueueConnectionWorker() { val workRequest = PeriodicWorkRequestBuilder<ConnectionWorker>(1, TimeUnit.MINUTES) .build() WorkManager .getInstance(this) .enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest) } private fun displayNotification(n: Notification) { val channelId = getString(R.string.notification_channel_id) val notification = NotificationCompat.Builder(this, channelId) Loading Loading @@ -132,3 +143,4 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { } } }