| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| @file:Suppress("DEPRECATION") |
| package com.android.permissioncontroller.permission.ui.model.v31 |
| |
| import android.Manifest |
| import android.Manifest.permission_group.CAMERA |
| import android.Manifest.permission_group.LOCATION |
| import android.Manifest.permission_group.MICROPHONE |
| import android.content.ComponentName |
| import android.content.Context |
| import android.content.Intent |
| import android.content.pm.PackageManager |
| import android.media.AudioManager |
| import android.media.AudioManager.MODE_IN_COMMUNICATION |
| import android.os.Bundle |
| import android.os.UserHandle |
| import android.provider.Settings |
| import android.speech.RecognitionService |
| import android.speech.RecognizerIntent |
| import android.telephony.TelephonyManager |
| import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS |
| import android.view.inputmethod.InputMethodManager |
| import androidx.lifecycle.AbstractSavedStateViewModelFactory |
| import androidx.lifecycle.SavedStateHandle |
| import androidx.lifecycle.ViewModel |
| import androidx.savedstate.SavedStateRegistryOwner |
| import com.android.permissioncontroller.PermissionControllerApplication |
| import com.android.permissioncontroller.permission.data.AttributionLabelLiveData |
| import com.android.permissioncontroller.permission.data.LoadAndFreezeLifeData |
| import com.android.permissioncontroller.permission.data.OpAccess |
| import com.android.permissioncontroller.permission.data.OpUsageLiveData |
| import com.android.permissioncontroller.permission.data.PermGroupUsageLiveData |
| import com.android.permissioncontroller.permission.data.SmartAsyncMediatorLiveData |
| import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData |
| import com.android.permissioncontroller.permission.data.micMutedLiveData |
| import com.android.permissioncontroller.permission.ui.handheld.v31.ReviewOngoingUsageFragment.PHONE_CALL |
| import com.android.permissioncontroller.permission.ui.handheld.v31.ReviewOngoingUsageFragment.VIDEO_CALL |
| import com.android.permissioncontroller.permission.utils.KotlinUtils |
| import com.android.permissioncontroller.permission.utils.KotlinUtils.shouldShowLocationIndicators |
| import com.android.permissioncontroller.permission.utils.KotlinUtils.shouldShowPermissionsDashboard |
| import com.android.permissioncontroller.permission.utils.Utils |
| import java.time.Instant |
| import kotlin.math.max |
| import kotlinx.coroutines.Job |
| |
| private const val FIRST_OPENED_KEY = "FIRST_OPENED" |
| private const val CALL_OP_USAGE_KEY = "CALL_OP_USAGE" |
| private const val USAGES_KEY = "USAGES_KEY" |
| private const val MIC_MUTED_KEY = "MIC_MUTED_KEY" |
| |
| /** |
| * ViewModel for {@link ReviewOngoingUsageFragment} |
| */ |
| class ReviewOngoingUsageViewModel( |
| state: SavedStateHandle, |
| extraDurationMills: Long |
| ) : ViewModel() { |
| /** Time of oldest usages considered */ |
| private val startTime = max(state.get<Long>(FIRST_OPENED_KEY)!! - extraDurationMills, |
| Instant.EPOCH.toEpochMilli()) |
| |
| private val SYSTEM_PKG = "android" |
| |
| data class Usages( |
| /** attribution-res-id/packageName/user -> perm groups accessed */ |
| val appUsages: Map<PackageAttribution, Set<String>>, |
| /** Op-names of phone call accesses */ |
| val callUsages: Collection<String>, |
| /** A map of attribution, packageName and user -> list of attribution labels to show with |
| * microphone*/ |
| val shownAttributions: Map<PackageAttribution, List<CharSequence>> = emptyMap() |
| ) |
| |
| data class PackageAttribution( |
| val attributionTag: String?, |
| val packageName: String, |
| val user: UserHandle |
| ) { |
| fun pkgEq(other: PackageAttribution): Boolean { |
| return packageName == other.packageName && user == other.user |
| } |
| } |
| |
| /** |
| * Base permission usage that will filtered by SystemPermGroupUsages and |
| * UserSensitivePermGroupUsages. |
| * |
| * <p>Note: This does not use a cached live-data to avoid getting stale data |
| */ |
| private val permGroupUsages = LoadAndFreezeLifeData(state, USAGES_KEY, |
| PermGroupUsageLiveData(PermissionControllerApplication.get(), |
| if (shouldShowPermissionsDashboard() || shouldShowLocationIndicators()) { |
| listOf(CAMERA, LOCATION, MICROPHONE) |
| } else { |
| listOf(CAMERA, MICROPHONE) |
| }, System.currentTimeMillis() - startTime)) |
| |
| /** |
| * Whether the mic is muted |
| */ |
| private val isMicMuted = LoadAndFreezeLifeData(state, MIC_MUTED_KEY, micMutedLiveData) |
| |
| /** App runtime permission usages */ |
| private val appUsagesLiveData = object : SmartUpdateMediatorLiveData<Map<PackageAttribution, |
| Set<String>>>() { |
| private val app = PermissionControllerApplication.get() |
| |
| init { |
| addSource(permGroupUsages) { |
| update() |
| } |
| |
| addSource(isMicMuted) { |
| update() |
| } |
| } |
| |
| override fun onUpdate() { |
| if (!permGroupUsages.isInitialized || !isMicMuted.isInitialized) { |
| return |
| } |
| |
| if (permGroupUsages.value == null) { |
| value = null |
| return |
| } |
| |
| // Filter out system package |
| val filteredUsages = mutableMapOf<PackageAttribution, MutableSet<String>>() |
| for ((permGroupName, usages) in permGroupUsages.value!!) { |
| if (permGroupName == MICROPHONE && isMicMuted.value == true) { |
| continue |
| } |
| |
| for (usage in usages) { |
| if (usage.packageName != SYSTEM_PKG) { |
| filteredUsages.getOrPut(getPackageAttr(usage), |
| { mutableSetOf() }).add(permGroupName) |
| } |
| } |
| } |
| |
| value = filteredUsages |
| } |
| |
| // TODO ntmyren: Replace this with better check if this moves beyond teamfood |
| private fun isAppPredictor(usage: OpAccess): Boolean { |
| return Utils.getUserContext(app, usage.user).packageManager.checkPermission( |
| Manifest.permission.MANAGE_APP_PREDICTIONS, usage.packageName) == |
| PackageManager.PERMISSION_GRANTED |
| } |
| } |
| |
| /** |
| * Gets all trusted proxied voice IME and voice recognition microphone uses, and get the |
| * label needed to display with it, as well as information about the proxy whose label is being |
| * shown, if applicable. |
| */ |
| private val trustedAttrsLiveData = object : SmartAsyncMediatorLiveData< |
| Map<PackageAttribution, CharSequence>>() { |
| private val VOICE_IME_SUBTYPE = "voice" |
| |
| private val attributionLabelLiveDatas = |
| mutableMapOf<Triple<String?, String, UserHandle>, AttributionLabelLiveData>() |
| |
| init { |
| addSource(permGroupUsages) { |
| updateAsync() |
| } |
| } |
| |
| override suspend fun loadDataAndPostValue(job: Job) { |
| if (!permGroupUsages.isInitialized) { |
| return |
| } |
| val usages = permGroupUsages.value?.get(MICROPHONE) ?: run { |
| postValue(emptyMap()) |
| return |
| } |
| val proxies = usages.mapNotNull { it.proxyAccess } |
| |
| val proxyLabelLiveDatas = proxies.map { |
| Triple(it.attributionTag, it.packageName, it.user) } |
| val toAddLabelLiveDatas = (usages.map { Triple(it.attributionTag, it.packageName, |
| it.user) } + proxyLabelLiveDatas).distinct() |
| val getLiveDataFun = { key: Triple<String?, String, UserHandle> -> |
| AttributionLabelLiveData[key] } |
| setSourcesToDifference(toAddLabelLiveDatas, attributionLabelLiveDatas, getLiveDataFun) |
| |
| if (attributionLabelLiveDatas.any { !it.value.isInitialized }) { |
| return |
| } |
| |
| val approvedAttrs = mutableMapOf<PackageAttribution, String>() |
| for (user in usages.map { it.user }.distinct()) { |
| val userContext = Utils.getUserContext(PermissionControllerApplication.get(), user) |
| |
| // TODO ntmyren: Observe changes, possibly split into separate LiveDatas |
| val voiceInputs = mutableMapOf<String, CharSequence>() |
| userContext.getSystemService(InputMethodManager::class.java)!! |
| .enabledInputMethodList.forEach { |
| for (i in 0 until it.subtypeCount) { |
| if (it.getSubtypeAt(i).mode == VOICE_IME_SUBTYPE) { |
| voiceInputs[it.packageName] = |
| it.serviceInfo.loadSafeLabel(userContext.packageManager, |
| Float.MAX_VALUE, 0) |
| break |
| } |
| } |
| } |
| |
| // Get the currently selected recognizer from the secure setting. |
| val recognitionPackageName = Settings.Secure.getString(userContext.contentResolver, |
| // Settings.Secure.VOICE_RECOGNITION_SERVICE |
| "voice_recognition_service") |
| ?.let(ComponentName::unflattenFromString)?.packageName |
| |
| val recognizers = mutableMapOf<String, CharSequence>() |
| val availableRecognizers = userContext.packageManager.queryIntentServices( |
| Intent(RecognitionService.SERVICE_INTERFACE), PackageManager.GET_META_DATA) |
| availableRecognizers.forEach { |
| val sI = it.serviceInfo |
| if (sI.packageName == recognitionPackageName) { |
| recognizers[sI.packageName] = sI.loadSafeLabel(userContext.packageManager, |
| Float.MAX_VALUE, 0) |
| } |
| } |
| |
| val recognizerIntents = mutableMapOf<String, CharSequence>() |
| val availableRecognizerIntents = userContext.packageManager.queryIntentActivities( |
| Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), PackageManager.GET_META_DATA) |
| availableRecognizers.forEach { rI -> |
| val servicePkg = rI.serviceInfo.packageName |
| if (servicePkg == recognitionPackageName && availableRecognizerIntents.any { |
| it.activityInfo.packageName == servicePkg }) { |
| // If this recognizer intent is also a recognizer service, and is trusted, |
| // Then attribute to voice recognition |
| recognizerIntents[servicePkg] = |
| rI.serviceInfo.loadSafeLabel(userContext.packageManager, |
| Float.MAX_VALUE, 0) |
| } |
| } |
| |
| // get attribution labels for voice IME, recognition intents, and recognition |
| // services |
| for (opAccess in usages) { |
| setTrustedAttrsForAccess(userContext, opAccess, user, false, voiceInputs, |
| approvedAttrs) |
| setTrustedAttrsForAccess(userContext, opAccess, user, false, recognizerIntents, |
| approvedAttrs) |
| setTrustedAttrsForAccess(userContext, opAccess, user, true, recognizers, |
| approvedAttrs) |
| } |
| } |
| postValue(approvedAttrs) |
| } |
| |
| private fun setTrustedAttrsForAccess( |
| context: Context, |
| opAccess: OpAccess, |
| currUser: UserHandle, |
| getProxyLabel: Boolean, |
| trustedMap: Map<String, CharSequence>, |
| toSetMap: MutableMap<PackageAttribution, String> |
| ) { |
| val access = if (getProxyLabel) { |
| opAccess.proxyAccess |
| } else { |
| opAccess |
| } |
| |
| if (access == null || access.user != currUser || access.packageName !in trustedMap) { |
| return |
| } |
| |
| val appAttr = getPackageAttr(access) |
| val packageName = access.packageName |
| |
| val labelResId = attributionLabelLiveDatas[Triple(access.attributionTag, |
| access.packageName, access.user)]?.value ?: 0 |
| val label = try { |
| context.createPackageContext(packageName, 0) |
| .getString(labelResId) |
| } catch (e: Exception) { |
| return |
| } |
| if (trustedMap[packageName] == label) { |
| toSetMap[appAttr] = label |
| } |
| } |
| } |
| |
| /** |
| * Get all chains of proxy usages. A proxy chain is defined as one usage at the root, then |
| * further proxy usages, where the app and attribution tag of the proxy matches the previous |
| * usage in the chain. |
| */ |
| private val proxyChainsLiveData = object : SmartUpdateMediatorLiveData<Set<List<OpAccess>>>() { |
| init { |
| addSource(permGroupUsages) { |
| update() |
| } |
| } |
| override fun onUpdate() { |
| if (!permGroupUsages.isInitialized) { |
| return |
| } |
| val usages = permGroupUsages.value?.get(MICROPHONE) ?: emptyList() |
| // a map of chain start -> in progress chain |
| val proxyChains = mutableMapOf<PackageAttribution, MutableList<OpAccess>>() |
| |
| val remainingProxyChainUsages = mutableMapOf<PackageAttribution, OpAccess>() |
| for (usage in usages) { |
| remainingProxyChainUsages[getPackageAttr(usage)] = usage |
| } |
| // find all one-link chains (that is, all proxied apps whose proxy is not included in |
| // the usage list) |
| for (usage in usages) { |
| val usageAttr = getPackageAttr(usage) |
| val proxyAttr = getPackageAttr(usage.proxyAccess ?: continue) |
| if (!usages.any { getPackageAttr(it) == proxyAttr }) { |
| proxyChains[usageAttr] = mutableListOf(usage) |
| remainingProxyChainUsages.remove(usageAttr) |
| } |
| } |
| |
| // find all possible starting points for chains |
| for ((usageAttr, usage) in remainingProxyChainUsages.toMap()) { |
| // If this usage has a proxy, but is not a proxy, it is the start of a chain. |
| // If it has no proxy, and isn't a proxy, remove it. |
| if (!remainingProxyChainUsages.values.any { it.proxyAccess != null && |
| getPackageAttr(it.proxyAccess) == usageAttr }) { |
| if (usage.proxyAccess != null) { |
| proxyChains[usageAttr] = mutableListOf(usage) |
| } else { |
| remainingProxyChainUsages.remove(usageAttr) |
| } |
| } |
| } |
| |
| // assemble the chains |
| for ((startUsageAttr, proxyChain) in proxyChains) { |
| var currentUsage = remainingProxyChainUsages[startUsageAttr] ?: continue |
| while (currentUsage.proxyAccess != null) { |
| val currPackageAttr = getPackageAttr(currentUsage.proxyAccess!!) |
| currentUsage = remainingProxyChainUsages[currPackageAttr] ?: break |
| if (proxyChain.any { it == currentUsage }) { |
| // we have a cycle, and should break |
| break |
| } |
| proxyChain.add(currentUsage) |
| } |
| // invert the lists, so the element without a proxy is first on the list |
| proxyChain.reverse() |
| } |
| |
| value = proxyChains.values.toSet() |
| } |
| } |
| |
| /** Phone call usages */ |
| private val callOpUsageLiveData = |
| object : SmartUpdateMediatorLiveData<Collection<String>>() { |
| private val rawOps = LoadAndFreezeLifeData(state, CALL_OP_USAGE_KEY, |
| OpUsageLiveData[listOf(PHONE_CALL, VIDEO_CALL), |
| System.currentTimeMillis() - startTime]) |
| |
| init { |
| addSource(rawOps) { |
| update() |
| } |
| |
| addSource(isMicMuted) { |
| update() |
| } |
| } |
| |
| override fun onUpdate() { |
| if (!isMicMuted.isInitialized || !rawOps.isInitialized) { |
| return |
| } |
| |
| value = if (isMicMuted.value == true) { |
| rawOps.value!!.keys.filter { it != PHONE_CALL } |
| } else { |
| rawOps.value!!.keys |
| } |
| } |
| } |
| |
| /** App, system, and call usages in a single, nice, handy package */ |
| val usages = object : SmartAsyncMediatorLiveData<Usages>() { |
| private val app = PermissionControllerApplication.get() |
| |
| init { |
| addSource(appUsagesLiveData) { |
| update() |
| } |
| |
| addSource(callOpUsageLiveData) { |
| update() |
| } |
| |
| addSource(trustedAttrsLiveData) { |
| update() |
| } |
| |
| addSource(proxyChainsLiveData) { |
| update() |
| } |
| } |
| |
| override suspend fun loadDataAndPostValue(job: Job) { |
| if (job.isCancelled) { |
| return |
| } |
| |
| if (!callOpUsageLiveData.isInitialized || !appUsagesLiveData.isInitialized || |
| !trustedAttrsLiveData.isInitialized || !proxyChainsLiveData.isInitialized) { |
| return |
| } |
| |
| val callOpUsages = callOpUsageLiveData.value?.toMutableSet() |
| val appUsages = appUsagesLiveData.value?.toMutableMap() |
| val approvedAttrs = trustedAttrsLiveData.value?.toMutableMap() ?: mutableMapOf() |
| val proxyChains = proxyChainsLiveData.value ?: emptySet() |
| |
| if (callOpUsages == null || appUsages == null) { |
| postValue(null) |
| return |
| } |
| |
| // If there is nothing to show the dialog should be closed, hence return a "invalid" |
| // value |
| if (appUsages.isEmpty() && callOpUsages.isEmpty()) { |
| postValue(null) |
| return |
| } |
| |
| // If we are in a VOIP call (aka MODE_IN_COMMUNICATION), and have a carrier privileged |
| // app using the mic, hide phone usage. |
| val audioManager = app.getSystemService(AudioManager::class.java)!! |
| if (callOpUsages.isNotEmpty() && audioManager.mode == MODE_IN_COMMUNICATION) { |
| val telephonyManager = app.getSystemService(TelephonyManager::class.java)!! |
| for ((pkg, usages) in appUsages) { |
| if (telephonyManager.checkCarrierPrivilegesForPackage(pkg.packageName) == |
| CARRIER_PRIVILEGE_STATUS_HAS_ACCESS && usages.contains(MICROPHONE)) { |
| callOpUsages.clear() |
| continue |
| } |
| } |
| } |
| |
| // Find labels for proxies, and assign them to the proper app, removing other usages |
| val approvedLabels = mutableMapOf<PackageAttribution, List<CharSequence>>() |
| for (chain in proxyChains) { |
| // if the final link in the chain is not user sensitive, do not show the chain |
| if (getPackageAttr(chain[chain.size - 1]) !in appUsages) { |
| continue |
| } |
| |
| // if the proxy access is missing, for some reason, do not show the proxy |
| if (chain.size == 1) { |
| continue |
| } |
| |
| val labels = mutableListOf<CharSequence>() |
| for ((idx, opAccess) in chain.withIndex()) { |
| val appAttr = getPackageAttr(opAccess) |
| // If this is the last link in the proxy chain, assign it the series of labels |
| // Else, if it has a special label, add that label |
| // Else, if there are no other apps in the remaining part of the chain which |
| // have the same package name, add the app label |
| // If it is not the last link in the chain, remove its attribution |
| if (idx == chain.size - 1) { |
| approvedLabels[appAttr] = labels |
| continue |
| } else if (appAttr in approvedAttrs) { |
| labels.add(approvedAttrs[appAttr]!!) |
| approvedAttrs.remove(appAttr) |
| } else if (chain.subList(idx + 1, chain.size).all { |
| it.packageName != opAccess.packageName } && |
| opAccess.packageName != SYSTEM_PKG) { |
| labels.add(KotlinUtils.getPackageLabel(app, opAccess.packageName, |
| opAccess.user)) |
| } |
| appUsages.remove(appAttr) |
| } |
| } |
| |
| // Any remaining truested attributions must be for non-proxy usages, so add them |
| for ((packageAttr, label) in approvedAttrs) { |
| approvedLabels[packageAttr] = listOf(label) |
| } |
| |
| removeDuplicates(appUsages, approvedLabels.keys) |
| |
| postValue(Usages(appUsages, callOpUsages, approvedLabels)) |
| } |
| |
| /** |
| * Merge any usages for the same app which don't have a special attribution |
| */ |
| private fun removeDuplicates( |
| appUsages: MutableMap<PackageAttribution, Set<String>>, |
| approvedUsages: Collection<PackageAttribution> |
| ) { |
| // Iterate over all non-special attribution keys |
| for (packageAttr in appUsages.keys.minus(approvedUsages)) { |
| var groupSet = appUsages[packageAttr] ?: continue |
| |
| for (otherAttr in appUsages.keys.minus(approvedUsages)) { |
| if (otherAttr.pkgEq(packageAttr)) { |
| groupSet = groupSet.plus(appUsages[otherAttr] ?: emptySet()) |
| appUsages.remove(otherAttr) |
| } |
| } |
| appUsages[packageAttr] = groupSet |
| } |
| } |
| } |
| |
| private fun getPackageAttr(usage: OpAccess): PackageAttribution { |
| return PackageAttribution(usage.attributionTag, usage.packageName, usage.user) |
| } |
| } |
| |
| /** |
| * Factory for a ReviewOngoingUsageViewModel |
| * |
| * @param extraDurationMillis The number of milliseconds old usages are considered for |
| * @param owner The owner of this saved state |
| * @param defaultArgs The default args to pass |
| */ |
| class ReviewOngoingUsageViewModelFactory( |
| private val extraDurationMillis: Long, |
| owner: SavedStateRegistryOwner, |
| defaultArgs: Bundle |
| ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { |
| override fun <T : ViewModel> create( |
| key: String, |
| modelClass: Class<T>, |
| handle: SavedStateHandle |
| ): T { |
| handle.set(FIRST_OPENED_KEY, handle.get<Long>(FIRST_OPENED_KEY) |
| ?: System.currentTimeMillis()) |
| @Suppress("UNCHECKED_CAST") |
| return ReviewOngoingUsageViewModel(handle, extraDurationMillis) as T |
| } |
| } |