diff options
25 files changed, 132 insertions, 40 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 5412d16..8d14e37 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -3430,9 +3430,10 @@ photos, in the same `YYYY/YYYY-MM` folders, under the same ledger, rules and unmetered network. `READ_MEDIA_VIDEO` is asked for when they are ticked in. The confirmation states the videos' count and size apart, because they are most of it. A file is never whole in the page: the upload reads it a chunk at -a time and each chunk is a ranged request to the phone (`Range: bytes=a-b`, -answered 206 by `ByteRange.kt`), so a video of gigabytes costs the WebView one -chunk of memory; the SHA-256 the ledger keeps is then read from the phone once +a time and each chunk is a ranged request to the phone (`?range=a-b` in the +address, served by `ByteRange.kt`; not a `Range` header, which the WebView +does not hand to the application that answers, found on a Pixel 9), so a +video of gigabytes costs the WebView one chunk of memory; the SHA-256 the ledger keeps is then read from the phone once the node has the file. The Photos tab shows them in their albums (§9.9). **A manifest, for a restore or a merge later.** The node keeps the files; only diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js index 22fbe3d..a07399f 100644 --- a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js +++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js @@ -238,6 +238,7 @@ sent: (token, dir, name) => call('contactsync:sent', token, dir, name), completed: () => call('contactsync:completed'), failed: (code, text) => call('contactsync:failed', code, text), + openSettings: () => call('contactsync:open-settings'), }, // Calendar backup (§9.13), the same way: one iCalendar file of every @@ -250,6 +251,7 @@ sent: (token, dir, name) => call('calendarsync:sent', token, dir, name), completed: () => call('calendarsync:completed'), failed: (code, text) => call('calendarsync:failed', code, text), + openSettings: () => call('calendarsync:open-settings'), }, // Files backup (§9.13): folders the person chose through the system's @@ -295,6 +297,7 @@ sent: (token, dir, name) => call('messagesync:sent', token, dir, name), completed: () => call('messagesync:completed'), failed: (code, text) => call('messagesync:failed', code, text), + openSettings: () => call('messagesync:open-settings'), } } : {}), // Where downloads go, chosen once. A display name comes back, never a URI. diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt index 5cf06fc..699e0c3 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt @@ -187,13 +187,11 @@ class MainActivity : Activity() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { val url = request.url if (url.host == UiAssets.HOST && url.path?.startsWith(PhotoChannels.PATH) == true) { - val range = request.requestHeaders.entries.firstOrNull { it.key.equals("Range", true) }?.value - return photos.takeIf { backups }?.serve(url.path ?: "", range) ?: refused() + return photos.takeIf { backups }?.serve(url.path ?: "", rangeOf(request)) ?: refused() } val driveKind = drives.firstOrNull { url.path?.startsWith(it.path) == true } if (url.host == UiAssets.HOST && driveKind != null) { - val range = request.requestHeaders.entries.firstOrNull { it.key.equals("Range", true) }?.value - return driveKind.serve(url.path ?: "", range) ?: refused() + return driveKind.serve(url.path ?: "", rangeOf(request)) ?: refused() } if (url.host == UiAssets.HOST && url.path?.startsWith(DocChannels.PATH) == true) { val path = url.path ?: "" @@ -383,6 +381,13 @@ class MainActivity : Activity() { super.onRequestPermissionsResult(requestCode, permissions, grantResults) } + /** + * The slice a backup asks for, `?range=a-b` (platform.js): the WebView does + * not hand a fetch's Range header over to the application that answers. + */ + private fun rangeOf(request: WebResourceRequest): String? = + request.url.getQueryParameter("range")?.let { "bytes=$it" } + private fun hasAsset(path: String) = try { assets.open(path).close(); true } catch (e: java.io.IOException) { false } private fun refused() = WebResourceResponse("text/plain", "utf-8", 403, "Forbidden", emptyMap(), "".byteInputStream()) diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt index 2efe558..f4da64f 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt @@ -64,6 +64,7 @@ class DocChannels( "sent" -> { sent(args.optString(0, ""), args.optString(1, ""), args.optString(2, "")); true } "completed" -> { completed(); status() } "failed" -> failed(args.optString(0, ""), args.optString(1, "")) + "open-settings" -> { openSettings(); true } else -> throw Refused("Refused: no such channel") } @@ -167,6 +168,19 @@ class DocChannels( permission = null } + /** + * The application's page in the system's settings. Android keeps some + * permissions (text messages) behind "restricted settings" for an + * application installed outside a store; that page is where the person + * lifts it (⋮ > Allow restricted settings), and nowhere in here can. + */ + private fun openSettings() { + val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + android.net.Uri.fromParts("package", activity.packageName, null)) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + activity.runOnUiThread { activity.startActivity(intent) } + } + /** From Activity.onRequestPermissionsResult; true when the request was ours. */ fun deliverPermission(requestCode: Int): Boolean { if (requestCode != permissionRequest) return false diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt index 2e7c519..c1de859 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt @@ -135,9 +135,11 @@ class PhotoChannels( if (destinations.get() == null) throw Refused("Refused: no destination") val next = SyncConfig.fromJson(o, System.currentTimeMillis(), previous) val edit = prefs.edit().putString(CONFIG, next.toJson().toString()) - // A different scope is a different backup: due at once, and whatever - // the last one was refused for is not this one's problem. - if (previous == null || previous.since != next.since) { + // Anything that asks for more (another scope, an album, the videos) + // is due at once rather than tomorrow: the person just asked for it, + // and waits to see it go. Whatever the last run was refused for is + // not this one's problem. + if (previous == null || previous != next) { edit.remove(LAST).remove(FAILURE).remove(FAILURE_AT).remove(NOTIFIED) } edit.apply() @@ -367,7 +369,7 @@ class PhotoChannels( .setContentIntent(pending).setAutoCancel(true).build()) } - /** `range` of `stream`, `size` bytes long, as a 206 (or a 416 for a range that cannot be served). */ + /** `range` of `stream`, `size` bytes long (or a 416 for a range that cannot be served). */ fun serveRange(stream: InputStream, size: Long, mime: String, range: String?): WebResourceResponse { val r = ByteRange.parse(range, size) ?: run { stream.close() @@ -377,7 +379,9 @@ class PhotoChannels( val headers = mapOf("Cache-Control" to "no-store", "X-Content-Type-Options" to "nosniff", "Content-Range" to "bytes ${r.first}-${r.last}/$size", "Content-Length" to (r.last - r.first + 1).toString()) - return WebResourceResponse(mime, null, 206, "Partial Content", headers, ByteRange.slice(stream, r)) + // 200, not 206: what was asked is in the address, and a 206 to a + // request that carried no Range is not something to rely on. + return WebResourceResponse(mime, null, 200, "OK", headers, ByteRange.slice(stream, r)) } fun digestOf(s: InputStream): ByteArray { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js index 631be50..9fac9e4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js @@ -44,11 +44,13 @@ export function AndroidSyncPage({ user, groups }) { const moved = (next) => { setDest(next); - // Each kind that is on starts again in the new place, now. - if (platform.photoSync.available) runPhotoSync(); - for (const kind of Object.keys(KINDS)) if (KINDS[kind].sync.available) runDocSync(kind); - if (platform.driveSync.available) runDriveSync(); - if (platform.whatsappSync.available) runWhatsAppSync(); + // Each kind that is on starts again in the new place, now: forced, as a + // run asked for by a change, not left to the 15-minute retry spacing. + const now = { force: true }; + if (platform.photoSync.available) runPhotoSync(now); + for (const kind of Object.keys(KINDS)) if (KINDS[kind].sync.available) runDocSync(kind, now); + if (platform.driveSync.available) runDriveSync(now); + if (platform.whatsappSync.available) runWhatsAppSync(now); }; return html` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js index 3fbc65e..8fa9a3e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js @@ -88,7 +88,7 @@ export function DriveSyncSection({ destination }) { <button class="admin-btn" disabled=${busy || !folders.length} onClick=${() => act(async () => { const next = await sync.configure({}); - runDriveSync(); + runDriveSync({ force: true }); return next; })}>${t('phonesync.turn_on')}</button> </div> diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index f61f163..17b8c2b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -1318,6 +1318,8 @@ export default { 'photosync.albums': "Alben auf diesem Telefon", 'photosync.album_camera': "Kamera", 'photosync.album_count': "{n} Fotos · {size}", + 'photosync.videos_not_included': "Videos werden nicht gesichert: „Ändern“, um sie hinzuzufügen.", + 'photosync.videos_included': "Videos werden ebenfalls gesichert.", 'photosync.include_videos': "Auch Videos", 'photosync.include_videos_hint': "Videos sind groß: Sie werden wie Fotos über WLAN gesendet, und die erste Sicherung kann dauern.", 'photosync.album_count_videos': "{n} Fotos, {v} Videos · {size}", @@ -1356,6 +1358,7 @@ export default { 'messagesync.heading': "SMS-Sicherung", 'messagesync.intro': "Die SMS dieses Telefons einmal täglich senden: Die erste Kopie enthält alle, jede weitere nur die neuen. Die Dateien sind im XML-Format, das SMS-Sicherungs-Apps wiederherstellen können. MMS sind nicht enthalten.", 'messagesync.permission_needed': "MeshBay braucht Zugriff auf die SMS dieses Telefons.", + 'messagesync.restricted_hint': "Wenn Android von einer eingeschränkten Einstellung spricht: Öffne die App-Einstellungen, tippe oben auf ⋮, „Eingeschränkte Einstellungen zulassen“, und schalte es dann hier wieder ein. Android verlangt das von Apps, die nicht aus einem Store installiert wurden.", 'phonesync.state_sending': "Wird gesendet…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Gesendete Kopien: {n}", @@ -1372,6 +1375,7 @@ export default { 'phonesync.fail_not_private': "Gestoppt: Jemand anderes ist der Gruppe beigetreten.", 'phonesync.fail_permission': "Gestoppt: MeshBay hat keinen Zugriff mehr.", 'phonesync.fail_other': "Gestoppt.", + 'phonesync.open_settings': "App-Einstellungen öffnen", 'drivesync.heading': "Datei-Sicherung", 'drivesync.intro': "Die Dateien der Ordner, die du auf diesem Telefon auswählst, einmal täglich über WLAN senden. Eine auf dem Telefon geänderte Datei wird neben der früheren Kopie erneut gesendet; nichts wird je ersetzt.", 'drivesync.folders': "Gesicherte Ordner", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a602da2..a4e07a1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -1299,6 +1299,8 @@ export default { 'photosync.albums': "Albums on this phone", 'photosync.album_camera': "Camera", 'photosync.album_count': "{n} photos · {size}", + 'photosync.videos_not_included': "Videos are not backed up: “Change” to add them.", + 'photosync.videos_included': "Videos are backed up too.", 'photosync.include_videos': "Videos too", 'photosync.include_videos_hint': "Videos are large: they go on Wi-Fi like photos, and the first backup can take a while.", 'photosync.album_count_videos': "{n} photos, {v} videos · {size}", @@ -1337,6 +1339,7 @@ export default { 'messagesync.heading': "Messages backup", 'messagesync.intro': "Send the text messages on this phone once a day: the first copy holds them all, each next one only the new ones. The files are in the XML format SMS backup applications restore. Multimedia messages are not included.", 'messagesync.permission_needed': "MeshBay needs access to this phone's text messages.", + 'messagesync.restricted_hint': "If Android says this is a restricted setting: open the application's settings, tap ⋮ at the top, “Allow restricted settings”, then come back and turn this on again. Android asks this of applications installed outside a store.", 'phonesync.state_sending': "Sending…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Copies sent: {n}", @@ -1353,6 +1356,7 @@ export default { 'phonesync.fail_not_private': "Stopped: somebody else has joined the group.", 'phonesync.fail_permission': "Stopped: MeshBay no longer has access.", 'phonesync.fail_other': "Stopped.", + 'phonesync.open_settings': "Open the application's settings", 'drivesync.heading': "Files backup", 'drivesync.intro': "Send the files of folders you choose on this phone, once a day, on Wi-Fi. A file changed on the phone is sent again beside the earlier copy; nothing is ever replaced.", 'drivesync.folders': "Folders backed up", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 8291f6b..e8ea3e7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -1312,6 +1312,8 @@ export default { 'photosync.albums': "Álbumes de este teléfono", 'photosync.album_camera': "Cámara", 'photosync.album_count': "{n} fotos · {size}", + 'photosync.videos_not_included': "No se hace copia de los vídeos: «Cambiar» para añadirlos.", + 'photosync.videos_included': "También se hace copia de los vídeos.", 'photosync.include_videos': "También los vídeos", 'photosync.include_videos_hint': "Los vídeos ocupan mucho: se envían por Wi-Fi como las fotos, y la primera copia puede tardar.", 'photosync.album_count_videos': "{n} fotos, {v} vídeos · {size}", @@ -1350,6 +1352,7 @@ export default { 'messagesync.heading': "Copia de seguridad de SMS", 'messagesync.intro': "Enviar los SMS de este teléfono una vez al día: la primera copia los contiene todos y cada una de las siguientes solo los nuevos. Los archivos están en el formato XML que restauran las aplicaciones de copia de seguridad de SMS. Los MMS no se incluyen.", 'messagesync.permission_needed': "MeshBay necesita acceso a los SMS de este teléfono.", + 'messagesync.restricted_hint': "Si Android habla de un ajuste restringido: abre los ajustes de la aplicación, toca ⋮ arriba, «Permitir ajustes restringidos» y vuelve para activarlo. Android lo exige a las aplicaciones instaladas fuera de una tienda.", 'phonesync.state_sending': "Enviando…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Copias enviadas: {n}", @@ -1366,6 +1369,7 @@ export default { 'phonesync.fail_not_private': "Detenida: otra persona se ha unido al grupo.", 'phonesync.fail_permission': "Detenida: MeshBay ya no tiene acceso.", 'phonesync.fail_other': "Detenida.", + 'phonesync.open_settings': "Abrir los ajustes de la aplicación", 'drivesync.heading': "Copia de seguridad de archivos", 'drivesync.intro': "Enviar los archivos de las carpetas que elijas en este teléfono, una vez al día, por Wi-Fi. Un archivo modificado en el teléfono se envía de nuevo junto a la copia anterior; nunca se reemplaza nada.", 'drivesync.folders': "Carpetas con copia", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 3ba3232..0d927ae 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -1327,6 +1327,8 @@ export default { 'photosync.albums': "Albums de ce téléphone", 'photosync.album_camera': "Appareil photo", 'photosync.album_count': "{n} photos · {size}", + 'photosync.videos_not_included': "Les vidéos ne sont pas sauvegardées : « Modifier » pour les ajouter.", + 'photosync.videos_included': "Les vidéos sont sauvegardées aussi.", 'photosync.include_videos': "Les vidéos aussi", 'photosync.include_videos_hint': "Les vidéos sont lourdes : elles partent en Wi-Fi comme les photos, et la première sauvegarde peut être longue.", 'photosync.album_count_videos': "{n} photos, {v} vidéos · {size}", @@ -1365,6 +1367,7 @@ export default { 'messagesync.heading': "Sauvegarde des SMS", 'messagesync.intro': "Envoyer les SMS de ce téléphone une fois par jour : la première copie les contient tous, les suivantes seulement les nouveaux. Les fichiers sont au format XML que les applis de sauvegarde SMS savent restaurer. Les MMS ne sont pas inclus.", 'messagesync.permission_needed': "MeshBay a besoin d'accéder aux SMS de ce téléphone.", + 'messagesync.restricted_hint': "Si Android parle d'un paramètre restreint : ouvrez les réglages de l'appli, touchez ⋮ en haut, « Autoriser les paramètres restreints », puis revenez l'activer. Android l'exige des applis installées hors d'un store.", 'phonesync.state_sending': "Envoi…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Copies envoyées : {n}", @@ -1381,6 +1384,7 @@ export default { 'phonesync.fail_not_private': "Arrêtée : quelqu'un d'autre a rejoint le groupe.", 'phonesync.fail_permission': "Arrêtée : MeshBay n'y a plus accès.", 'phonesync.fail_other': "Arrêtée.", + 'phonesync.open_settings': "Ouvrir les réglages de l'appli", 'drivesync.heading': "Sauvegarde des fichiers", 'drivesync.intro': "Envoyer les fichiers des dossiers que vous choisissez sur ce téléphone, une fois par jour, en Wi-Fi. Un fichier modifié sur le téléphone est renvoyé à côté de la copie précédente ; rien n'est jamais remplacé.", 'drivesync.folders': "Dossiers sauvegardés", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 02bd661..e2c6df3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -1326,6 +1326,8 @@ export default { 'photosync.albums': "Album su questo telefono", 'photosync.album_camera': "Fotocamera", 'photosync.album_count': "{n} foto · {size}", + 'photosync.videos_not_included': "I video non vengono salvati: «Modifica» per aggiungerli.", + 'photosync.videos_included': "Anche i video vengono salvati.", 'photosync.include_videos': "Anche i video", 'photosync.include_videos_hint': "I video sono pesanti: partono in Wi-Fi come le foto, e il primo backup può richiedere tempo.", 'photosync.album_count_videos': "{n} foto, {v} video · {size}", @@ -1364,6 +1366,7 @@ export default { 'messagesync.heading': "Backup degli SMS", 'messagesync.intro': "Invia gli SMS di questo telefono una volta al giorno: la prima copia li contiene tutti, le successive solo quelli nuovi. I file sono nel formato XML che le app di backup degli SMS sanno ripristinare. Gli MMS non sono inclusi.", 'messagesync.permission_needed': "MeshBay ha bisogno di accedere agli SMS di questo telefono.", + 'messagesync.restricted_hint': "Se Android parla di un'impostazione con restrizioni: apri le impostazioni dell'app, tocca ⋮ in alto, «Consenti impostazioni con restrizioni», poi torna qui ad attivarlo. Android lo richiede alle app installate fuori da uno store.", 'phonesync.state_sending': "Invio…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Copie inviate: {n}", @@ -1380,6 +1383,7 @@ export default { 'phonesync.fail_not_private': "Interrotto: qualcun altro si è unito al gruppo.", 'phonesync.fail_permission': "Interrotto: MeshBay non ha più accesso.", 'phonesync.fail_other': "Interrotto.", + 'phonesync.open_settings': "Apri le impostazioni dell'app", 'drivesync.heading': "Backup dei file", 'drivesync.intro': "Invia i file delle cartelle che scegli su questo telefono, una volta al giorno, in Wi-Fi. Un file modificato sul telefono viene inviato di nuovo accanto alla copia precedente; non viene mai sostituito nulla.", 'drivesync.folders': "Cartelle salvate", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index ff09dd3..9aa4833 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -1310,6 +1310,8 @@ export default { 'photosync.albums': "この電話のアルバム", 'photosync.album_camera': "カメラ", 'photosync.album_count': "{n} 枚 · {size}", + 'photosync.videos_not_included': "動画はバックアップされていません。追加するには「変更」を押してください。", + 'photosync.videos_included': "動画もバックアップされます。", 'photosync.include_videos': "動画も含める", 'photosync.include_videos_hint': "動画はサイズが大きいため、写真と同じく Wi-Fi で送信され、最初のバックアップには時間がかかることがあります。", 'photosync.album_count_videos': "写真 {n} 枚、動画 {v} 本 · {size}", @@ -1348,6 +1350,7 @@ export default { 'messagesync.heading': "SMS のバックアップ", 'messagesync.intro': "このスマートフォンの SMS を1日1回送ります。最初のコピーにはすべてが含まれ、以降は新しいものだけが含まれます。ファイルは SMS バックアップアプリで復元できる XML 形式です。MMS は含まれません。", 'messagesync.permission_needed': "MeshBay にこのスマートフォンの SMS へのアクセスを許可してください。", + 'messagesync.restricted_hint': "Android が制限付き設定と表示した場合は、アプリの設定を開き、上部の ⋮ から「制限付き設定を許可」を選び、ここに戻ってもう一度オンにしてください。ストア以外からインストールしたアプリには Android がこれを求めます。", 'phonesync.state_sending': "送信中…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "送信したコピー: {n}", @@ -1364,6 +1367,7 @@ export default { 'phonesync.fail_not_private': "停止しました: 他の人がグループに参加しました。", 'phonesync.fail_permission': "停止しました: MeshBay はアクセスできなくなりました。", 'phonesync.fail_other': "停止しました。", + 'phonesync.open_settings': "アプリの設定を開く", 'drivesync.heading': "ファイルのバックアップ", 'drivesync.intro': "このスマートフォンで選んだフォルダーのファイルを、1日1回 Wi-Fi で送ります。スマートフォンで変更したファイルは、以前のコピーの横にもう一度送られます。何も置き換えられません。", 'drivesync.folders': "バックアップするフォルダー", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index c9a51a2..863f7c9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -1328,6 +1328,8 @@ export default { 'photosync.albums': "Albums op deze telefoon", 'photosync.album_camera': "Camera", 'photosync.album_count': "{n} foto's · {size}", + 'photosync.videos_not_included': "Video's worden niet geback-upt: ‘Wijzigen’ om ze toe te voegen.", + 'photosync.videos_included': "Video's worden ook geback-upt.", 'photosync.include_videos': "Ook video's", 'photosync.include_videos_hint': "Video's zijn groot: ze gaan net als foto's via wifi, en de eerste back-up kan even duren.", 'photosync.album_count_videos': "{n} foto's, {v} video's · {size}", @@ -1366,6 +1368,7 @@ export default { 'messagesync.heading': "Back-up van sms'jes", 'messagesync.intro': "Stuur de sms'jes op deze telefoon eenmaal per dag: de eerste kopie bevat ze allemaal, elke volgende alleen de nieuwe. De bestanden zijn in het XML-formaat dat sms-back-upapps kunnen terugzetten. Mms'jes zijn niet inbegrepen.", 'messagesync.permission_needed': "MeshBay heeft toegang nodig tot de sms'jes op deze telefoon.", + 'messagesync.restricted_hint': "Als Android het over een beperkte instelling heeft: open de instellingen van de app, tik bovenaan op ⋮, ‘Beperkte instellingen toestaan’, en zet het daarna hier weer aan. Android vraagt dit van apps die buiten een winkel zijn geïnstalleerd.", 'phonesync.state_sending': "Bezig met verzenden…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Verzonden kopieën: {n}", @@ -1382,6 +1385,7 @@ export default { 'phonesync.fail_not_private': "Gestopt: iemand anders is lid geworden van de groep.", 'phonesync.fail_permission': "Gestopt: MeshBay heeft geen toegang meer.", 'phonesync.fail_other': "Gestopt.", + 'phonesync.open_settings': "Instellingen van de app openen", 'drivesync.heading': "Back-up van bestanden", 'drivesync.intro': "Stuur de bestanden van mappen die je op deze telefoon kiest, eenmaal per dag, via wifi. Een bestand dat op de telefoon is gewijzigd, wordt opnieuw verstuurd naast de eerdere kopie; er wordt nooit iets vervangen.", 'drivesync.folders': "Geback-upte mappen", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 194e20c..2dc2e74 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -1356,6 +1356,8 @@ export default { 'photosync.albums': "Albumy na tym telefonie", 'photosync.album_camera': "Aparat", 'photosync.album_count': "Zdjęcia: {n} · {size}", + 'photosync.videos_not_included': "Filmy nie są kopiowane: „Zmień”, aby je dodać.", + 'photosync.videos_included': "Filmy też są kopiowane.", 'photosync.include_videos': "Również filmy", 'photosync.include_videos_hint': "Filmy są duże: wysyłane są przez Wi-Fi jak zdjęcia, a pierwsza kopia może potrwać.", 'photosync.album_count_videos': "Zdjęcia: {n}, filmy: {v} · {size}", @@ -1394,6 +1396,7 @@ export default { 'messagesync.heading': "Kopia SMS-ów", 'messagesync.intro': "Wysyłaj SMS-y z tego telefonu raz dziennie: pierwsza kopia zawiera wszystkie, każda kolejna tylko nowe. Pliki są w formacie XML, który przywracają aplikacje do kopii SMS-ów. MMS-y nie są uwzględniane.", 'messagesync.permission_needed': "MeshBay potrzebuje dostępu do SMS-ów w tym telefonie.", + 'messagesync.restricted_hint': "Jeśli Android mówi o ustawieniu z ograniczeniami: otwórz ustawienia aplikacji, dotknij ⋮ u góry, „Zezwól na ustawienia z ograniczeniami”, a potem wróć i włącz to ponownie. Android wymaga tego od aplikacji zainstalowanych spoza sklepu.", 'phonesync.state_sending': "Wysyłanie…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Wysłane kopie: {n}", @@ -1410,6 +1413,7 @@ export default { 'phonesync.fail_not_private': "Zatrzymano: ktoś inny dołączył do grupy.", 'phonesync.fail_permission': "Zatrzymano: MeshBay nie ma już dostępu.", 'phonesync.fail_other': "Zatrzymano.", + 'phonesync.open_settings': "Otwórz ustawienia aplikacji", 'drivesync.heading': "Kopia plików", 'drivesync.intro': "Wysyłaj pliki z folderów wybranych w tym telefonie raz dziennie przez Wi-Fi. Plik zmieniony w telefonie jest wysyłany ponownie obok wcześniejszej kopii; nic nie jest nigdy zastępowane.", 'drivesync.folders': "Kopiowane foldery", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 9afedd9..6c4f03b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -1313,6 +1313,8 @@ export default { 'photosync.albums': "Álbuns deste celular", 'photosync.album_camera': "Câmera", 'photosync.album_count': "{n} fotos · {size}", + 'photosync.videos_not_included': "Os vídeos não têm backup: “Alterar” para adicioná-los.", + 'photosync.videos_included': "Os vídeos também têm backup.", 'photosync.include_videos': "Vídeos também", 'photosync.include_videos_hint': "Vídeos são pesados: vão pelo Wi-Fi como as fotos, e o primeiro backup pode demorar.", 'photosync.album_count_videos': "{n} fotos, {v} vídeos · {size}", @@ -1351,6 +1353,7 @@ export default { 'messagesync.heading': "Backup de SMS", 'messagesync.intro': "Enviar os SMS deste celular uma vez por dia: a primeira cópia contém todos, cada uma das seguintes só os novos. Os arquivos estão no formato XML que os apps de backup de SMS conseguem restaurar. MMS não são incluídos.", 'messagesync.permission_needed': "O MeshBay precisa de acesso aos SMS deste celular.", + 'messagesync.restricted_hint': "Se o Android falar de uma configuração restrita: abra as configurações do app, toque em ⋮ no alto, “Permitir configurações restritas” e volte para ativar. O Android exige isso de apps instalados fora de uma loja.", 'phonesync.state_sending': "Enviando…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "Cópias enviadas: {n}", @@ -1367,6 +1370,7 @@ export default { 'phonesync.fail_not_private': "Parado: outra pessoa entrou no grupo.", 'phonesync.fail_permission': "Parado: o MeshBay não tem mais acesso.", 'phonesync.fail_other': "Parado.", + 'phonesync.open_settings': "Abrir as configurações do app", 'drivesync.heading': "Backup de arquivos", 'drivesync.intro': "Enviar os arquivos das pastas que você escolher neste celular, uma vez por dia, pelo Wi-Fi. Um arquivo alterado no celular é enviado de novo ao lado da cópia anterior; nada é substituído.", 'drivesync.folders': "Pastas com backup", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index fde577c..8899ed0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -1299,6 +1299,8 @@ export default { 'photosync.albums': "这部手机上的相册", 'photosync.album_camera': "相机", 'photosync.album_count': "{n} 张照片 · {size}", + 'photosync.videos_not_included': "视频未备份:点“更改”添加。", + 'photosync.videos_included': "视频也会备份。", 'photosync.include_videos': "包括视频", 'photosync.include_videos_hint': "视频体积较大:它们和照片一样通过 Wi-Fi 发送,首次备份可能需要一些时间。", 'photosync.album_count_videos': "{n} 张照片,{v} 个视频 · {size}", @@ -1337,6 +1339,7 @@ export default { 'messagesync.heading': "短信备份", 'messagesync.intro': "每天一次发送本机的短信:第一份副本包含全部短信,之后每份只包含新短信。文件采用短信备份应用可以恢复的 XML 格式。不包括彩信。", 'messagesync.permission_needed': "MeshBay 需要访问本机的短信。", + 'messagesync.restricted_hint': "如果 Android 提示这是受限设置:打开应用设置,点按顶部的 ⋮,选择“允许受限设置”,然后回到这里再次开启。Android 对不是从应用商店安装的应用有此要求。", 'phonesync.state_sending': "正在发送…", 'phonesync.summary': "{group} → {dir}", 'phonesync.copies_sent': "已发送副本:{n}", @@ -1353,6 +1356,7 @@ export default { 'phonesync.fail_not_private': "已停止:有其他人加入了群组。", 'phonesync.fail_permission': "已停止:MeshBay 已无法访问。", 'phonesync.fail_other': "已停止。", + 'phonesync.open_settings': "打开应用设置", 'drivesync.heading': "文件备份", 'drivesync.intro': "每天一次通过 Wi-Fi 发送你在本机选择的文件夹中的文件。在手机上修改过的文件会在之前的副本旁边再次发送;不会替换任何内容。", 'drivesync.folders': "已备份的文件夹", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js index 2c1ebc0..998a3b2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js @@ -20,6 +20,9 @@ const TEXT = { messages: { intro: 'messagesync.intro', permission: 'messagesync.permission_needed', + // Android keeps text messages behind "restricted settings" for an + // application installed outside a store: said, with the way to lift it. + restricted: 'messagesync.restricted_hint', }, }; @@ -59,7 +62,7 @@ export function DocSyncSection({ kind, destination }) { if (st.permission !== 'granted') st = await sync.permit(); if (st.permission !== 'granted') { setStatus(st); return; } setStatus(await sync.configure({})); - runDocSync(kind); + runDocSync(kind, { force: true }); } catch (e) { setError(e.message); } finally { @@ -84,7 +87,12 @@ export function DocSyncSection({ kind, destination }) { <p class="settings-hint">${t(TEXT[kind].intro)}</p> ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`} ${destination && status.permission !== 'granted' && html` - <p class="settings-hint">${t(TEXT[kind].permission)}</p>`} + <p class="settings-hint">${t(TEXT[kind].permission)}</p> + ${TEXT[kind].restricted && html` + <p class="settings-hint">${t(TEXT[kind].restricted)}</p> + <button class="btn-secondary" style="margin-bottom:8px" + onClick=${() => sync.openSettings().catch((e) => setError(e.message))}> + ${t('phonesync.open_settings')}</button>`}`} ${destination && html` <button class="admin-btn" disabled=${busy} onClick=${turnOn}>${t('phonesync.turn_on')}</button>`} `} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js index 088de76..d2a839e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js @@ -103,6 +103,8 @@ export function PhotoSyncSection({ destination }) { ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() }) : t('photosync.never')} </p> + <p class="settings-hint"> + ${t(cfg.includeVideos ? 'photosync.videos_included' : 'photosync.videos_not_included')}</p> ${status.permission === 'partial' && html` <p class="settings-hint">${t('photosync.permission_partial')}</p>`} ${cfg.includeVideos && status.videoPermission === 'denied' && html` @@ -123,7 +125,7 @@ export function PhotoSyncSection({ destination }) { ${editing && html` <${Setup} status=${status} onPermission=${setStatus} - onDone=${(next) => { setEditing(false); if (next) { setStatus(next); runPhotoSync(); } }} />`} + onDone=${(next) => { setEditing(false); if (next) { setStatus(next); runPhotoSync({ force: true }); } }} />`} ${error && html`<p class="error-msg">${error}</p>`} </div> `; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js index a9d4517..9a45cf8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js @@ -25,6 +25,8 @@ const RETRY_MS = 15 * 60 * 1000; // A slot the node keeps queued this long means it is busy with others; the // backup can wait for tomorrow's run, they cannot. const LEASE_WAIT_MS = 10 * 60 * 1000; +// The notification's progress line is updated at most this often. +const NOTIFY_MS = 1000; // Refusals that will hold at the next run too. Said once (a notification, from // the phone) and retried a day later, never at every opening. @@ -216,11 +218,18 @@ export function createBackup({ for (const r of index.roots || []) dirs.add(r.name); let done = 0; - const report = () => { + // The page follows every file; the notification at most once a second, + // which is all Android shows anyway: files already on the node go by + // faster than that, and a burst of updates is dropped by the system. + let reported = 0; + const report = async () => { setState({ phase: 'sending', done, total: items.length }); - return t(progress, { done, total: items.length }); + const now = Date.now(); + if (done && done < items.length && now - reported < NOTIFY_MS) return; + reported = now; + await sync.keepAlive(true, t(progress, { done, total: items.length })); }; - await sync.keepAlive(true, report()); + await report(); for (const item of items) { // Off Wi-Fi mid-run: stop after the file in flight. The node keeps the @@ -244,7 +253,7 @@ export function createBackup({ } } done += 1; - await sync.keepAlive(true, report()); + await report(); } if (!await sendManifest(transport, dest, dirs)) return; await sync.completed(); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index 596c327..47e3a72 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -255,15 +255,17 @@ export const photoSync = { /** * A file the phone issued under `token`, as what an upload reads (`name`, * `size`, `slice(a, b).arrayBuffer()`): each slice is fetched from the phone - * when it is read, so a video of gigabytes is never whole in this page. + * when it is read, so a video of gigabytes is never whole in this page. The + * slice is in the address (`?range=a-b`), not a Range header: the WebView + * does not hand that header to the application that answers. */ function rangedFile(prefix, token, name, size) { const url = `${prefix}${encodeURIComponent(token)}`; const part = (start, end) => ({ async arrayBuffer() { if (end <= start) return new ArrayBuffer(0); - const r = await fetch(url, { cache: 'no-store', headers: { Range: `bytes=${start}-${end - 1}` } }); - if (r.status !== 206) throw new Error('This file is no longer on the phone'); + const r = await fetch(`${url}?range=${start}-${end - 1}`, { cache: 'no-store' }); + if (!r.ok) throw new Error('This file is no longer on the phone'); return r.arrayBuffer(); }, }); @@ -343,6 +345,7 @@ function docSync(name) { sent(token, dir, n) { return b.sent(token, dir, n); }, completed() { return b.completed(); }, failed(code, text) { return b.failed(code, text); }, + openSettings() { return b.openSettings(); }, /** The file the last plan wrote, as a File named for the node. */ async file(token, fileName) { const r = await fetch(`/phonesync/${encodeURIComponent(token)}`, { cache: 'no-store' }); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js index 23aeb32..2bd2c44 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js @@ -56,7 +56,7 @@ export function WhatsAppSyncSection({ destination }) { const turnOn = (withMedia) => act(async () => { const next = await sync.configure({ media: withMedia }); - runWhatsAppSync(); + runWhatsAppSync({ force: true }); return next; }); diff --git a/packages/meshbay-hub/tests/test_android_shell.py b/packages/meshbay-hub/tests/test_android_shell.py index 001f6d9..cd5a36e 100644 --- a/packages/meshbay-hub/tests/test_android_shell.py +++ b/packages/meshbay-hub/tests/test_android_shell.py @@ -139,7 +139,7 @@ def test_the_shim_offers_desktop_channels_and_native_answers_each(): SRC / "photos" / "PhotoChannels.kt", SRC / "phonesync" / "Destination.kt"): native |= set(re.findall(r'^\s*"([\w:-]+)" ->', _read(path), flags=re.M)) # Contacts and messages share one class, its channels named by a prefix. - docs = set(re.findall(r'^\s*"(\w+)" ->', _read(SRC / "phonesync" / "DocChannels.kt"), flags=re.M)) + docs = set(re.findall(r'^\s*"([\w-]+)" ->', _read(SRC / "phonesync" / "DocChannels.kt"), flags=re.M)) for prefix in re.findall(r'DocChannels\("(\w+)"', _read(SRC / "MainActivity.kt")): native |= {f"{prefix}:{name}" for name in docs} # The chosen-folder backups too, their prefixes in DriveKind.kt. diff --git a/packages/meshbay-hub/tests/test_drive_sync.py b/packages/meshbay-hub/tests/test_drive_sync.py index 342b310..3861585 100644 --- a/packages/meshbay-hub/tests/test_drive_sync.py +++ b/packages/meshbay-hub/tests/test_drive_sync.py @@ -41,11 +41,11 @@ doc.visibilityState = 'visible'; globalThis.window = win; globalThis.document = doc; globalThis.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} }; -globalThis.fetch = async (url, opts) => { - const range = opts && opts.headers && opts.headers.Range; - fake.log.push(['fetch', url, range || null]); - const m = /bytes=(\d+)-(\d+)/.exec(range || ''); - return { ok: true, status: m ? 206 : 200, +globalThis.fetch = async (url) => { + const [path, query] = url.split('?'); + const m = /range=(\d+)-(\d+)/.exec(query || ''); + fake.log.push(['fetch', path, m ? `bytes=${m[1]}-${m[2]}` : null]); + return { ok: true, status: 200, arrayBuffer: async () => new ArrayBuffer(m ? Number(m[2]) - Number(m[1]) + 1 : 0) }; }; fake.status = { diff --git a/packages/meshbay-hub/tests/test_photo_sync.py b/packages/meshbay-hub/tests/test_photo_sync.py index 266a111..9ade08b 100644 --- a/packages/meshbay-hub/tests/test_photo_sync.py +++ b/packages/meshbay-hub/tests/test_photo_sync.py @@ -57,12 +57,13 @@ doc.visibilityState = 'visible'; globalThis.window = win; globalThis.document = doc; globalThis.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} }; -globalThis.fetch = async (url, opts) => { - const range = opts && opts.headers && opts.headers.Range; - fake.log.push(['fetch', url, range || null]); - const m = /bytes=(\d+)-(\d+)/.exec(range || ''); +globalThis.fetch = async (url) => { + // The slice is in the address (platform.js rangedFile), never a header. + const [path, query] = url.split('?'); + const m = /range=(\d+)-(\d+)/.exec(query || ''); + fake.log.push(['fetch', path, m ? `bytes=${m[1]}-${m[2]}` : null]); const n = m ? Number(m[2]) - Number(m[1]) + 1 : 4; - return { ok: true, status: m ? 206 : 200, arrayBuffer: async () => new ArrayBuffer(n), + return { ok: true, status: 200, arrayBuffer: async () => new ArrayBuffer(n), blob: async () => new Blob([new Uint8Array(n)]) }; }; |