diff --git a/app/build.gradle b/app/build.gradle
index 63498e4..2b1b457 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -63,6 +63,7 @@ dependencies {
implementation "androidx.core:core-ktx:$rootProject.coreKtxVersion"
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
+ implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion"
implementation 'com.google.code.gson:gson:2.8.8'
// WorkManager
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bffc978..bd12a74 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,6 +13,7 @@
+
+ if (!response.isSuccessful || response.body == null) {
+ throw Exception("Preview download failed: ${response.code}")
+ }
+ Log.d(TAG, "Preview download: successful response, proceeding with download")
+ /*val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
+ Log.d(TAG, "dir: $dir")
+ if (dir == null /*|| !dir.mkdirs()*/) {
+ throw Exception("Cannot access target storage dir")
+ }*/
+ val contentResolver = applicationContext.contentResolver
+ val contentValues = ContentValues()
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "flower.jpg")
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+ val uri = contentResolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), contentValues)
+ ?: throw Exception("Cannot get content URI")
+ val out = contentResolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
+ out.use { fileOut ->
+ response.body!!.byteStream().copyTo(fileOut)
+ }
+
+ /*
+
+ val file = File(context.cacheDir, "somefile")
+ context.openFileOutput(file.absolutePath, Context.MODE_PRIVATE).use { fileOut ->
+ response.body!!.byteStream().copyTo(fileOut)
+ }
+
+ val file = File(dir, "myfile.txt")
+ Log.d(TAG, "dir: $dir, file: $file")
+ FileOutputStream(file).use { fileOut ->
+ response.body!!.byteStream().copyTo(fileOut)
+ }*/
+ /*
+ context.openFileOutput(file.absolutePath, Context.MODE_PRIVATE).use { fileOut ->
+ response.body!!.byteStream().copyTo(fileOut)
+ }*/
+ //val bitmap = BitmapFactory.decodeFile(file.absolutePath)
+ Log.d(TAG, "now we would display the preview image")
+ //displayInternal(subscription, notification, bitmap)
+ }
+ }
+
+ private fun downloadAttachment(subscription: Subscription, notification: Notification) {
+ val url = notification.attachmentUrl ?: return
+ Log.d(TAG, "Downloading attachment from $url")
+
+ val request = Request.Builder()
+ .url(url)
+ .addHeader("User-Agent", ApiService.USER_AGENT)
+ .build()
+ client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful || response.body == null) {
+ throw Exception("Attachment download failed: ${response.code}")
+ }
+ val name = notification.attachmentName ?: "attachment.bin"
+ val mimeType = notification.attachmentType ?: "application/octet-stream"
+ val resolver = applicationContext.contentResolver
+ val details = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+ put(MediaStore.MediaColumns.IS_DOWNLOAD, 1)
+ }
+ val uri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), details)
+ ?: throw Exception("Cannot get content URI")
+ Log.d(TAG, "Starting download to content URI: $uri")
+ val out = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
+ out.use { fileOut ->
+ response.body!!.byteStream().copyTo(fileOut)
+ }
+ Log.d(TAG, "Attachment download: successful response, proceeding with download")
+ notifier.update(subscription, notification)
+ }
+ }
+
+ companion object {
+ private const val TAG = "NtfyAttachDownload"
+ }
+}
diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
index e8f525c..e9e2314 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -7,14 +7,15 @@ import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
-import android.graphics.BitmapFactory
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
-import android.os.Environment
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import androidx.work.workDataOf
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
@@ -22,28 +23,27 @@ import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.formatMessage
import io.heckel.ntfy.util.formatTitle
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import java.io.File
-import java.util.concurrent.TimeUnit
class NotificationService(val context: Context) {
- private val client = OkHttpClient.Builder()
- .callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request
- .connectTimeout(15, TimeUnit.SECONDS)
- .readTimeout(15, TimeUnit.SECONDS)
- .writeTimeout(15, TimeUnit.SECONDS)
- .build()
-
fun display(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Displaying notification $notification")
+ // Display notification immediately
displayInternal(subscription, notification)
- if (notification.attachmentPreviewUrl != null) {
- downloadPreviewAndUpdate(subscription, notification)
+
+ // Download attachment (+ preview if available) in the background via WorkManager
+ // The indirection via WorkManager is required since this code may be executed
+ // in a doze state and Internet may not be available. It's also best practice apparently.
+ if (notification.attachmentUrl != null) {
+ scheduleAttachmentDownload(subscription, notification)
}
}
+ fun update(subscription: Subscription, notification: Notification) {
+ Log.d(TAG, "Updating notification $notification")
+ displayInternal(subscription, notification)
+ }
+
fun cancel(notification: Notification) {
if (notification.notificationId != 0) {
Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}")
@@ -73,8 +73,9 @@ class NotificationService(val context: Context) {
if (notification.attachmentUrl != null) {
val viewIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse(notification.attachmentUrl)), 0)
+ val openIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse("content://media/external/file/39")), 0)
notificationBuilder
- .addAction(NotificationCompat.Action.Builder(0, "Open", viewIntent).build())
+ .addAction(NotificationCompat.Action.Builder(0, "Open", openIntent).build())
.addAction(NotificationCompat.Action.Builder(0, "Copy URL", viewIntent).build())
.addAction(NotificationCompat.Action.Builder(0, "Download", viewIntent).build())
}
@@ -93,23 +94,15 @@ class NotificationService(val context: Context) {
notificationManager.notify(notification.notificationId, notificationBuilder.build())
}
- private fun downloadPreviewAndUpdate(subscription: Subscription, notification: Notification) {
- val previewUrl = notification.attachmentPreviewUrl ?: return
- Log.d(TAG, "Downloading preview image $previewUrl")
-
- val request = Request.Builder()
- .url(previewUrl)
- .addHeader("User-Agent", ApiService.USER_AGENT)
+ private fun scheduleAttachmentDownload(subscription: Subscription, notification: Notification) {
+ Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)")
+ val workManager = WorkManager.getInstance(context)
+ val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
+ .setInputData(workDataOf(
+ "id" to notification.id,
+ ))
.build()
- client.newCall(request).execute().use { response ->
- if (!response.isSuccessful || response.body == null) {
- Log.d(TAG, "Preview response failed: ${response.code}")
- } else {
- Log.d(TAG, "Successful response, streaming preview")
- val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
- displayInternal(subscription, notification, bitmap)
- }
- }
+ workManager.enqueue(workRequest)
}
private fun setContentIntent(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification): NotificationCompat.Builder? {
@@ -125,32 +118,6 @@ class NotificationService(val context: Context) {
}
}
- private fun downloadPreviewAndUpdateXXX(subscription: Subscription, notification: Notification) {
- val url = notification.attachmentUrl ?: return
- Log.d(TAG, "Downloading attachment from $url")
-
- val request = Request.Builder()
- .url(url)
- .addHeader("User-Agent", ApiService.USER_AGENT)
- .build()
- client.newCall(request).execute().use { response ->
- if (!response.isSuccessful || response.body == null) {
- Log.d(TAG, "Attachment download failed: ${response.code}")
- } else {
- Log.d(TAG, "Successful response")
- /*val filename = notification.id
- val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS + "/ntfy/" + notification.id)
- context.openFileOutput(filename, Context.MODE_PRIVATE).use {
- response.body!!.byteStream()
- }*/
- // TODO work manager
-
- val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
- displayInternal(subscription, notification, bitmap)
- }
- }
- }
-
private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
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 28c381c..5979d39 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -1,9 +1,11 @@
package io.heckel.ntfy.ui
+import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog
import android.content.Intent
+import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
@@ -11,6 +13,7 @@ import android.view.*
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
@@ -114,8 +117,44 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Background things
startPeriodicPollWorker()
startPeriodicServiceRestartWorker()
- }
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1234);
+ }
+ else {
+ Toast.makeText(this, "Permission already granted", Toast.LENGTH_SHORT).show();
+ }
+ }
+ override fun onRequestPermissionsResult(requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == 1234) { // FIXME
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this@MainActivity, "Camera Permission Granted", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(this@MainActivity, "Camera Permission Denied", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+/*
+ public static final int REQUEST_WRITE_STORAGE = 112;
+
+ fun requestPermission(Activity context) {
+ boolean hasPermission = (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
+ if (!hasPermission) {
+ ActivityCompat.requestPermissions(context,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ REQUEST_WRITE_STORAGE);
+ } else {
+ // You are allowed to write external storage:
+ String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/new_folder";
+ File storageDir = new File(path);
+ if (!storageDir.exists() && !storageDir.mkdirs()) {
+ // This should never happen - log handled exception!
+ }
+ }
+*/
override fun onResume() {
super.onResume()
showHideNotificationMenuItems()
diff --git a/build.gradle b/build.gradle
index 5e9e565..3df7b17 100644
--- a/build.gradle
+++ b/build.gradle
@@ -33,4 +33,5 @@ ext {
coreKtxVersion = '1.3.2'
constraintLayoutVersion = '2.0.4'
activityVersion = '1.1.0'
+ fragmentVersion = '1.1.0'
}