diff options
Diffstat (limited to 'packages/meshbay-android')
4 files changed, 35 insertions, 9 deletions
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 { |