/* * 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(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>, /** Op-names of phone call accesses */ val callUsages: Collection, /** A map of attribution, packageName and user -> list of attribution labels to show with * microphone*/ val shownAttributions: Map> = 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. * *

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>>() { 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>() 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>() { private val VOICE_IME_SUBTYPE = "voice" private val attributionLabelLiveDatas = mutableMapOf, 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 -> AttributionLabelLiveData[key] } setSourcesToDifference(toAddLabelLiveDatas, attributionLabelLiveDatas, getLiveDataFun) if (attributionLabelLiveDatas.any { !it.value.isInitialized }) { return } val approvedAttrs = mutableMapOf() 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() 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() 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() 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, toSetMap: MutableMap ) { 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>>() { 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>() val remainingProxyChainUsages = mutableMapOf() 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>() { 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() { 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>() 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() 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>, approvedUsages: Collection ) { // 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 create( key: String, modelClass: Class, handle: SavedStateHandle ): T { handle.set(FIRST_OPENED_KEY, handle.get(FIRST_OPENED_KEY) ?: System.currentTimeMillis()) @Suppress("UNCHECKED_CAST") return ReviewOngoingUsageViewModel(handle, extraDurationMillis) as T } }