Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
77 lines
2.8 KiB
Plaintext
77 lines
2.8 KiB
Plaintext
package com.soulstream.transfernotice
|
|
|
|
import android.Manifest
|
|
import android.app.Activity
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.content.ContextCompat
|
|
import app.tauri.annotation.Command
|
|
import app.tauri.annotation.InvokeArg
|
|
import app.tauri.annotation.TauriPlugin
|
|
import app.tauri.plugin.Invoke
|
|
import app.tauri.plugin.JSObject
|
|
import app.tauri.plugin.Plugin
|
|
|
|
/** What the notice says — the Rust `TransferNotice`, camelCase. */
|
|
@InvokeArg
|
|
class NoticeArgs {
|
|
/** how many files are still in flight; zero means take the notice down */
|
|
var count: Int = 0
|
|
var title: String? = null
|
|
var line: String? = null
|
|
var percent: Int = 0
|
|
/** nobody has stated a size yet, so the bar has no end to walk towards */
|
|
var indeterminate: Boolean = false
|
|
}
|
|
|
|
/**
|
|
* The bridge between the WebView and the service that owns the notice.
|
|
* `update` mirrors what Save is fetching (and starts the service the first
|
|
* time); `clear` takes the notice down. Its buttons come back as the plugin
|
|
* event `action` with `{ action }` — "cancel" or "open".
|
|
*/
|
|
@TauriPlugin
|
|
class TransferNoticePlugin(private val activity: Activity) : Plugin(activity) {
|
|
private var askedForNotifications = false
|
|
|
|
init {
|
|
TransferHub.onAction = { action ->
|
|
val payload = JSObject()
|
|
payload.put("action", action)
|
|
trigger("action", payload)
|
|
}
|
|
/* the channel exists from app start, so a user can silence the
|
|
downloads on their own before the first one ever runs */
|
|
TransferHub.ensureChannel(activity.applicationContext)
|
|
}
|
|
|
|
@Command
|
|
fun update(invoke: Invoke) {
|
|
val args = invoke.parseArgs(NoticeArgs::class.java)
|
|
ensureNotificationPermission()
|
|
TransferHub.update(activity.applicationContext, args)
|
|
invoke.resolve()
|
|
}
|
|
|
|
@Command
|
|
fun clear(invoke: Invoke) {
|
|
TransferHub.clear(activity.applicationContext)
|
|
invoke.resolve()
|
|
}
|
|
|
|
/** Android 13+: the notice is a notification and needs the permission;
|
|
* asked once, the first time something is fetched. Its own request code
|
|
* — 4711 is the media card's, and two requests that share one would
|
|
* answer each other. The service runs either way; without the
|
|
* permission it simply has nothing to show. */
|
|
private fun ensureNotificationPermission() {
|
|
if (Build.VERSION.SDK_INT < 33 || askedForNotifications) return
|
|
askedForNotifications = true
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
|
|
== PackageManager.PERMISSION_GRANTED
|
|
) return
|
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4712)
|
|
}
|
|
}
|