diff --git a/app/build.gradle b/app/build.gradle index 530058f..67da056 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" @@ -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" diff --git a/app/src/main/java/io/heckel/ntfy/data/ConnectionWorker.kt b/app/src/main/java/io/heckel/ntfy/data/ConnectionWorker.kt new file mode 100644 index 0000000..521d3c5 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/data/ConnectionWorker.kt @@ -0,0 +1,107 @@ +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) + } + } + +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index cdbcf47..a8506c1 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -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 { SubscriptionsViewModelFactory() } @@ -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 -> { @@ -101,6 +103,15 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { subscriptionsViewModel.add(subscription) } + private fun enqueueConnectionWorker() { + val workRequest = + PeriodicWorkRequestBuilder(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) @@ -132,3 +143,4 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { } } } +