diff options
10 files changed, 576 insertions, 33 deletions
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java index 02e80d611f3f..f652b33b3fd3 100644 --- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java @@ -16,12 +16,14 @@ package com.android.server.audio; +import com.android.media.permission.INativePermissionController; /** * Facade to IAudioPolicyService which fulfills AudioService dependencies. * See @link{IAudioPolicyService.aidl} */ public interface AudioPolicyFacade { - public boolean isHotwordStreamSupported(boolean lookbackAudio); + public INativePermissionController getPermissionController(); + public void registerOnStartTask(Runnable r); } diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java new file mode 100644 index 000000000000..5ea3c4bf538d --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 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.server.audio; + +import android.annotation.Nullable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArraySet; + +import com.android.internal.annotations.GuardedBy; +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** Responsible for synchronizing system server permission state to the native audioserver. */ +public class AudioServerPermissionProvider { + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private INativePermissionController mDest; + + @GuardedBy("mLock") + private final Map<Integer, Set<String>> mPackageMap; + + /** + * @param appInfos - PackageState for all apps on the device, used to populate init state + */ + public AudioServerPermissionProvider(Collection<PackageState> appInfos) { + // Initialize the package state + mPackageMap = generatePackageMappings(appInfos); + } + + /** + * Called whenever audioserver starts (or started before us) + * + * @param pc - The permission controller interface from audioserver, which we push updates to + */ + public void onServiceStart(@Nullable INativePermissionController pc) { + if (pc == null) return; + synchronized (mLock) { + mDest = pc; + resetNativePackageState(); + } + } + + /** + * Called when a package is added or removed + * + * @param uid - uid of modified package (only app-id matters) + * @param packageName - the (new) packageName + * @param isRemove - true if the package is being removed, false if it is being added + */ + public void onModifyPackageState(int uid, String packageName, boolean isRemove) { + // No point in maintaining package mappings for uids of different users + uid = UserHandle.getAppId(uid); + synchronized (mLock) { + // Update state + Set<String> packages; + if (!isRemove) { + packages = mPackageMap.computeIfAbsent(uid, unused -> new ArraySet(1)); + packages.add(packageName); + } else { + packages = mPackageMap.get(uid); + if (packages != null) { + packages.remove(packageName); + if (packages.isEmpty()) mPackageMap.remove(uid); + } + } + // Push state to destination + if (mDest == null) { + return; + } + var state = new UidPackageState(); + state.uid = uid; + state.packageNames = packages != null ? List.copyOf(packages) : Collections.emptyList(); + try { + mDest.updatePackagesForUid(state); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + } + + /** Called when full syncing package state to audioserver. */ + @GuardedBy("mLock") + private void resetNativePackageState() { + if (mDest == null) return; + List<UidPackageState> states = + mPackageMap.entrySet().stream() + .map( + entry -> { + UidPackageState state = new UidPackageState(); + state.uid = entry.getKey(); + state.packageNames = List.copyOf(entry.getValue()); + return state; + }) + .toList(); + try { + mDest.populatePackagesForUids(states); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + + /** + * Aggregation operation on all package states list: groups by states by app-id and merges the + * packageName for each state into an ArraySet. + */ + private static Map<Integer, Set<String>> generatePackageMappings( + Collection<PackageState> appInfos) { + Collector<PackageState, Object, Set<String>> reducer = + Collectors.mapping( + (PackageState p) -> p.getPackageName(), + Collectors.toCollection(() -> new ArraySet(1))); + + return appInfos.stream() + .collect( + Collectors.groupingBy( + /* predicate */ (PackageState p) -> p.getAppId(), + /* factory */ HashMap::new, + /* downstream collector */ reducer)); + } +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 68347a085be1..ef65b2523024 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -31,6 +31,10 @@ import static android.Manifest.permission.MODIFY_PHONE_STATE; import static android.Manifest.permission.QUERY_AUDIO_STATE; import static android.Manifest.permission.WRITE_SETTINGS; import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_ARCHIVAL; +import static android.content.Intent.EXTRA_REPLACING; import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; @@ -59,6 +63,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; +import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; @@ -240,15 +245,18 @@ import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent; import com.android.server.audio.AudioServiceEvents.PhoneStateEvent; import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent; import com.android.server.audio.AudioServiceEvents.VolumeEvent; +import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; import com.android.server.pm.UserManagerService; +import com.android.server.pm.pkg.PackageState; import com.android.server.utils.EventLogger; import com.android.server.wm.ActivityTaskManagerInternal; @@ -273,6 +281,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; @@ -303,6 +312,8 @@ public class AudioService extends IAudioService.Stub private final SettingsAdapter mSettings; private final AudioPolicyFacade mAudioPolicy; + private final AudioServerPermissionProvider mPermissionProvider; + private final MusicFxHelper mMusicFxHelper; /** Debug audio mode */ @@ -1021,14 +1032,22 @@ public class AudioService extends IAudioService.Stub public Lifecycle(Context context) { super(context); + var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor(); + var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor); mService = new AudioService(context, AudioSystemAdapter.getDefaultAdapter(), SystemServerAdapter.getDefaultAdapter(context), SettingsAdapter.getDefaultAdapter(), new AudioVolumeGroupHelper(), - new DefaultAudioPolicyFacade(r -> r.run()), - null); - + audioPolicyFacade, + null, + context.getSystemService(AppOpsManager.class), + PermissionEnforcer.fromContext(context), + audioserverPermissions() ? + initializeAudioServerPermissionProvider( + context, audioPolicyFacade, audioserverLifecycleExecutor) : + null + ); } @Override @@ -1105,25 +1124,6 @@ public class AudioService extends IAudioService.Stub /** * @param context * @param audioSystem Adapter for {@link AudioSystem} - * @param systemServer Adapter for privileged functionality for system server components - * @param settings Adapter for {@link Settings} - * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} - * @param audioPolicy Interface of a facade to IAudioPolicyManager - * @param looper Looper to use for the service's message handler. If this is null, an - * {@link AudioSystemThread} is created as the messaging thread instead. - */ - public AudioService(Context context, AudioSystemAdapter audioSystem, - SystemServerAdapter systemServer, SettingsAdapter settings, - AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper) { - this (context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, context.getSystemService(AppOpsManager.class), - PermissionEnforcer.fromContext(context)); - } - - /** - * @param context - * @param audioSystem Adapter for {@link AudioSystem} * @param systemServer Adapter for privilieged functionality for system server components * @param settings Adapter for {@link Settings} * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} @@ -1137,13 +1137,16 @@ public class AudioService extends IAudioService.Stub public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer) { + @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, + /* @NonNull */ AudioServerPermissionProvider permissionProvider) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; mContentResolver = context.getContentResolver(); mAppOps = appOps; + mPermissionProvider = permissionProvider; + mAudioSystem = audioSystem; mSystemServer = systemServer; mAudioVolumeGroupHelper = audioVolumeGroupHelper; @@ -4559,6 +4562,8 @@ public class AudioService extends IAudioService.Stub + featureSpatialAudioHeadtrackingLowLatency()); pw.println("\tandroid.media.audio.focusFreezeTestApi:" + focusFreezeTestApi()); + pw.println("\tcom.android.media.audio.audioserverPermissions:" + + audioserverPermissions()); pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); pw.println("\tcom.android.media.audio.setStreamVolumeOrder:" @@ -11937,6 +11942,45 @@ public class AudioService extends IAudioService.Stub private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE + MediaMetrics.SEPARATOR; + private static AudioServerPermissionProvider initializeAudioServerPermissionProvider( + Context context, AudioPolicyFacade audioPolicy, Executor audioserverExecutor) { + Collection<PackageState> packageStates = null; + try (PackageManagerLocal.UnfilteredSnapshot snapshot = + LocalManagerRegistry.getManager(PackageManagerLocal.class) + .withUnfilteredSnapshot()) { + packageStates = snapshot.getPackageStates().values(); + } + var provider = new AudioServerPermissionProvider(packageStates); + audioPolicy.registerOnStartTask(() -> { + provider.onServiceStart(audioPolicy.getPermissionController()); + }); + + // Set up event listeners + IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); + packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); + packageUpdateFilter.addDataScheme("package"); + + context.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String pkgName = intent.getData().getEncodedSchemeSpecificPart(); + int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + if (intent.getBooleanExtra(EXTRA_REPLACING, false) || + intent.getBooleanExtra(EXTRA_ARCHIVAL, false)) return; + if (action.equals(ACTION_PACKAGE_ADDED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, false /* isRemoved */)); + } else if (action.equals(ACTION_PACKAGE_REMOVED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, true /* isRemoved */)); + } + } + }, packageUpdateFilter, null, null); // main thread is fine, since dispatch on executor + return provider; + } + // Inform AudioFlinger of our device's low RAM attribute private static void readAndSetLowRamDevice() { diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java index 75febbc1cf9c..09701e49a8ac 100644 --- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java @@ -16,10 +16,15 @@ package com.android.server.audio; +import android.annotation.Nullable; import android.media.IAudioPolicyService; +import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import com.android.media.permission.INativePermissionController; + +import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Function; @@ -43,6 +48,7 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade { (Function<IBinder, IAudioPolicyService>) IAudioPolicyService.Stub::asInterface, e); + mServiceHolder.registerOnStartTask(i -> Binder.allowBlocking(i.asBinder())); } @Override @@ -55,4 +61,23 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade { throw new IllegalStateException(); } } + + @Override + public @Nullable INativePermissionController getPermissionController() { + IAudioPolicyService ap = mServiceHolder.checkService(); + if (ap == null) return null; + try { + var res = Objects.requireNonNull(ap.getPermissionController()); + Binder.allowBlocking(res.asBinder()); + return res; + } catch (RemoteException e) { + mServiceHolder.attemptClear(ap.asBinder()); + return null; + } + } + + @Override + public void registerOnStartTask(Runnable task) { + mServiceHolder.registerOnStartTask(unused -> task.run()); + } } diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index e756082bc912..758c84a26dcd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.content.Context; import android.content.res.Resources; import android.media.AudioDeviceAttributes; @@ -37,6 +38,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioDeviceVolumeDispatcher; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -98,7 +100,8 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 3623012b348f..2cb02bdd2806 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -23,12 +23,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioSystem; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -75,7 +77,8 @@ public class AudioDeviceVolumeManagerTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java new file mode 100644 index 000000000000..8d772ad5c124 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2024 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.server.audio; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@RunWith(AndroidJUnit4.class) +@Presubmit +public final class AudioServerPermissionProviderTest { + + // Class under test + private AudioServerPermissionProvider mPermissionProvider; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock public INativePermissionController mMockPc; + + @Mock public PackageState mMockPackageStateOne_10000_one; + @Mock public PackageState mMockPackageStateTwo_10001_two; + @Mock public PackageState mMockPackageStateThree_10000_one; + @Mock public PackageState mMockPackageStateFour_10000_three; + @Mock public PackageState mMockPackageStateFive_10001_four; + @Mock public PackageState mMockPackageStateSix_10000_two; + + public List<UidPackageState> mInitPackageListExpected; + + // Argument matcher which matches that the state is equal even if the package names are out of + // order (since they are logically a set). + public static final class UidPackageStateMatcher implements ArgumentMatcher<UidPackageState> { + private final int mUid; + private final List<String> mSortedPackages; + + public UidPackageStateMatcher(int uid, List<String> packageNames) { + mUid = uid; + if (packageNames != null) { + mSortedPackages = new ArrayList(packageNames); + Collections.sort(mSortedPackages); + } else { + mSortedPackages = null; + } + } + + public UidPackageStateMatcher(UidPackageState toMatch) { + this(toMatch.uid, toMatch.packageNames); + } + + @Override + public boolean matches(UidPackageState state) { + if (state == null) return false; + if (state.uid != mUid) return false; + if ((state.packageNames == null) != (mSortedPackages == null)) return false; + var copy = new ArrayList(state.packageNames); + Collections.sort(copy); + return mSortedPackages.equals(copy); + } + + @Override + public String toString() { + return "Matcher for UidState with uid: " + mUid + ": " + mSortedPackages; + } + } + + public static final class PackageStateListMatcher + implements ArgumentMatcher<List<UidPackageState>> { + + private final List<UidPackageState> mToMatch; + + public PackageStateListMatcher(List<UidPackageState> toMatch) { + mToMatch = Objects.requireNonNull(toMatch); + } + + @Override + public boolean matches(List<UidPackageState> other) { + if (other == null) return false; + if (other.size() != mToMatch.size()) return false; + for (int i = 0; i < mToMatch.size(); i++) { + var matcher = new UidPackageStateMatcher(mToMatch.get(i)); + if (!matcher.matches(other.get(i))) return false; + } + return true; + } + + @Override + public String toString() { + return "Matcher for List<UidState> with uid: " + mToMatch; + } + } + + @Before + public void setup() { + when(mMockPackageStateOne_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateOne_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateTwo_10001_two.getAppId()).thenReturn(10001); + when(mMockPackageStateTwo_10001_two.getPackageName()).thenReturn("com.package.two"); + + // Same state as the first is intentional, emulating multi-user + when(mMockPackageStateThree_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateThree_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateFour_10000_three.getAppId()).thenReturn(10000); + when(mMockPackageStateFour_10000_three.getPackageName()).thenReturn("com.package.three"); + + when(mMockPackageStateFive_10001_four.getAppId()).thenReturn(10001); + when(mMockPackageStateFive_10001_four.getPackageName()).thenReturn("com.package.four"); + + when(mMockPackageStateSix_10000_two.getAppId()).thenReturn(10000); + when(mMockPackageStateSix_10000_two.getPackageName()).thenReturn("com.package.two"); + } + + @Test + public void testInitialPackagePopulation() throws Exception { + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateThree_10000_one, + mMockPackageStateFour_10000_three, + mMockPackageStateFive_10001_four, + mMockPackageStateSix_10000_two); + var expectedPackageList = + List.of( + createUidPackageState( + 10000, + List.of("com.package.one", "com.package.two", "com.package.three")), + createUidPackageState( + 10001, List.of("com.package.two", "com.package.four"))); + + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + verify(mMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + } + + @Test + public void testOnModifyPackageState_whenNewUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // new uid, including user component + mPermissionProvider.onModifyPackageState(1_10002, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10002, List.of("com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenRemoveUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc).updatePackagesForUid(argThat(new UidPackageStateMatcher(10000, List.of()))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdatedUidAddition() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.one", "com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdateUidRemoval() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + createUidPackageState(10000, List.of("com.package.two"))))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnServiceStart() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10000, List.of("com.package.two")))); + + verify(mMockPc).updatePackagesForUid(any()); // exactly once + mPermissionProvider.onModifyPackageState( + 1_10000, "com.package.three", false /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.two", "com.package.three")))); + verify(mMockPc, times(2)).updatePackagesForUid(any()); // exactly twice + // state is now 10000: two, three | 10001: two + + // simulate restart of the service + mPermissionProvider.onServiceStart(null); // should handle null + var newMockPc = mock(INativePermissionController.class); + mPermissionProvider.onServiceStart(newMockPc); + + var expectedPackageList = + List.of( + createUidPackageState( + 10000, List.of("com.package.two", "com.package.three")), + createUidPackageState(10001, List.of("com.package.two"))); + + verify(newMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + + verify(newMockPc, never()).updatePackagesForUid(any()); + // updates should still work after restart + mPermissionProvider.onModifyPackageState(10001, "com.package.four", false /* isRemove */); + verify(newMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10001, List.of("com.package.two", "com.package.four")))); + // exactly once + verify(newMockPc).updatePackagesForUid(any()); + } + + private static UidPackageState createUidPackageState(int uid, List<String> packages) { + var res = new UidPackageState(); + res.uid = uid; + res.packageNames = packages; + return res; + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 634877eb2539..037c3c00443c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -66,6 +66,7 @@ public class AudioServiceTest { @Mock private AppOpsManager mMockAppOpsManager; @Mock private AudioPolicyFacade mMockAudioPolicy; @Mock private PermissionEnforcer mMockPermissionEnforcer; + @Mock private AudioServerPermissionProvider mMockPermissionProvider; // the class being unit-tested here private AudioService mAudioService; @@ -86,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 8dfcc1843fed..27b552fa7cdd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -22,11 +22,13 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.mock; import android.annotation.NonNull; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -75,7 +77,8 @@ public class DeviceVolumeBehaviorTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()); + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 271720573e38..8e34ee1b6a42 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -133,9 +133,12 @@ public class VolumeHelperTest { @Mock private PermissionEnforcer mMockPermissionEnforcer; @Mock + private AudioServerPermissionProvider mMockPermissionProvider; + @Mock private AudioVolumeGroupHelperBase mAudioVolumeGroupHelper; - private final AudioPolicyFacade mFakeAudioPolicy = lookbackAudio -> false; + @Mock + private AudioPolicyFacade mMockAudioPolicy; private AudioVolumeGroup mAudioMusicVolumeGroup; @@ -154,9 +157,10 @@ public class VolumeHelperTest { SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, - @NonNull PermissionEnforcer enforcer) { + @NonNull PermissionEnforcer enforcer, + AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer); + audioPolicy, looper, appOps, enforcer, permissionProvider); } public void setDeviceForStream(int stream, int device) { @@ -210,8 +214,9 @@ public class VolumeHelperTest { mAm = mContext.getSystemService(AudioManager.class); mAudioService = new MyAudioService(mContext, mSpyAudioSystem, mSpySystemServer, - mSettingsAdapter, mAudioVolumeGroupHelper, mFakeAudioPolicy, - mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer); + mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, + mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer, + mMockPermissionProvider); mTestLooper.dispatchAll(); prepareAudioServiceState(); |