Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9d98487d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[Catalyst] Move AppOpsModeObservable" into main

parents e1afc48c 9a3b9216
Loading
Loading
Loading
Loading
+0 −119
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.appops

import android.app.AppOpsManager
import android.app.AppOpsManager.OnOpChangedListener
import android.content.Context
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.collection.MutableIntObjectMap
import com.android.settingslib.datastore.AbstractKeyedDataObservable
import com.android.settingslib.datastore.DataChangeReason
import com.android.settingslib.datastore.KeyedObservable
import com.android.settingslib.datastore.KeyedObserver
import java.util.concurrent.Executor

@GuardedBy("itself")
@VisibleForTesting
internal val appOpsModeObservables = MutableIntObjectMap<KeyedObservable<String>>()

/**
 * Adds an observer to monitor app-ops mode change on given operation. To avoid memory leak,
 * [removeAppOpsModeObserver] must be invoked sometime later.
 *
 * As an improvement, there is only one [OnOpChangedListener] registered to [AppOpsManager] for the
 * same [op].
 *
 * @param op the `AppOpsManager.OP_XXX` operation to monitor
 * @param observer observer of the mode change (callback key is package name)
 * @param executor executor to run the callback
 */
fun Context.addAppOpsModeObserver(op: Int, observer: KeyedObserver<String?>, executor: Executor) =
    appOpsModeObservable(op).addObserver(observer, executor)

/**
 * Adds an observer to monitor app-ops mode change on given operation and package. To avoid memory
 * leak, [removeAppOpsModeObserver] must be invoked sometime later.
 *
 * As an improvement, there is only one [OnOpChangedListener] registered to [AppOpsManager] for the
 * same [op] even if several observers are added for different packages.
 *
 * @param op the `AppOpsManager.OP_XXX` operation to monitor
 * @param packageName package to monitor the mode change
 * @param observer observer of the mode change (callback key is package name)
 * @param executor executor to run the callback
 */
fun Context.addAppOpsModeObserver(
    op: Int,
    packageName: String,
    observer: KeyedObserver<String>,
    executor: Executor,
) = appOpsModeObservable(op).addObserver(packageName, observer, executor)

/** Removes the observer added by [addAppOpsModeObserver]. */
fun Context.removeAppOpsModeObserver(op: Int, observer: KeyedObserver<String?>) =
    appOpsModeObservable(op).removeObserver(observer)

/** Removes the observer added by [addAppOpsModeObserver]. */
fun Context.removeAppOpsModeObserver(
    op: Int,
    packageName: String,
    observer: KeyedObserver<String>,
) = appOpsModeObservable(op).removeObserver(packageName, observer)

/**
 * Returns a shared [KeyedObservable] to monitor app-ops mode change on given operation. Only a
 * single [OnOpChangedListener] will be registered to [AppOpsManager] for the same [op].
 *
 * Notes:
 * - The observer key is package name.
 * - It is not recommend to save the returned object (e.g. keep in a field, pass around lambda),
 *   just invoke this method everytime. This is to avoid multiple instances of [KeyedObservable]
 *   created for the same [op]. Hence use [addAppOpsModeObserver] and [removeAppOpsModeObserver]
 *   whenever possible.
 */
internal fun Context.appOpsModeObservable(op: Int): KeyedObservable<String> =
    synchronized(appOpsModeObservables) {
        appOpsModeObservables.getOrPut(op) { AppOpsModeObservable(applicationContext, op) }
    }

/**
 * Class to monitor app-ops mode change on given operation.
 *
 * This class helps to reduce the number of [OnOpChangedListener] registered to [AppOpsManager]. No
 * matter how many packages are observed, there is only one [OnOpChangedListener].
 */
private class AppOpsModeObservable(private val appContext: Context, private val op: Int) :
    AbstractKeyedDataObservable<String>(), OnOpChangedListener {

    private val appOpsManager: AppOpsManager
        get() = appContext.getSystemService(AppOpsManager::class.java)!!

    override fun onOpChanged(op: String, packageName: String) {
        notifyChange(packageName, DataChangeReason.UPDATE)
    }

    override fun onFirstObserverAdded() {
        appOpsManager.startWatchingMode(op, null, this)
    }

    override fun onLastObserverRemoved() {
        appOpsManager.stopWatchingMode(this)
        synchronized(appOpsModeObservables) { appOpsModeObservables.remove(op, this) }
    }
}
+0 −103
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.appops

import android.app.AppOpsManager
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.datastore.DataChangeReason
import com.android.settingslib.datastore.HandlerExecutor
import com.android.settingslib.datastore.KeyedObservable
import com.android.settingslib.datastore.KeyedObserver
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

@RunWith(AndroidJUnit4::class)
class AppOpsModeObservableTest {
    private val context: Context = ApplicationProvider.getApplicationContext()
    private val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
    private val executor = HandlerExecutor.main
    private val op = AppOpsManager.OP_GPS

    @Test
    fun observerNotified() {
        val pkg1 = "pkg1"
        val pkg2 = "pkg2"
        val observer1: KeyedObserver<String> = mock()
        val observer2: KeyedObserver<String> = mock()
        val anyPkgObserver: KeyedObserver<String?> = mock()

        context.addAppOpsModeObserver(op, anyPkgObserver, executor)
        val observable1 = context.appOpsModeObservable(op)
        context.addAppOpsModeObserver(op, pkg1, observer1, executor)
        context.addAppOpsModeObserver(op, pkg2, observer2, executor)
        val observable2 = context.appOpsModeObservable(op)
        try {
            assertThat(observable1).isSameInstanceAs(observable2)

            appOpsManager.setMode(op, 0, pkg1, AppOpsManager.MODE_ERRORED)
            appOpsManager.setMode(op, 0, pkg1, AppOpsManager.MODE_ERRORED) // mode unchanged
            appOpsManager.setMode(op, 0, pkg2, AppOpsManager.MODE_ERRORED)
            appOpsManager.setMode(op, 0, pkg2, AppOpsManager.MODE_ALLOWED)

            verify(observer1).onKeyChanged(pkg1, DataChangeReason.UPDATE)
            verify(anyPkgObserver).onKeyChanged(pkg1, DataChangeReason.UPDATE)
            verify(observer2, times(2)).onKeyChanged(pkg2, DataChangeReason.UPDATE)
            verify(anyPkgObserver, times(2)).onKeyChanged(pkg2, DataChangeReason.UPDATE)
        } finally {
            context.removeAppOpsModeObserver(op, anyPkgObserver)
            context.removeAppOpsModeObserver(op, pkg1, observer1)
            context.removeAppOpsModeObserver(op, pkg2, observer2)
        }
        assertThat(appOpsModeObservables.size).isEqualTo(0)
    }

    @Test
    fun appOpsModeObservable_multipleInstances() {
        val observable1 = context.appOpsModeObservable(op)
        observable1.verifyObserverNotified(0) // observable is released due to no observer

        // a new observable is created
        val observable2 = context.appOpsModeObservable(op)
        assertThat(observable1).isNotSameInstanceAs(observable2)

        // the old observable should still work
        observable1.verifyObserverNotified(1, AppOpsManager.MODE_ALLOWED) // use a different mode
        // the new observable also works
        observable2.verifyObserverNotified(0)
    }

    private fun KeyedObservable<String>.verifyObserverNotified(
        size: Int,
        mode: Int = AppOpsManager.MODE_ERRORED,
    ) {
        val observer: KeyedObserver<String> = mock()
        addObserver("pkg", observer, executor)
        try {
            appOpsManager.setMode(op, 0, "pkg", mode)
            verify(observer).onKeyChanged("pkg", DataChangeReason.UPDATE)
        } finally {
            removeObserver("pkg", observer)
        }
        assertThat(appOpsModeObservables.size).isEqualTo(size)
    }
}