diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 96251dffc..6e9fdd94a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -61,6 +61,15 @@ object ManagerService : IManagerService.Stub() { */ private const val UNINSTALL_TIMEOUT_SECONDS = 60L + /** + * `PackageManager.INSTALL_SUCCEEDED`, which is hidden and so cannot be named from here. + * + * The one non-negative code the installer answers with; every failure is a negative + * `INSTALL_FAILED_*`. Written out rather than tested as `>= 0` so that a future code that is + * neither is treated as the refusal it would be. + */ + private const val INSTALL_SUCCEEDED = 1 + private var managerPid = -1 private var pendingManager = false @@ -314,6 +323,42 @@ object ManagerService : IManagerService.Stub() { activityManager?.forceStopPackage(packageName, userId) } + /** + * Puts a package an existing user already holds into another user. + * + * The platform grew a fifth parameter in Android 10 — the permissions to allowlist — and the + * daemon still runs on 8.1, so the call is split on the version the parameter arrived in. + * Passing `null` for that list is what the shell's own `pm install-existing` passes: it means + * "allowlist nothing extra", not "grant nothing", and the package keeps the grants its install + * state already implies. + * + * `INSTALL_REASON_USER` rather than `UNKNOWN`, because a person tapped this in the module list. + * The platform records the reason and shows it in `dumpsys package`. + */ + override fun installExistingPackageAsUser(packageName: String, userId: Int): Boolean { + val pm = packageManager ?: return false + return runCatching { + val status = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + pm.installExistingPackageAsUser( + packageName, userId, 0, PackageManager.INSTALL_REASON_USER, null) + } else { + pm.installExistingPackageAsUser( + packageName, userId, 0, PackageManager.INSTALL_REASON_USER) + } + // The platform answers with an INSTALL_* code, where the single success is + // INSTALL_SUCCEEDED (1) and every refusal is negative. Anything else is a refusal we + // have no name for, and reporting it as success would leave the manager claiming a + // module is in a profile that never received it. + if (status != INSTALL_SUCCEEDED) { + Log.w(TAG, "install-existing of $packageName for user $userId answered $status") + } + status == INSTALL_SUCCEEDED + } + .onFailure { Log.e(TAG, "Failed to install $packageName for user $userId", it) } + .getOrDefault(false) + } + override fun softReboot() = VectorDaemon.softReboot() /** diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index b4dd39e5a..3383914f4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -197,18 +197,25 @@ private val getInstalledPackagesMethod: Method? by lazy { ?.apply { isAccessible = true } } -/** Reflectively calls getInstalledPackages and casts to ParceledListSlice. */ +/** + * Reflectively calls getInstalledPackages and casts to ParceledListSlice. + * + * Answers `null` when the query did not happen, which is not the same answer as an empty list and + * must never be flattened into one: a user that holds no packages and a user the platform refused + * to answer for look identical once both are `emptyList()`, and the caller builds the manager's + * whole app list out of this. + */ private fun IPackageManager.getInstalledPackagesReflect( flags: Any, userId: Int -): List { - val method = getInstalledPackagesMethod ?: return emptyList() +): List? { + val method = getInstalledPackagesMethod ?: return null return runCatching { val result = method.invoke(this, flags, userId) @Suppress("UNCHECKED_CAST") (result as? ParceledListSlice)?.list } .onFailure { Log.e(TAG, "Reflection call failed", it.cause ?: it) } - .getOrNull() ?: emptyList() + .getOrNull() } fun IPackageManager.getInstalledPackagesFromAllUsers( @@ -224,7 +231,20 @@ fun IPackageManager.getInstalledPackagesFromAllUsers( val flagParam: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) flags.toLong() else flags - val infos = getInstalledPackagesReflect(flagParam, user.id) + // A user the platform would not answer for fails the whole call, as it did before the daemon + // was rewritten in Kotlin: `PackageService.getInstalledPackagesFromAllUsers` was declared + // `throws RemoteException` and called `getInstalledPackages` straight, so a dead transaction + // came back to the manager as a failure. Catching it per user and carrying on turns that into + // a list that is short by one whole profile, and nothing downstream can tell it from the truth + // — the manager draws it as the device's apps, so a work profile whose query died is a work + // profile the reader is told does not exist. The list is worth having only entire. + // + // This is reachable on an ordinary device: `getInstalledPackages` returns every package with + // its metadata for each user in turn, and on a large one that repeatedly runs the binder + // buffer out and answers DeadObjectException. + val infos = + getInstalledPackagesReflect(flagParam, user.id) + ?: throw IllegalStateException("No package list for user ${user.id}") if (infos.isEmpty()) continue val validUserApps = diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt index a0e7964f0..202b3dd63 100644 --- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt +++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt @@ -257,6 +257,9 @@ class FakeManagerService( override fun uninstallPackage(packageName: String?, userId: Int): Boolean = real?.uninstallPackage(packageName, userId) ?: false + override fun installExistingPackageAsUser(packageName: String?, userId: Int): Boolean = + real?.installExistingPackageAsUser(packageName, userId) ?: false + override fun getUsers(): MutableList = real?.users ?: mutableListOf() override fun startActivityAsUser(intent: Intent?, userId: Int, noUserSwitch: Boolean): Int = diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt index 6865d71ba..616dadc0b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/AppRepository.kt @@ -1,5 +1,6 @@ package org.matrix.vector.manager.data.repository import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo import android.content.pm.PackageManager import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.CancellationException @@ -15,6 +16,7 @@ import org.matrix.vector.manager.data.model.ModuleDetectionCache import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logW +import org.matrix.vector.ui.module.MATCH_ANY_USER /** Fetches and caches the list of installed applications from the daemon. */ class AppRepository( @@ -63,6 +65,13 @@ class AppRepository( */ private var inFlight: Deferred>? = null + /** The same arrangement for [moduleScanPackages], which asks the daemon a different question. */ + @Volatile private var cachedModuleScan: List? = null + + private val moduleScanLock = Mutex() + + private var moduleScanInFlight: Deferred>>? = null + /** * Drops the cache so the next read goes back to the daemon. * @@ -73,6 +82,59 @@ class AppRepository( generation.incrementAndGet() cachedApps = null cachedModulePackages = null + cachedModuleScan = null + } + + /** + * The raw package list the Modules panel scans, shared the same way [getInstalledApps] is. + * + * A second enumeration rather than a filter over the first, because the two ask the daemon + * different questions: this one wants `MATCH_ANY_USER` and uninstalled packages so a module + * held only by a work profile is still seen, and it must *not* set `filterNoProcess`, because a + * module with no components of its own is still a module. + * + * Shared because the module list is the heaviest caller of it and had nothing stopping it + * running against itself. Every package event drops the caches and bumps the revision this + * feeds, and that event arrives twice by design — once from the platform, once from the + * daemon's re-broadcast — so a single install started two full enumerations, and a device whose + * packages churn kept dozens alive at once. That is not a slow path, it is a broken one: each + * enumeration pulls every package with its metadata, per user, as a chunked + * `ParceledListSlice`, and the daemon's binder heap is a fixed megabyte. One report has 409 + * `binder_alloc_buf ... failed, no address space` against the daemon, with 687 buffers + * outstanding and 577 KB free in blocks too small to hold a 197 KB reply — system_server's + * answers failing with ENOSPC not because any one of them was too large, but because sixty + * threads were asking at once. + * + * Answers a [Result] rather than a bare list: a failed enumeration is not a device with no + * packages, and the caller has to be able to tell. Only a success is cached. + */ + suspend fun moduleScanPackages(): Result> { + cachedModuleScan?.let { + return Result.success(it) + } + val job = + moduleScanLock.withLock { + cachedModuleScan?.let { + return Result.success(it) + } + moduleScanInFlight?.takeIf { it.isActive } + ?: scope.async(Dispatchers.IO) { fetchModuleScanPackages() } + .also { moduleScanInFlight = it } + } + return job.await() + } + + private suspend fun fetchModuleScanPackages(): Result> { + val startedAt = generation.get() + val flags = + PackageManager.GET_META_DATA or + PackageManager.MATCH_UNINSTALLED_PACKAGES or + MATCH_ANY_USER + + val result = daemonClient.getInstalledPackagesFromAllUsers(flags, filterNoProcess = false) + val packages = result.getOrElse { return result } + if (generation.get() == startedAt) cachedModuleScan = packages + return Result.success(packages) } suspend fun getInstalledApps(): List { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt index b4c0705f8..e1f9f9e62 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt @@ -264,6 +264,16 @@ class DaemonClient(private val serviceState: StateFlow) { suspend fun uninstallPackage(packageName: String, userId: Int): Result = runIpc { it.uninstallPackage(packageName, userId) } + /** + * Installs a module another user already holds into [userId]. + * + * No APK travels: the device keeps one copy per package name and this flips whether [userId] + * has it. It is the only way the manager can put a module into a profile it does not run in, + * and so the only way a profile with no modules ever gets its first one. + */ + suspend fun installExistingPackageAsUser(packageName: String, userId: Int): Result = + runIpc { it.installExistingPackageAsUser(packageName, userId) } + suspend fun isSepolicyLoaded(): Result = runIpc { it.isSepolicyLoaded } suspend fun getUsers(): Result> = runIpc { it.users diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt index 6e64fc7be..323587da8 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt @@ -44,6 +44,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import androidx.compose.material.icons.rounded.PersonAdd +import org.matrix.vector.ipc.DeviceUser import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW import org.matrix.vector.manager.ui.theme.LocalizedOverlay @@ -113,6 +115,15 @@ fun PackageActionSheet( * module and has no page. Null there rather than a row that leads nowhere. */ onOpenStore: ((String) -> Unit)? = null, + /** + * The users that already hold this package, when the caller knows. + * + * Null from the Scope screen, where the sheet is over a hook target rather than a module and + * installing it elsewhere is not an act that screen is about. Given from the module list, + * where it is the difference between offering a profile that needs this module and offering + * one that already has it. + */ + installedUserIds: Set? = null, ) { // The framework is a scope target, not an app. It has no launcher entry, no settings page in // Settings, and nothing ART could re-optimize, so those three rows would lead nowhere. What it @@ -134,6 +145,26 @@ fun PackageActionSheet( } var confirmSoftReboot by remember { mutableStateOf(false) } + // The users this module could still be put into. Asked once with the sheet, and only for a + // module the caller told us about — the list is short and the call is cheap, but it is an IPC + // and the Scope screen has no use for the answer. + var installTargets by + remember(packageName) { mutableStateOf>(emptyList()) } + var choosingUser by remember { mutableStateOf(false) } + // Bound to a local so the lambda below closes over a plain Set rather than relying on the + // parameter still being non-null across the coroutine boundary. + val holders = installedUserIds + if (isModule && holders != null && !isSystemFramework) { + LaunchedEffect(packageName, holders) { + installTargets = + ServiceLocator.daemon + .getUsers() + .onFailure { e -> logW("actions: user list for $packageName failed", e) } + .getOrDefault(emptyList()) + .filter { it.id !in holders } + } + } + // Deliberately not `rememberCoroutineScope()`. Every action on this sheet dismisses it before // it starts working, and the dismissal takes this composable out of the composition — which // cancels the scope a composition remembered for it. The work launched into that scope then @@ -190,6 +221,64 @@ fun PackageActionSheet( scope.launch(Dispatchers.Main) { onResult(block()) } } + // A list rather than a confirmation, because the question is *which* user and there is no + // sensible default to preselect: on a device with a work profile and a private space, either + // may be the one meant. + if (choosingUser) { + SharedAlertDialog( + onDismissRequest = { choosingUser = false }, + icon = { Icon(Icons.Rounded.PersonAdd, contentDescription = null) }, + title = { Text(stringResource(R.string.action_install_other_user)) }, + text = { + Column { + Text(stringResource(R.string.action_install_other_user_prompt)) + Spacer(Modifier.height(8.dp)) + installTargets.forEach { target -> + TextButton( + onClick = { + choosingUser = false + finish { + val result = + daemon.installExistingPackageAsUser(packageName, target.id) + val ok = result.getOrDefault(false) + // A device-policy refusal and a user that has gone away both + // come back as a plain false, which onFailure never sees. + if (!ok) { + logE( + "actions: install of $packageName for user " + + "${target.id} failed", + result.exceptionOrNull(), + ) + } + PackageActionResult( + if (ok) R.string.action_installed_other_user + else R.string.action_install_other_user_failed, + target.name ?: target.id.toString(), + tone = + if (ok) SnackbarTone.Success + else SnackbarTone.Failure, + ) + } + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = target.name ?: target.id.toString(), + modifier = Modifier.weight(1f), + ) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = { choosingUser = false }) { + Text(stringResource(UiR.string.store_cancel)) + } + }, + ) + } + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { LocalizedOverlay { @@ -363,6 +452,21 @@ LocalizedOverlay { } } + // Only when there is a user this module is not already in. A module every profile holds + // would otherwise carry a row whose whole function is to report that there is nowhere to + // send it, and on a single-user device — which is most of them — the row would never do + // anything at all. + if (installTargets.isNotEmpty()) { + ActionDrawerItem( + icon = Icons.Rounded.PersonAdd, + title = stringResource(R.string.action_install_other_user), + subtitle = stringResource(R.string.action_install_other_user_summary), + tint = colors.primary, + ) { + choosingUser = true + } + } + if (isModule) { HorizontalDivider(Modifier.padding(horizontal = 24.dp, vertical = 4.dp)) ActionDrawerItem( diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt index 3496443fe..e746b5ca4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt @@ -156,6 +156,20 @@ fun ModulesScreen( viewModel: ModulesViewModel = viewModel(factory = ModulesViewModelFactory()), ) { val tabs by viewModel.userModulesTabs.collectAsStateWithLifecycle() + + // Which users already hold a given module, read off the tabs rather than asked of the daemon: + // the tabs are built from the same per-user scan, so this costs nothing and cannot disagree + // with what the list is showing. A user with no modules has no tab and therefore holds none, + // which is the right answer for every package — and is exactly the user the install action + // below exists to reach. + val holdersOf: (String) -> Set = + remember(tabs) { + { pkg -> + tabs.filter { tab -> tab.modules.any { it.packageName == pkg } } + .map { it.user.id } + .toSet() + } + } val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() val query by viewModel.query.collectAsStateWithLifecycle() val filter by viewModel.filter.collectAsStateWithLifecycle() @@ -411,7 +425,7 @@ fun ModulesScreen( stickyHeader(key = "h:active") { SectionHeader(stringResource(R.string.modules_section_active), active.size) } - moduleRows(active, facts, selection, upgradable, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) + moduleRows(active, facts, selection, upgradable, holdersOf, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) } if (inactive.isNotEmpty()) { stickyHeader(key = "h:inactive") { @@ -420,10 +434,10 @@ fun ModulesScreen( inactive.size, ) } - moduleRows(inactive, facts, selection, upgradable, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) + moduleRows(inactive, facts, selection, upgradable, holdersOf, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) } } else { - moduleRows(modules, facts, selection, upgradable, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) + moduleRows(modules, facts, selection, upgradable, holdersOf, onModuleClick, onOpenStore, viewModel::toggleSelected, ::report) } } } @@ -904,6 +918,7 @@ private fun androidx.compose.foundation.lazy.LazyListScope.moduleRows( facts: Map, selection: Set, upgradable: Set, + holdersOf: (String) -> Set, onModuleClick: (String, Int) -> Unit, onOpenStore: (String) -> Unit, onSelect: (InstalledModule) -> Unit, @@ -916,6 +931,7 @@ private fun androidx.compose.foundation.lazy.LazyListScope.moduleRows( hasUpdate = module.packageName in upgradable, selected = ModuleKey(module.packageName, module.userId) in selection, selectionActive = selection.isNotEmpty(), + holders = holdersOf(module.packageName), onClick = { onModuleClick(module.packageName, module.userId) }, onOpenStore = { onOpenStore(module.packageName) }, onSelect = { onSelect(module) }, @@ -947,6 +963,7 @@ private fun ModuleListItem( hasUpdate: Boolean, selected: Boolean, selectionActive: Boolean, + holders: Set, onClick: () -> Unit, onOpenStore: () -> Unit, onSelect: () -> Unit, @@ -982,6 +999,7 @@ private fun ModuleListItem( appName = module.appName, applicationInfo = module.applicationInfo, isModule = true, + installedUserIds = holders, onDismiss = { menuOpen = false }, onResult = onAction, onOpenStore = { onOpenStore() }, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt index c7da5e501..73cb09959 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt @@ -20,7 +20,6 @@ import org.matrix.vector.ipc.DeviceUser import org.matrix.vector.ipc.ScopeEntry import org.matrix.vector.manager.data.model.InstalledModule import org.matrix.vector.ui.REACH_PREVIEW_LIMIT -import org.matrix.vector.ui.module.MATCH_ANY_USER import org.matrix.vector.ui.module.PER_USER_RANGE import org.matrix.vector.manager.data.repository.ModuleRepository import org.matrix.vector.ui.store.StoreEntry @@ -483,18 +482,17 @@ class ModulesViewModel( _daemonAvailable.value = usersResult.isSuccess val users = usersResult.getOrNull() ?: emptyList() - val flags = - PackageManager.GET_META_DATA or - PackageManager.MATCH_UNINSTALLED_PACKAGES or - MATCH_ANY_USER - + // Through the repository rather than straight to the daemon, so that concurrent rescans + // join one enumeration instead of each starting their own. This collector is driven by a + // package event that is delivered twice on purpose, and the daemon's binder heap is a + // fixed megabyte that a handful of these in flight together will exhaust — at which point + // it stops being a performance question and becomes a wrong answer, because a per-user + // query that fails takes that whole profile out of the list. val packages = - daemonClient - .getInstalledPackagesFromAllUsers(flags, filterNoProcess = false) - .getOrElse { e -> - logE("modules: installed package list unavailable, showing no modules", e) - emptyList() - } + ServiceLocator.apps.moduleScanPackages().getOrElse { e -> + logE("modules: installed package list unavailable, showing no modules", e) + emptyList() + } // Through the cache, not straight to ModuleDetection: inspecting a package means opening // its APK and every split as a zip, and there are ~550 of those on a normal device. Keyed diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 932f431a3..cf4bd87d9 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -464,4 +464,9 @@ ليس لـ Vector أيقونة بعد يعمل Vector داخل عملية أخرى بدل أن يكون مثبَّتًا، فلا يظهر شيء في المشغّل ولا توجد طريقة واضحة للعودة إليه. امنحه اختصارًا على الشاشة الرئيسية، أو ثبِّته كتطبيق عادي. عدم السؤال مجددًا + تثبيت لمستخدم آخر + يضيف هذه الوحدة إلى ملف تعريف لا يملكها، ليتسنّى تحديد نطاقها هناك + اختر المستخدم الذي تريد تثبيت هذه الوحدة له. + تم التثبيت لـ %1$s. + تعذّر التثبيت لـ %1$s. diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index 55a712de7..603987557 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -420,4 +420,9 @@ Vector hat noch kein Symbol Vector läuft in einem fremden Prozess, statt installiert zu sein — im Launcher erscheint also nichts, und es gibt keinen offensichtlichen Weg zurück. Gib ihm eine Verknüpfung auf dem Startbildschirm, oder installiere es als gewöhnliche App. Nicht mehr fragen + Für anderen Nutzer installieren + Fügt dieses Modul einem Profil hinzu, das es noch nicht hat, damit es dort einen Geltungsbereich bekommen kann + Wähle den Nutzer, für den dieses Modul installiert werden soll. + Für %1$s installiert. + Konnte nicht für %1$s installiert werden. diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index d3fc8ad5c..cc36b0b73 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -420,4 +420,9 @@ Vector todavía no tiene icono Vector se ejecuta dentro de otro proceso en lugar de estar instalado, así que no aparece nada en tu launcher y no hay una forma evidente de volver. Dale un acceso directo en la pantalla de inicio, o instálalo como una app normal. No volver a preguntar + Instalar para otro usuario + Añade este módulo a un perfil que no lo tiene, para poder darle un ámbito allí + Elige el usuario para el que instalar este módulo. + Instalado para %1$s. + No se pudo instalar para %1$s. diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index d4c8a0670..e6c572451 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -420,4 +420,9 @@ Vector هنوز نمادی ندارد Vector به‌جای آنکه نصب شده باشد درون فرایندی دیگر اجرا می‌شود، پس چیزی در لانچر پیدا نمی‌شود و راه روشنی برای بازگشت به آن نیست. به آن میان‌بری در صفحهٔ اصلی بدهید، یا آن را مانند برنامه‌ای معمولی نصب کنید. دیگر پرسیده نشود + نصب برای کاربر دیگر + این ماژول را به نمایه‌ای که آن را ندارد اضافه می‌کند تا بتوان دامنه‌اش را آنجا تعیین کرد + کاربری را که می‌خواهید این ماژول برایش نصب شود انتخاب کنید. + برای %1$s نصب شد. + نصب برای %1$s ممکن نشد. diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index 3a401d36b..0b98c9968 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -420,4 +420,9 @@ Vector n\'a pas encore d\'icône Vector s\'exécute dans un autre processus au lieu d\'être installé : rien n\'apparaît dans votre lanceur et il n\'y a pas de moyen évident d\'y revenir. Donnez-lui un raccourci sur l\'écran d\'accueil, ou installez-le comme une application ordinaire. Ne plus demander + Installer pour un autre utilisateur + Ajoute ce module à un profil qui ne l’a pas, afin de lui donner une portée là-bas + Choisissez l’utilisateur pour lequel installer ce module. + Installé pour %1$s. + Impossible d’installer pour %1$s. diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index 3c6c6390e..d1b25f8da 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -413,4 +413,9 @@ Vector belum punya ikon Vector berjalan di dalam proses lain alih-alih dipasang, jadi tidak ada yang muncul di launcher Anda dan tidak ada jalan kembali yang jelas. Beri dia pintasan di layar utama, atau pasang sebagai aplikasi biasa. Jangan tanya lagi + Pasang untuk pengguna lain + Menambahkan modul ini ke profil yang belum memilikinya, agar cakupannya bisa diatur di sana + Pilih pengguna untuk memasang modul ini. + Terpasang untuk %1$s. + Tidak dapat memasang untuk %1$s. diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index e71e14e18..4423169d8 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -420,4 +420,9 @@ Vector non ha ancora un\'icona Vector gira dentro un altro processo invece di essere installato, quindi nel launcher non compare nulla e non c\'è un modo evidente per tornarci. Dagli una scorciatoia nella schermata Home, oppure installalo come una normale app. Non chiedere più + Installa per un altro utente + Aggiunge questo modulo a un profilo che non lo ha, così da poterne definire l’ambito lì + Scegli l’utente per cui installare questo modulo. + Installato per %1$s. + Impossibile installare per %1$s. diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index 385c87583..59be1565f 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -446,4 +446,9 @@ ל-Vector עדיין אין סמל Vector פועל בתוך תהליך אחר במקום להיות מותקן, ולכן שום דבר לא מופיע במסך הבית ואין דרך ברורה לחזור אליו. תנו לו קיצור דרך במסך הבית, או התקינו אותו כאפליקציה רגילה. לא לשאול שוב + התקנה עבור משתמש אחר + מוסיף את המודול הזה לפרופיל שאין לו אותו, כדי שאפשר יהיה להגדיר לו טווח שם + בחר את המשתמש שעבורו להתקין את המודול הזה. + הותקן עבור %1$s. + לא ניתן היה להתקין עבור %1$s. diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index 3b75f0c3f..ab213f087 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -399,4 +399,9 @@ Vector のアイコンはまだありません Vector はインストールされるのではなく、別のプロセス内で実行されるため、ランチャーには何も表示されず、Vector に戻ってくる手段もありません。ホーム画面にショートカットを作成するか、通常のアプリとしてインストールしてください。 今後表示しない + 他のユーザーにインストール + このモジュールを持っていないプロファイルに追加し、そこでスコープを設定できるようにします + このモジュールをインストールするユーザーを選択してください。 + %1$s にインストールしました。 + %1$s にインストールできませんでした。 diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index 533f39735..c81bb5621 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -409,4 +409,9 @@ Vector에 아직 아이콘이 없습니다 Vector는 설치되는 대신 다른 프로세스 안에서 실행되므로 런처에 아무것도 나타나지 않고 다시 들어올 뚜렷한 방법도 없습니다. 홈 화면 바로가기를 만들거나, 일반 앱으로 설치하세요. 다시 묻지 않기 + 다른 사용자에게 설치 + 이 모듈이 없는 프로필에 추가하여 거기서 범위를 지정할 수 있게 합니다 + 이 모듈을 설치할 사용자를 선택하세요. + %1$s에 설치했습니다. + %1$s에 설치하지 못했습니다. diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index e8f40a59c..dca8127e0 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -482,4 +482,9 @@ Vector nie ma jeszcze ikony Vector działa w ramach innego procesu i nie jest instalowany, więc nic nie pojawia się w launcherze, więc nie ma oczywistej drogi powrotu. Dodaj skrót na ekranie głównym lub zainstaluj go jako zwykłą aplikację. Nie pytaj ponownie + Zainstaluj dla innego użytkownika + Dodaje ten moduł do profilu, który go nie ma, aby można było określić tam jego zakres + Wybierz użytkownika, dla którego zainstalować ten moduł. + Zainstalowano dla %1$s. + Nie udało się zainstalować dla %1$s. diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index 3977bb60a..819805870 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -420,4 +420,9 @@ O Vector ainda não tem ícone O Vector roda dentro de outro processo em vez de estar instalado, então nada aparece no seu launcher e não há um caminho óbvio de volta. Dê a ele um atalho na tela inicial, ou instale-o como um app comum. Não perguntar de novo + Instalar para outro usuário + Adiciona este módulo a um perfil que não o tem, para que possa ter escopo ali + Escolha o usuário para instalar este módulo. + Instalado para %1$s. + Não foi possível instalar para %1$s. diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index c0c7f5370..afa46ebb4 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -425,4 +425,9 @@ У Vector пока нет значка Vector работает внутри чужого процесса, а не установлен, поэтому в лаунчере ничего не появляется и очевидного пути обратно нет. Добавьте ярлык на главный экран или установите Vector как обычное приложение. Больше не спрашивать + Установить для другого пользователя + Добавляет модуль в профиль, где его нет, чтобы задать там его область действия + Выберите пользователя, для которого установить модуль. + Установлено для %1$s. + Не удалось установить для %1$s. diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index 701a438e6..7c96d9715 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -420,4 +420,9 @@ Vector\'ün henüz bir simgesi yok Vector kurulmak yerine başka bir sürecin içinde çalışır; bu yüzden başlatıcınızda hiçbir şey görünmez ve geri dönmenin bariz bir yolu yoktur. Ona ana ekranda bir kısayol verin ya da sıradan bir uygulama olarak kurun. Bir daha sorma + Başka kullanıcı için yükle + Bu modülü, onda olmayan bir profile ekler; böylece orada kapsam verilebilir + Bu modülün yükleneceği kullanıcıyı seçin. + %1$s için yüklendi. + %1$s için yüklenemedi. diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index 29cb7052f..c9d63bd09 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -462,4 +462,9 @@ У Vector ще немає значка Vector працює всередині чужого процесу, а не встановлений, тому в лаунчері нічого не з\'являється і очевидного шляху назад немає. Додайте ярлик на головний екран або встановіть Vector як звичайний застосунок. Більше не питати + Встановити для іншого користувача + Додає модуль до профілю, де його немає, щоб задати там його область дії + Виберіть користувача, для якого встановити модуль. + Встановлено для %1$s. + Не вдалося встановити для %1$s. diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index c592b94e7..b7a4eea72 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -409,4 +409,9 @@ Vector vẫn chưa có biểu tượng Vector chạy bên trong một tiến trình khác thay vì được cài đặt, nên không có gì hiện ra trong trình khởi chạy và cũng không có cách quay lại rõ ràng. Hãy tạo cho nó một lối tắt trên màn hình chính, hoặc cài nó như một ứng dụng bình thường. Đừng hỏi lại + Cài cho người dùng khác + Thêm mô-đun này vào hồ sơ chưa có nó, để có thể đặt phạm vi ở đó + Chọn người dùng để cài mô-đun này. + Đã cài cho %1$s. + Không thể cài cho %1$s. diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 5068e3cf5..b93191cdc 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -410,4 +410,9 @@ Vector 还没有图标 Vector 运行在别的进程里,而不是被安装到系统中,所以桌面上看不到它,也没有明显的途径再打开它。给它一个桌面快捷方式,或者把它安装成一个普通应用。 不再询问 + 为其他用户安装 + 把此模块添加到尚未安装它的用户空间,以便在那里设置作用域 + 选择要为其安装此模块的用户。 + 已为 %1$s 安装。 + 无法为 %1$s 安装。 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 76a243e73..ecb859b93 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -410,4 +410,9 @@ Vector 還沒有圖示 Vector 執行在別的程序裡,而不是被安裝到系統中,所以啟動器上看不到它,也沒有明顯的途徑再開啟它。給它一個主畫面捷徑,或者把它安裝成一般的應用程式。 不再詢問 + 為其他使用者安裝 + 把此模組加入尚未安裝它的使用者空間,以便在那裡設定作用域 + 選擇要為其安裝此模組的使用者。 + 已為 %1$s 安裝。 + 無法為 %1$s 安裝。 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index ec54516e1..71e7a97cd 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -464,4 +464,9 @@ Vector has no icon yet Vector runs inside another process rather than being installed, so nothing appears in your launcher and there is no obvious way back in. Give it a home screen shortcut, or install it as an ordinary app. Don\'t ask again + Install for another user + Puts this module into a profile that does not have it, so it can be scoped there + Choose the user to install this module for. + Installed for %1$s. + Could not install for %1$s. diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl index 19319ba12..21389a296 100644 --- a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl @@ -594,6 +594,26 @@ interface IManagerService { */ boolean uninstallPackage(String packageName, int userId); + /** + * Installs a package another user already holds into {@code userId}, without an APK. + * + *

Android keeps one APK per package name for the whole device and varies only who has it + * installed, so putting a module into a work profile is not a copy - it is the platform's own + * {@code installExistingPackageAsUser}, which flips the install state for that user and + * nothing else. The module keeps its version, its signature and its uid derivation; only the + * user's set of installed packages changes.

+ * + *

This is what the module list needs to be able to offer at all. A module reaches a + * profile's apps only from inside that profile, so a module the owner holds and the profile + * does not can never be scoped there - and the manager runs in one user, so without this call + * there is nothing in the product that can put it in the other. A profile with no modules is + * then a profile that can never get one.

+ * + * @return whether the platform reported the install. A user that does not exist, a package no + * user holds, and a device-policy refusal all come back false + */ + boolean installExistingPackageAsUser(String packageName, int userId); + /** * Clears an app's ART profiles and forces a profile-guided recompile. *