Skip to content

Commit f3448c8

Browse files
authored
Fix store installs, update offers and in-app browsing (#825)
Three of these are what borrowing `com.android.shell`'s uid costs the manager. Below API 33 `ContextCompat.registerReceiver` demands `<package>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION`, which under the host is nobody's, so every store install threw before `commit()`; both installers register by hand now, under a UUID-named action. `createSessionInternal` skips `INSTALL_REPLACE_EXISTING` for `SHELL_UID`, so a module already on the device failed with `ALREADY_EXISTS` -- that branch is unchanged from API 27 to AOSP main, so a store update has never worked parasitically. And `AwSettings` reads `checkSelfPermission(INTERNET)` in its constructor, which AOSP's Shell did not request until android-12, so every in-app page failed as `ERR_CACHE_MISS` on a device whose networking is fine; the context a WebView is built with now answers that one question, and only where the platform says no. The fourth is that an offer is decided by numbers a git tag states, which nothing obliges to be the ones in the APK's manifest, and no rule over `(code, name)` can bridge that: a module that never changes its tag code is only ever seen to update through the name clause, and one that reuses a versionName across several codes only through the code clause. So the Store stops inferring and records instead -- the release it installed, and the version the device reported once it was on -- read in `StoreEntry.upgradable` so every count, filter and badge agrees. Where an offer names the version already installed it is worded as a reinstall rather than as an update from a version to itself, which is true whether the tag disagrees with the manifest or the release is a genuine rebuild. All eighteen locales are filled in. Fixes #823.
1 parent fbcff2f commit f3448c8

55 files changed

Lines changed: 554 additions & 215 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

manager/src/main/kotlin/org/matrix/vector/manager/data/model/RepoModels.kt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,29 @@ data class ReleaseAsset(
137137
*/
138138
data class RepoVersion(val versionCode: Long, val versionName: String) {
139139

140+
/** The tag this was read from, which is also how [StoreInstall] writes one back down. */
141+
val tag: String
142+
get() = "$versionCode-$versionName"
143+
140144
fun upgradableOver(installedCode: Long, installedName: String): Boolean =
141145
versionCode > installedCode ||
142146
(versionCode == installedCode && installedName.replace(' ', '_') != versionName)
143147

148+
/**
149+
* Whether installing this would leave the reader on the version they already have, by name.
150+
*
151+
* Which is all the offer can be worded as when it is true. Two different things reach here — a
152+
* rebuild of the same version under a higher code, and a tag whose code is simply not the APK's
153+
* — and nothing in either number tells them apart, so the wording has to be true of both. What
154+
* is certain in both is where the reader ends up: on this version name again.
155+
*
156+
* The underscores are the same normalisation [upgradableOver] applies, and for the same reason:
157+
* a git tag cannot carry a space, so an author whose versionName has one writes it with an
158+
* underscore.
159+
*/
160+
fun sameVersionAs(installed: RepoVersion?): Boolean =
161+
installed != null && installed.versionName.replace(' ', '_') == versionName
162+
144163
companion object {
145164
fun parse(raw: String?): RepoVersion? {
146165
val text = raw?.takeIf { it.isNotBlank() } ?: return null
@@ -152,6 +171,35 @@ data class RepoVersion(val versionCode: Long, val versionName: String) {
152171
}
153172
}
154173

174+
/**
175+
* A release this manager installed, and what the device said the module was afterwards.
176+
*
177+
* Two versions, because they are not the same kind of fact and need not be the same number:
178+
* [release] is what a tag claimed, [installed] is what the APK inside it turned out to be.
179+
*
180+
* That difference is the whole reason this is recorded. The comparison above believes the tag, and
181+
* nothing obliges an author to tag a release with the version their manifest actually states. Where
182+
* the two disagree the offer cannot be satisfied by taking it: installing leaves the device on a
183+
* version the tag still claims to beat, so the row asks again, and again, for ever.
184+
*
185+
* Nor can it be settled by reading the two numbers harder, because both halves of the comparison
186+
* are load-bearing for someone: a module that never changes its tag code is only ever seen to
187+
* update through the name clause, and one that reuses a versionName across several codes only
188+
* through the code clause. Any rule over `(code, name)` is wrong for one of them.
189+
*
190+
* So the Store stops inferring and records instead. An offer it has already installed, on a device
191+
* still reporting what that install produced, is one the reader has taken.
192+
*
193+
* [installed] is what makes the record expire on its own: it is checked against what the device
194+
* reports now, so a module replaced from anywhere else stops matching and the offer comes back.
195+
*/
196+
data class StoreInstall(val release: RepoVersion, val installed: RepoVersion) {
197+
198+
/** Whether this note says [latest] is already here, as [current]. */
199+
fun satisfies(latest: RepoVersion?, current: RepoVersion?): Boolean =
200+
release == latest && installed == current
201+
}
202+
155203
/**
156204
* One row of the Store: a catalogue entry, plus what this device has to say about it.
157205
*
@@ -164,10 +212,30 @@ data class StoreEntry(
164212
val installed: RepoVersion?,
165213
/** The reader asked not to be told about this one again. */
166214
val updatesMuted: Boolean = false,
215+
/** What this manager last installed here, if this manager is what installed it. */
216+
val storeInstall: StoreInstall? = null,
167217
) {
218+
219+
/** The newest release is one we installed, and the device still reports what it left behind. */
220+
private val alreadyInstalled: Boolean
221+
get() = storeInstall?.satisfies(latest, installed) == true
222+
223+
/**
224+
* The offer would not change which version this device says it has. See [sameVersionAs].
225+
*
226+
* Read by everything that *words* an offer, because `1.1.1 → 1.1.1` is a sentence the app cannot
227+
* mean. [upgradable] deliberately does not consult it: whether to offer at all is a different
228+
* question from what to call it, and a rebuild is worth offering.
229+
*/
230+
val sameVersion: Boolean
231+
get() = latest?.sameVersionAs(installed) == true
232+
168233
/**
169234
* There is a newer version *and* the reader wants to hear about it.
170235
*
236+
* A release this manager itself installed is not a newer version, whatever the two numbers say;
237+
* see [StoreInstall].
238+
*
171239
* Muting is folded in here rather than at each place that reads this, because every list and
172240
* count that mentions updates reads it — the Store's header count, its updates filter, its row
173241
* badge, and the set the Modules screen badges from — and a mute that only some of them
@@ -184,6 +252,7 @@ data class StoreEntry(
184252
!updatesMuted &&
185253
installed != null &&
186254
latest != null &&
255+
!alreadyInstalled &&
187256
latest.upgradableOver(installed.versionCode, installed.versionName)
188257
}
189258

manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ManagerInstaller.kt

Lines changed: 16 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
package org.matrix.vector.manager.data.repository
22

3-
import android.app.PendingIntent
4-
import android.content.BroadcastReceiver
53
import android.content.Context
6-
import android.content.Intent
7-
import android.content.IntentFilter
84
import android.content.pm.PackageInstaller
9-
import android.os.Build
105
import android.util.Log
11-
import androidx.core.content.ContextCompat
12-
import androidx.core.content.IntentCompat
136
import java.io.FileInputStream
147
import kotlinx.coroutines.CancellationException
158
import kotlinx.coroutines.Dispatchers
169
import kotlinx.coroutines.flow.MutableStateFlow
1710
import kotlinx.coroutines.flow.StateFlow
1811
import kotlinx.coroutines.flow.asStateFlow
19-
import kotlinx.coroutines.suspendCancellableCoroutine
2012
import kotlinx.coroutines.withContext
2113
import kotlinx.coroutines.withTimeoutOrNull
2214
import org.matrix.vector.manager.BuildConfig
2315
import org.matrix.vector.manager.Constants
2416
import org.matrix.vector.manager.ipc.DaemonClient
17+
import org.matrix.vector.manager.ipc.commitForResult
18+
import org.matrix.vector.manager.ipc.requestReplaceExisting
2519

2620
/** Where installing the manager as an app has got to. */
2721
sealed interface ManagerInstallStep {
@@ -132,6 +126,9 @@ class ManagerInstaller(private val context: Context, private val daemon: DaemonC
132126
// with it. A daemon serving something else cannot install it as Vector.
133127
setAppPackageName(BuildConfig.MANAGER_PACKAGE_NAME)
134128
if (size > 0) setSize(size)
129+
// Updating an installed manager from the host is a replace, and
130+
// parasitically the platform does not make it one for us.
131+
requestReplaceExisting()
135132
}
136133
sessionId = packageInstaller.createSession(params)
137134

@@ -179,85 +176,25 @@ class ManagerInstaller(private val context: Context, private val daemon: DaemonC
179176
/**
180177
* Commits the session and waits for the platform's verdict.
181178
*
182-
* Registered at runtime rather than declared, because parasitically nothing in this app's
183-
* manifest exists and a declared receiver would never fire. `STATUS_PENDING_USER_ACTION` is not
184-
* terminal — it means the system is asking, and the real status follows the answer. It should
185-
* not arise here: the host holds `INSTALL_PACKAGES`, so the commit is silent. It is handled
186-
* anyway, because the same code runs from a manager that is already installed and updating
187-
* itself, where the prompt is exactly what the platform will do.
179+
* `STATUS_PENDING_USER_ACTION` should not arise here — the host holds `INSTALL_PACKAGES`, so
180+
* the commit is silent — but it is handled anyway, because the same code runs from a manager
181+
* that is already installed and updating itself, where the prompt is exactly what the platform
182+
* will do.
183+
*
184+
* @see commitForResult
188185
*/
189186
private suspend fun commit(
190187
session: PackageInstaller.Session,
191188
sessionId: Int,
192-
): Pair<Int, String?> = suspendCancellableCoroutine { continuation ->
193-
val action = "$RESULT_ACTION.$sessionId"
194-
val receiver =
195-
object : BroadcastReceiver() {
196-
override fun onReceive(received: Context, intent: Intent) {
197-
val status =
198-
intent.getIntExtra(
199-
PackageInstaller.EXTRA_STATUS,
200-
PackageInstaller.STATUS_FAILURE,
201-
)
202-
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
203-
IntentCompat.getParcelableExtra(
204-
intent,
205-
Intent.EXTRA_INTENT,
206-
Intent::class.java,
207-
)
208-
?.let { confirm ->
209-
runCatching {
210-
context.startActivity(
211-
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
212-
)
213-
}
214-
.onFailure { e ->
215-
Log.e(
216-
Constants.TAG,
217-
"actions: manager install prompt could not be started",
218-
e,
219-
)
220-
}
221-
}
222-
return
223-
}
224-
runCatching { context.unregisterReceiver(this) }
225-
if (continuation.isActive) {
226-
continuation.resumeWith(
227-
Result.success(
228-
status to
229-
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
230-
)
231-
)
232-
}
233-
}
234-
}
235-
236-
ContextCompat.registerReceiver(
237-
context,
238-
receiver,
239-
IntentFilter(action),
240-
ContextCompat.RECEIVER_NOT_EXPORTED,
189+
): Pair<Int, String?> =
190+
context.commitForResult(
191+
session,
192+
sessionId,
193+
promptFailure = "actions: manager install prompt could not be started",
241194
)
242-
continuation.invokeOnCancellation { runCatching { context.unregisterReceiver(receiver) } }
243-
244-
val flags =
245-
PendingIntent.FLAG_UPDATE_CURRENT or
246-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE
247-
else 0
248-
val pending =
249-
PendingIntent.getBroadcast(
250-
context,
251-
sessionId,
252-
Intent(action).setPackage(context.packageName),
253-
flags,
254-
)
255-
session.commit(pending.intentSender)
256-
}
257195

258196
private companion object {
259197
const val WRITE_NAME = "manager.apk"
260-
const val RESULT_ACTION = "org.matrix.vector.manager.INSTALL_MANAGER_RESULT"
261198

262199
/** What the platform calls it in `EXTRA_STATUS_MESSAGE`; see PackageManagerException. */
263200
const val SIGNATURE_CONFLICT = "INSTALL_FAILED_UPDATE_INCOMPATIBLE"

manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ModuleInstaller.kt

Lines changed: 17 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
package org.matrix.vector.manager.data.repository
22

3-
import android.app.PendingIntent
4-
import android.content.BroadcastReceiver
53
import android.content.Context
6-
import android.content.Intent
7-
import android.content.IntentFilter
84
import android.content.pm.PackageInstaller
9-
import android.os.Build
105
import android.util.Log
11-
import androidx.core.content.ContextCompat
12-
import androidx.core.content.IntentCompat
136
import java.io.IOException
147
import kotlinx.coroutines.CancellationException
158
import kotlinx.coroutines.Dispatchers
@@ -18,12 +11,13 @@ import kotlinx.coroutines.ensureActive
1811
import kotlinx.coroutines.flow.MutableStateFlow
1912
import kotlinx.coroutines.flow.StateFlow
2013
import kotlinx.coroutines.flow.asStateFlow
21-
import kotlinx.coroutines.suspendCancellableCoroutine
2214
import kotlinx.coroutines.withContext
2315
import okhttp3.OkHttpClient
2416
import okhttp3.Request
2517
import org.matrix.vector.manager.Constants
2618
import org.matrix.vector.manager.data.model.ReleaseAsset
19+
import org.matrix.vector.manager.ipc.commitForResult
20+
import org.matrix.vector.manager.ipc.requestReplaceExisting
2721

2822
/** Where an install has got to. One at a time, because a user installs one module at a time. */
2923
sealed interface InstallStep {
@@ -80,6 +74,11 @@ class ModuleInstaller(private val context: Context, private val client: OkHttpCl
8074
* Returns true only when the platform reports the package installed. There is no resume: a
8175
* dropped connection costs the whole transfer, which is an acceptable trade for module APKs
8276
* (tens to a few hundred kilobytes) in exchange for never touching the filesystem.
77+
*
78+
* What became of it is recorded by the caller rather than here — see RepoRepository.readInstalled
79+
* and SettingsRepository.noteStoreInstall — because the version to record has to be read the way
80+
* the Store reads it, across every user, and this class talks to the platform rather than to the
81+
* daemon.
8382
*/
8483
suspend fun install(packageName: String, asset: ReleaseAsset): Boolean =
8584
withContext(Dispatchers.IO) {
@@ -102,6 +101,7 @@ class ModuleInstaller(private val context: Context, private val client: OkHttpCl
102101
.apply {
103102
setAppPackageName(packageName)
104103
if (asset.size > 0) setSize(asset.size)
104+
requestReplaceExisting()
105105
}
106106
sessionId = packageInstaller.createSession(params)
107107

@@ -182,82 +182,24 @@ class ModuleInstaller(private val context: Context, private val client: OkHttpCl
182182
/**
183183
* Commits the session and waits for the platform's verdict.
184184
*
185-
* The result arrives as a broadcast, and the receiver is registered at runtime rather than
186-
* declared: parasitically nothing in the manifest exists, so a declared receiver would simply
187-
* never fire. `STATUS_PENDING_USER_ACTION` is not terminal — it means the system is asking the
188-
* user, and the real status follows once they answer.
185+
* @see commitForResult
189186
*/
190187
private suspend fun commit(
191188
session: PackageInstaller.Session,
192189
sessionId: Int,
193190
packageName: String,
194-
): Pair<Int, String?> = suspendCancellableCoroutine { continuation ->
195-
val action = "$RESULT_ACTION.$sessionId"
196-
val receiver =
197-
object : BroadcastReceiver() {
198-
override fun onReceive(received: Context, intent: Intent) {
199-
val status =
200-
intent.getIntExtra(
201-
PackageInstaller.EXTRA_STATUS,
202-
PackageInstaller.STATUS_FAILURE,
203-
)
204-
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
205-
_state.value = InstallStep.Confirming(packageName)
206-
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)
207-
?.let { confirm ->
208-
runCatching {
209-
context.startActivity(
210-
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
211-
)
212-
}
213-
.onFailure { e ->
214-
Log.e(
215-
Constants.TAG,
216-
"store: install prompt for $packageName could not be started",
217-
e,
218-
)
219-
}
220-
}
221-
return
222-
}
223-
runCatching { context.unregisterReceiver(this) }
224-
if (continuation.isActive) {
225-
continuation.resumeWith(
226-
Result.success(
227-
status to
228-
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
229-
)
230-
)
231-
}
232-
}
233-
}
234-
235-
ContextCompat.registerReceiver(
236-
context,
237-
receiver,
238-
IntentFilter(action),
239-
ContextCompat.RECEIVER_NOT_EXPORTED,
240-
)
241-
continuation.invokeOnCancellation { runCatching { context.unregisterReceiver(receiver) } }
242-
243-
val flags =
244-
PendingIntent.FLAG_UPDATE_CURRENT or
245-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE
246-
else 0
247-
val pending =
248-
PendingIntent.getBroadcast(
249-
context,
250-
sessionId,
251-
Intent(action).setPackage(context.packageName),
252-
flags,
253-
)
254-
session.commit(pending.intentSender)
255-
}
191+
): Pair<Int, String?> =
192+
context.commitForResult(
193+
session,
194+
sessionId,
195+
promptFailure = "store: install prompt for $packageName could not be started",
196+
) {
197+
_state.value = InstallStep.Confirming(packageName)
198+
}
256199

257200
private companion object {
258201
const val WRITE_NAME = "module.apk"
259202
const val CHUNK_BYTES = 64 * 1024
260203
const val PROGRESS_STEP_BYTES = 256L * 1024
261-
const val RESULT_ACTION = "org.matrix.vector.manager.INSTALL_RESULT"
262204
}
263205
}

0 commit comments

Comments
 (0)