/* * Copyright (C) 2022 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. */ package com.android.settings.spa.app.specialaccess import android.app.AppOpsManager import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager.GET_ACTIVITIES import android.content.pm.PackageManager.PackageInfoFlags import android.util.Log import androidx.compose.runtime.Composable import com.android.settings.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.installed import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListModel import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map object PictureInPictureListProvider : TogglePermissionAppListProvider { override val permissionType = "PictureInPicture" override fun createModel(context: Context) = PictureInPictureListModel(context) } data class PictureInPictureRecord( override val app: ApplicationInfo, val isSupport: Boolean, val appOpsController: AppOpsController, ) : AppRecord class PictureInPictureListModel(private val context: Context) : TogglePermissionAppListModel { override val pageTitleResId = R.string.picture_in_picture_title override val switchTitleResId = R.string.picture_in_picture_app_detail_switch override val footerResId = R.string.picture_in_picture_app_detail_summary override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_PICTURE_IN_PICTURE private val packageManager = context.packageManager override fun transform(userIdFlow: Flow, appListFlow: Flow>) = userIdFlow.map(::getPictureInPicturePackages) .combine(appListFlow) { pictureInPicturePackages, appList -> appList.map { app -> createPictureInPictureRecord( app = app, isSupport = app.packageName in pictureInPicturePackages, ) } } override fun transformItem(app: ApplicationInfo) = createPictureInPictureRecord( app = app, isSupport = app.installed && getPackageAndActivityInfo(app)?.supportsPictureInPicture() == true, ) private fun createPictureInPictureRecord(app: ApplicationInfo, isSupport: Boolean) = PictureInPictureRecord( app = app, isSupport = isSupport, appOpsController = AppOpsController(context = context, app = app, appOps = APP_OPS), ) override fun filter(userIdFlow: Flow, recordListFlow: Flow>) = recordListFlow.map { recordList -> recordList.filter { it.isSupport } } @Composable override fun isAllowed(record: PictureInPictureRecord) = record.appOpsController.isAllowed.collectAsCallbackWithLifecycle() override fun isChangeable(record: PictureInPictureRecord) = record.isSupport override fun setAllowed(record: PictureInPictureRecord, newAllowed: Boolean) { record.appOpsController.setAllowed(newAllowed) } private fun getPictureInPicturePackages(userId: Int): Set = getPackageAndActivityInfoList(userId) .filter { it.supportsPictureInPicture() } .map { it.packageName } .toSet() private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try { packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId) } catch (e: Exception) { // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture // packages, we need to catch the exception to alleviate the impact before PackageManager // fixing this issue or provide a better api. Log.e(TAG, "Exception while getPackageInfoAsUser", e) null } private fun getPackageAndActivityInfoList(userId: Int): List = try { packageManager.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId) } catch (e: Exception) { // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture // packages, we need to catch the exception to alleviate the impact before PackageManager // fixing this issue or provide a better api. Log.e(TAG, "Exception while getInstalledPackagesAsUser", e) emptyList() } companion object { private const val TAG = "PictureInPictureListModel" private val APP_OPS = AppOps(AppOpsManager.OP_PICTURE_IN_PICTURE) private fun PackageInfo.supportsPictureInPicture() = activities?.any(ActivityInfo::supportsPictureInPicture) ?: false private val GET_ACTIVITIES_FLAGS = PackageInfoFlags.of(GET_ACTIVITIES.toLong()) } }