Replace receive_sharing_intent; add background upload service; fix add-account login page
Sharing a large file to Noo used to black-screen and silently kick back to the home screen: receive_sharing_intent copies the entire shared file into the cache dir synchronously on the main thread before Flutter even renders, which Android's watchdog eventually kills. Replaced with hand-rolled handling in MainActivity.kt that only ever reads cheap Uri metadata up front (ShareIntentService), so the destination picker always appears instantly regardless of file size. The destination picker (ShareUploadView) now mirrors the Files tab's own controls/filters/listing instead of a bare folder list, and the actual prepare+upload is handed off to ShareUploadService.kt, a real Android foreground service with a single cancellable progress notification for the whole batch - this survives the app being closed right after the user confirms a destination, the same guarantee a file-manager app's own upload notification gives. Also fixes the "add another account" login page: it used to open in url_launcher's bare inAppWebView, which has no close button and can hide the status bar. LoginWebViewView is a real screen this app owns instead (package:webview_flutter), with a normal AppBar/close button/safe area, and can now auto-close itself on successful login. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- For local_auth's biometric app-lock (Settings > Security). -->
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||
<!-- For ShareUploadService, which keeps a "Share to Noo" upload going
|
||||
(and cancellable from its notification) even if the app is fully
|
||||
closed - see the service's own doc comment. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<application
|
||||
android:label="Noo"
|
||||
android:name="${applicationName}"
|
||||
@@ -31,7 +37,8 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- Lets other apps' share sheets ("Share to...") send a file here
|
||||
to be uploaded, via receive_sharing_intent. -->
|
||||
to be uploaded - handled by MainActivity.kt directly, not a
|
||||
plugin (see its doc comment for why). -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -43,6 +50,10 @@
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".ShareUploadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"/>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -1,8 +1,183 @@
|
||||
package dev.ayushya.noo
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
// FlutterFragmentActivity (not the default FlutterActivity) is required by
|
||||
// local_auth's Android implementation, which hosts its biometric/device
|
||||
// credential prompt via a Fragment.
|
||||
class MainActivity : FlutterFragmentActivity()
|
||||
//
|
||||
// Also hand-rolls "Share to Noo" intent handling instead of using the
|
||||
// receive_sharing_intent plugin: that plugin resolves a shared content:// URI
|
||||
// by synchronously copying the *entire* file into the app's cache dir on the
|
||||
// main thread, during activity startup, before Flutter has even drawn its
|
||||
// first frame - fine for a photo, but for a large video/archive it blocks
|
||||
// long enough that Android decides this newly-launched activity failed to
|
||||
// render and kills it (silently back to the home screen - no crash, no
|
||||
// Dart-side error, since Dart never got a chance to run). Other file-manager
|
||||
// apps show their destination picker instantly regardless of file size
|
||||
// because they defer touching the file's actual bytes until the user picks
|
||||
// a destination - `getInitialShare`/`onNewShare` only ever query cheap Uri
|
||||
// metadata (name/size/mime, not content). The actual copy+upload then runs
|
||||
// in ShareUploadService (see its own doc comment), not here, so it survives
|
||||
// the app being closed right after the user confirms a destination.
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
private val shareIntentChannelName = "dev.ayushya.noo/share_intent"
|
||||
private val newShareChannelName = "dev.ayushya.noo/share_intent/new"
|
||||
private val uploadServiceChannelName = "dev.ayushya.noo/upload_service"
|
||||
private val notificationPermissionRequestCode = 4202
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var newShareSink: EventChannel.EventSink? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, shareIntentChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getInitialShare" -> result.success(extractShareMetadata(intent))
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, newShareChannelName)
|
||||
.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
newShareSink = events
|
||||
}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
newShareSink = null
|
||||
}
|
||||
})
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, uploadServiceChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"startUpload" -> {
|
||||
startUploadService(call, result)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
val shared = extractShareMetadata(intent)
|
||||
if (shared.isNotEmpty()) {
|
||||
mainHandler.post { newShareSink?.success(shared) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Cheap Uri metadata only (display name / size / mime type) via a
|
||||
/// single ContentResolver query - never reads the file's actual bytes,
|
||||
/// so this is fast regardless of how large the shared file is.
|
||||
private fun extractShareMetadata(intent: Intent): List<Map<String, Any?>> {
|
||||
val uris: List<Uri> = when (intent.action) {
|
||||
Intent.ACTION_SEND -> listOfNotNull(intentStreamUri(intent))
|
||||
Intent.ACTION_SEND_MULTIPLE -> intentStreamUris(intent)
|
||||
else -> emptyList()
|
||||
}
|
||||
return uris.map { uriMetadata(it) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun intentStreamUri(intent: Intent): Uri? {
|
||||
return if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun intentStreamUris(intent: Intent): List<Uri> {
|
||||
val list = if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
}
|
||||
return list ?: emptyList()
|
||||
}
|
||||
|
||||
private fun uriMetadata(uri: Uri): Map<String, Any?> {
|
||||
var name = uri.lastPathSegment ?: "shared_file"
|
||||
var size: Long? = null
|
||||
try {
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIdx >= 0) cursor.getString(nameIdx)?.let { name = it }
|
||||
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
if (sizeIdx >= 0 && !cursor.isNull(sizeIdx)) size = cursor.getLong(sizeIdx)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Fall back to the Uri-derived name / unknown size below.
|
||||
}
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
return mapOf("uri" to uri.toString(), "name" to name, "mimeType" to mimeType, "size" to size)
|
||||
}
|
||||
|
||||
/// Starts ShareUploadService with everything it needs to run entirely on
|
||||
/// its own (see its doc comment) - the Intent extras are the full
|
||||
/// contract between this call and that service, kept in sync manually
|
||||
/// since they cross the Kotlin/Dart boundary as loosely-typed args.
|
||||
private fun startUploadService(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filesJson = call.argument<String>("files")
|
||||
val serverUrl = call.argument<String>("serverUrl")
|
||||
val username = call.argument<String>("username")
|
||||
val authHeader = call.argument<String>("authHeader")
|
||||
val remoteFolder = call.argument<String>("remoteFolder")
|
||||
if (filesJson == null || serverUrl == null || username == null ||
|
||||
authHeader == null || remoteFolder == null
|
||||
) {
|
||||
result.error("bad_args", "Missing required upload arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33 &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Fire-and-forget: the service works fine even if this is denied,
|
||||
// it just won't be able to show progress/cancel in a notification.
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
notificationPermissionRequestCode,
|
||||
)
|
||||
}
|
||||
|
||||
val serviceIntent = Intent(this, ShareUploadService::class.java).apply {
|
||||
putExtra(ShareUploadService.EXTRA_FILES, filesJson)
|
||||
putExtra(ShareUploadService.EXTRA_SERVER_URL, serverUrl)
|
||||
putExtra(ShareUploadService.EXTRA_USERNAME, username)
|
||||
putExtra(ShareUploadService.EXTRA_AUTH_HEADER, authHeader)
|
||||
putExtra(ShareUploadService.EXTRA_REMOTE_FOLDER, remoteFolder)
|
||||
}
|
||||
ContextCompat.startForegroundService(this, serviceIntent)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package dev.ayushya.noo
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Foreground service that prepares (copies from a content:// Uri) and
|
||||
* uploads one or more "Share to Noo" files over WebDAV, independent of
|
||||
* MainActivity/the Flutter engine being alive - the whole point is that
|
||||
* closing the app right after confirming a destination in ShareUploadView
|
||||
* doesn't interrupt the upload, the same way a file-manager app's own
|
||||
* upload notification survives the app being closed.
|
||||
*
|
||||
* Re-implements a plain WebDAV PUT here in Kotlin (HttpURLConnection, no
|
||||
* new HTTP dependency) rather than reusing NextcloudService/Dio from Dart,
|
||||
* since a Dart isolate doesn't keep running once the Flutter engine/
|
||||
* Activity are gone - only a real Android Service does. This does mean the
|
||||
* PUT request itself is duplicated logic (see NextcloudService.
|
||||
* uploadFileFromPath); keep both in sync if the upload semantics change.
|
||||
*
|
||||
* Started via the `dev.ayushya.noo/upload_service` MethodChannel
|
||||
* (MainActivity.kt) with credentials/destination passed as Intent extras -
|
||||
* never has an Activity in the loop after that. Shows one persistent,
|
||||
* cancellable notification for the whole batch; the Cancel action re-enters
|
||||
* this same running service instance with [ACTION_CANCEL], which the
|
||||
* copy/upload loops poll.
|
||||
*/
|
||||
class ShareUploadService : Service() {
|
||||
companion object {
|
||||
const val ACTION_CANCEL = "dev.ayushya.noo.action.CANCEL_UPLOAD"
|
||||
const val EXTRA_FILES = "files" // JSON array of {uri, name, size}
|
||||
const val EXTRA_SERVER_URL = "serverUrl"
|
||||
const val EXTRA_USERNAME = "username"
|
||||
const val EXTRA_AUTH_HEADER = "authHeader"
|
||||
const val EXTRA_REMOTE_FOLDER = "remoteFolder"
|
||||
|
||||
private const val CHANNEL_ID = "share_upload"
|
||||
private const val NOTIFICATION_ID = 4201
|
||||
}
|
||||
|
||||
private val cancelled = AtomicBoolean(false)
|
||||
private var uploadThread: Thread? = null
|
||||
|
||||
private data class ShareFile(val uri: String, val name: String, val size: Long?)
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL) {
|
||||
cancelled.set(true)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
val filesJson = intent?.getStringExtra(EXTRA_FILES)
|
||||
val serverUrl = intent?.getStringExtra(EXTRA_SERVER_URL)
|
||||
val username = intent?.getStringExtra(EXTRA_USERNAME)
|
||||
val authHeader = intent?.getStringExtra(EXTRA_AUTH_HEADER)
|
||||
val remoteFolder = intent?.getStringExtra(EXTRA_REMOTE_FOLDER)
|
||||
if (filesJson == null || serverUrl == null || username == null ||
|
||||
authHeader == null || remoteFolder == null
|
||||
) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
createNotificationChannel()
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NOTIFICATION_ID,
|
||||
buildProgressNotification("Preparing to upload…", null, indeterminate = true),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
|
||||
// A second share arriving mid-upload is dropped rather than queued -
|
||||
// rare in practice (sharing again before the first batch finishes),
|
||||
// not worth a real queue for.
|
||||
if (uploadThread == null) {
|
||||
val files = parseFiles(filesJson)
|
||||
uploadThread = Thread {
|
||||
runUploads(files, serverUrl, username, authHeader, remoteFolder)
|
||||
}.also { it.start() }
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun parseFiles(json: String): List<ShareFile> {
|
||||
val arr = JSONArray(json)
|
||||
return (0 until arr.length()).map { i ->
|
||||
val obj = arr.getJSONObject(i)
|
||||
ShareFile(
|
||||
uri = obj.getString("uri"),
|
||||
name = obj.getString("name"),
|
||||
size = if (obj.isNull("size")) null else obj.getLong("size"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runUploads(
|
||||
files: List<ShareFile>,
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
authHeader: String,
|
||||
remoteFolder: String,
|
||||
) {
|
||||
var succeeded = 0
|
||||
var failed = 0
|
||||
val cleanServer = serverUrl.trimEnd('/')
|
||||
var cleanFolder = remoteFolder.trim()
|
||||
if (!cleanFolder.startsWith("/")) cleanFolder = "/$cleanFolder"
|
||||
if (!cleanFolder.endsWith("/")) cleanFolder = "$cleanFolder/"
|
||||
|
||||
for ((index, file) in files.withIndex()) {
|
||||
if (cancelled.get()) break
|
||||
val label = if (files.size == 1) file.name else "${file.name} (${index + 1}/${files.size})"
|
||||
var tempFile: File? = null
|
||||
try {
|
||||
notify(buildProgressNotification("Preparing $label…", null, indeterminate = true))
|
||||
tempFile = materialize(file) { sent, total ->
|
||||
notify(
|
||||
buildProgressNotification(
|
||||
"Preparing $label…",
|
||||
progressFraction(sent, total),
|
||||
indeterminate = total == null,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (cancelled.get()) break
|
||||
|
||||
val encodedName = Uri.encode(file.name)
|
||||
val url = URL("$cleanServer/remote.php/dav/files/$username$cleanFolder$encodedName")
|
||||
val ok = uploadFile(tempFile, url, authHeader) { sent, total ->
|
||||
notify(
|
||||
buildProgressNotification(
|
||||
"Uploading $label…",
|
||||
progressFraction(sent, total),
|
||||
indeterminate = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (ok) succeeded++ else failed++
|
||||
} catch (e: Exception) {
|
||||
failed++
|
||||
} finally {
|
||||
tempFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val finalText = when {
|
||||
cancelled.get() -> "Upload cancelled"
|
||||
failed == 0 && files.size == 1 -> "Uploaded ${files.first().name}"
|
||||
failed == 0 -> "Uploaded $succeeded of ${files.size} files"
|
||||
else -> "Uploaded $succeeded of ${files.size} files - $failed failed"
|
||||
}
|
||||
manager.notify(NOTIFICATION_ID, buildFinalNotification(finalText))
|
||||
|
||||
stopForeground(STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun progressFraction(sent: Long, total: Long?): Float? {
|
||||
if (total == null || total <= 0) return null
|
||||
return (sent.toFloat() / total.toFloat()).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
private fun materialize(file: ShareFile, onProgress: (Long, Long?) -> Unit): File {
|
||||
val uri = Uri.parse(file.uri)
|
||||
val target = File(cacheDir, "share_upload_${System.currentTimeMillis()}_${file.name}")
|
||||
contentResolver.openInputStream(uri)?.use { input ->
|
||||
FileOutputStream(target).use { output ->
|
||||
val buffer = ByteArray(256 * 1024)
|
||||
var sent = 0L
|
||||
var lastEmit = 0L
|
||||
while (!cancelled.get()) {
|
||||
val read = input.read(buffer)
|
||||
if (read == -1) break
|
||||
output.write(buffer, 0, read)
|
||||
sent += read
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastEmit >= 200) {
|
||||
lastEmit = now
|
||||
onProgress(sent, file.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalStateException("Could not open ${file.uri}")
|
||||
return target
|
||||
}
|
||||
|
||||
private fun uploadFile(
|
||||
file: File,
|
||||
url: URL,
|
||||
authHeader: String,
|
||||
onProgress: (Long, Long) -> Unit,
|
||||
): Boolean {
|
||||
val length = file.length()
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
return try {
|
||||
connection.requestMethod = "PUT"
|
||||
connection.doOutput = true
|
||||
connection.setFixedLengthStreamingMode(length)
|
||||
connection.setRequestProperty("Authorization", authHeader)
|
||||
connection.setRequestProperty("OCS-APIRequest", "true")
|
||||
connection.setRequestProperty("Content-Type", "application/octet-stream")
|
||||
connection.connectTimeout = 15000
|
||||
connection.readTimeout = 30000
|
||||
|
||||
connection.outputStream.use { output ->
|
||||
file.inputStream().use { input ->
|
||||
val buffer = ByteArray(256 * 1024)
|
||||
var sent = 0L
|
||||
var lastEmit = 0L
|
||||
while (!cancelled.get()) {
|
||||
val read = input.read(buffer)
|
||||
if (read == -1) break
|
||||
output.write(buffer, 0, read)
|
||||
sent += read
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastEmit >= 200) {
|
||||
lastEmit = now
|
||||
onProgress(sent, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cancelled.get()) {
|
||||
false
|
||||
} else {
|
||||
val code = connection.responseCode
|
||||
code == 201 || code == 204 || code == 200
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(notification: Notification) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"File uploads",
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply { description = "Progress for files shared to Noo" }
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildProgressNotification(text: String, progress: Float?, indeterminate: Boolean): Notification {
|
||||
val cancelIntent = Intent(this, ShareUploadService::class.java).apply { action = ACTION_CANCEL }
|
||||
val cancelPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
cancelIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload)
|
||||
.setContentTitle("Uploading to Noo")
|
||||
.setContentText(text)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Cancel", cancelPendingIntent)
|
||||
when {
|
||||
indeterminate -> builder.setProgress(100, 0, true)
|
||||
progress != null -> builder.setProgress(100, (progress * 100).toInt(), false)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildFinalNotification(text: String): Notification {
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload_done)
|
||||
.setContentTitle("Noo")
|
||||
.setContentText(text)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancelled.set(true)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user