WIP; but now I actually am starting to understand what's going on
This commit is contained in:
parent
a123edbd8e
commit
63fff52fcf
7 changed files with 197 additions and 62 deletions
|
@ -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
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:name=".app.Application"
|
||||
|
|
|
@ -42,9 +42,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
|||
.map { Pair(it.id, it.instant) }.toSet()
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun getSubscription(subscriptionId: Long): Subscription? {
|
||||
fun getSubscription(subscriptionId: Long): Subscription? {
|
||||
return toSubscription(subscriptionDao.get(subscriptionId))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package io.heckel.ntfy.msg
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class AttachmentDownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
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()
|
||||
private val notifier = NotificationService(context)
|
||||
|
||||
override fun doWork(): Result {
|
||||
if (context.applicationContext !is Application) return Result.failure()
|
||||
val notificationId = inputData.getString("id") ?: return Result.failure()
|
||||
val app = context.applicationContext as Application
|
||||
val notification = app.repository.getNotification(notificationId) ?: return Result.failure()
|
||||
val subscription = app.repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
||||
if (notification.attachmentPreviewUrl != null) {
|
||||
downloadPreview(subscription, notification)
|
||||
}
|
||||
downloadAttachment(subscription, notification)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun downloadPreview(subscription: Subscription, notification: Notification) {
|
||||
val url = notification.attachmentPreviewUrl ?: return
|
||||
Log.d(TAG, "Downloading preview 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("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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<String>,
|
||||
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()
|
||||
|
|
|
@ -33,4 +33,5 @@ ext {
|
|||
coreKtxVersion = '1.3.2'
|
||||
constraintLayoutVersion = '2.0.4'
|
||||
activityVersion = '1.1.0'
|
||||
fragmentVersion = '1.1.0'
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue