diff options
47 files changed, 1249 insertions, 1009 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 29349cba35ae..ea05abbcc6fb 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -24,6 +24,7 @@ java_defaults { ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", ":com.android.text.flags-aconfig-java{.generated_srcjars}", ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", + ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", ], // Add aconfig-annotations-lib as a dependency for the optimization libs: ["aconfig-annotations-lib"], @@ -116,3 +117,16 @@ aconfig_declarations { package: "android.companion.virtual.flags", srcs: ["core/java/android/companion/virtual/*.aconfig"], } + +// InputMethod +aconfig_declarations { + name: "android.view.inputmethod.flags-aconfig", + package: "android.view.inputmethod", + srcs: ["core/java/android/view/inputmethod/flags.aconfig"], +} + +java_aconfig_library { + name: "android.view.inputmethod.flags-aconfig-java", + aconfig_declarations: "android.view.inputmethod.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/core/api/system-current.txt b/core/api/system-current.txt index adf1da9f9e75..2fa31ed91201 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3797,6 +3797,10 @@ package android.content.pm { method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException; } + public class PackageInfo implements android.os.Parcelable { + field public boolean isArchived; + } + public class PackageInstaller { method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException; method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException; @@ -4016,6 +4020,7 @@ package android.content.pm { field @Deprecated public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1 field @Deprecated public static final int MASK_PERMISSION_FLAGS = 255; // 0xff field public static final int MATCH_ANY_USER = 4194304; // 0x400000 + field public static final long MATCH_ARCHIVED_PACKAGES = 4294967296L; // 0x100000000L field public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000 field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000 field public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 536870912; // 0x20000000 @@ -7281,8 +7286,11 @@ package android.media.audiofx { package android.media.audiopolicy { - public class AudioMix { + public class AudioMix implements android.os.Parcelable { + method public int describeContents(); method public int getMixState(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.audiopolicy.AudioMix> CREATOR; field public static final int MIX_STATE_DISABLED = -1; // 0xffffffff field public static final int MIX_STATE_IDLE = 0; // 0x0 field public static final int MIX_STATE_MIXING = 1; // 0x1 @@ -7291,15 +7299,18 @@ package android.media.audiopolicy { } public static class AudioMix.Builder { - ctor public AudioMix.Builder(android.media.audiopolicy.AudioMixingRule) throws java.lang.IllegalArgumentException; + ctor public AudioMix.Builder(@NonNull android.media.audiopolicy.AudioMixingRule) throws java.lang.IllegalArgumentException; method public android.media.audiopolicy.AudioMix build() throws java.lang.IllegalArgumentException; method public android.media.audiopolicy.AudioMix.Builder setDevice(@NonNull android.media.AudioDeviceInfo) throws java.lang.IllegalArgumentException; - method public android.media.audiopolicy.AudioMix.Builder setFormat(android.media.AudioFormat) throws java.lang.IllegalArgumentException; + method public android.media.audiopolicy.AudioMix.Builder setFormat(@NonNull android.media.AudioFormat) throws java.lang.IllegalArgumentException; method public android.media.audiopolicy.AudioMix.Builder setRouteFlags(int) throws java.lang.IllegalArgumentException; } - public class AudioMixingRule { + public class AudioMixingRule implements android.os.Parcelable { + method public int describeContents(); method public int getTargetMixRole(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.audiopolicy.AudioMixingRule> CREATOR; field public static final int MIX_ROLE_INJECTOR = 1; // 0x1 field public static final int MIX_ROLE_PLAYERS = 0; // 0x0 field public static final int RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET = 2; // 0x2 @@ -7336,6 +7347,7 @@ package android.media.audiopolicy { method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>); method public boolean setUserIdDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>); method public String toLogFriendlyString(); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int updateMixingRules(@NonNull java.util.List<android.util.Pair<android.media.audiopolicy.AudioMix,android.media.audiopolicy.AudioMixingRule>>); field public static final int FOCUS_POLICY_DUCKING_DEFAULT = 0; // 0x0 field public static final int FOCUS_POLICY_DUCKING_IN_APP = 0; // 0x0 field public static final int FOCUS_POLICY_DUCKING_IN_POLICY = 1; // 0x1 diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index d66fca8945f1..ed0f872bf9bc 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -2495,7 +2495,8 @@ public class AssistStructure implements Parcelable { + ", hints=" + Arrays.toString(node.getAutofillHints()) + ", value=" + node.getAutofillValue() + ", sanitized=" + node.isSanitized() - + ", important=" + node.getImportantForAutofill()); + + ", important=" + node.getImportantForAutofill() + + ", visibility=" + node.getVisibility()); } final int NCHILDREN = node.getChildCount(); diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 63c11b779641..cdb8b46ba41a 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Parcel; @@ -488,6 +489,17 @@ public class PackageInfo implements Parcelable { */ public boolean isActiveApex; + /** + * Whether the package is currently in an archived state. + * + * <p>Packages can be archived through {@link PackageArchiver} and do not have any APKs stored + * on the device, but do keep the data directory. + * @hide + */ + // TODO(b/278553670) Unhide and update @links before launch. + @SystemApi + public boolean isArchived; + public PackageInfo() { } @@ -575,6 +587,7 @@ public class PackageInfo implements Parcelable { } dest.writeBoolean(isApex); dest.writeBoolean(isActiveApex); + dest.writeBoolean(isArchived); dest.restoreAllowSquashing(prevAllowSquashing); } @@ -640,5 +653,6 @@ public class PackageInfo implements Parcelable { } isApex = source.readBoolean(); isActiveApex = source.readBoolean(); + isArchived = source.readBoolean(); } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c384389f45a3..9a53a2a60076 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -787,6 +787,7 @@ public abstract class PackageManager { MATCH_DEBUG_TRIAGED_MISSING, MATCH_INSTANT, MATCH_APEX, + MATCH_ARCHIVED_PACKAGES, GET_DISABLED_COMPONENTS, GET_DISABLED_UNTIL_USED_COMPONENTS, GET_UNINSTALLED_PACKAGES, @@ -811,6 +812,7 @@ public abstract class PackageManager { GET_UNINSTALLED_PACKAGES, MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS, MATCH_APEX, + MATCH_ARCHIVED_PACKAGES, }) @Retention(RetentionPolicy.SOURCE) public @interface ApplicationInfoFlagsBits {} @@ -1235,6 +1237,21 @@ public abstract class PackageManager { public static final long GET_ATTRIBUTIONS_LONG = 0x80000000L; /** + * Flag parameter to also retrieve some information about archived packages. + * Packages can be archived through {@link PackageArchiver} and do not have any APKs stored on + * the device, but do keep the data directory. + * <p> Note: Archived apps are a subset of apps returned by {@link #MATCH_UNINSTALLED_PACKAGES}. + * <p> Note: this flag may cause less information about currently installed + * applications to be returned. + * <p> Note: use of this flag requires the android.permission.QUERY_ALL_PACKAGES + * permission to see uninstalled packages. + * @hide + */ + // TODO(b/278553670) Unhide and update @links before launch. + @SystemApi + public static final long MATCH_ARCHIVED_PACKAGES = 1L << 32; + + /** * @hide */ public static final long FILTER_OUT_QUARANTINED_COMPONENTS = 0x100000000L; diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 85f5395f2657..5fe2aa1f5295 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -156,6 +156,13 @@ public class FeatureFlagUtils { public static final String SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS = "settings_biometrics2_fingerprint"; + /** + * Flag to enable/disable remote auth enrollment and settings + * @hide + */ + public static final String SETTINGS_REMOTEAUTH_ENROLLMENT_SETTINGS = + "settings_remoteauth_enrollment"; + /** Flag to enable/disable entire page in Accessibility -> Hearing aids * @hide */ @@ -248,6 +255,8 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, "true"); DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS, "false"); DEFAULT_FLAGS.put("settings_press_hold_nav_handle_to_search", "false"); + // TODO: b/298454866 Replace with Trunk Stable Feature Flag + DEFAULT_FLAGS.put(SETTINGS_REMOTEAUTH_ENROLLMENT_SETTINGS, "false"); } private static final Set<String> PERSISTENT_FLAGS; diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig new file mode 100644 index 000000000000..92d34089cb22 --- /dev/null +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.view.inputmethod" + +flag { + name: "refactor_insets_controller" + namespace: "inputmethod" + description: "Feature flag for refactoring InsetsController and removing ImeInsetsSourceConsumer" + bug: "298172246" +}
\ No newline at end of file diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index ccae67f3e953..6440cc3952f8 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -2071,10 +2071,67 @@ jobject convertAudioMixerAttributesFromNative(JNIEnv *env, mixerBehavior); } -static jint convertAudioMixToNative(JNIEnv *env, - AudioMix *nAudioMix, - const jobject jAudioMix) -{ +static jint convertAudioMixingRuleToNative(JNIEnv *env, const jobject audioMixingRule, + std::vector<AudioMixMatchCriterion> *nCriteria) { + jobject jRuleCriteria = env->GetObjectField(audioMixingRule, gAudioMixingRuleFields.mCriteria); + + jobjectArray jCriteria = static_cast<jobjectArray>( + env->CallObjectMethod(jRuleCriteria, gArrayListMethods.toArray)); + env->DeleteLocalRef(jRuleCriteria); + + jint numCriteria = env->GetArrayLength(jCriteria); + if (numCriteria > MAX_CRITERIA_PER_MIX) { + numCriteria = MAX_CRITERIA_PER_MIX; + } + + nCriteria->resize(numCriteria); + for (jint i = 0; i < numCriteria; i++) { + AudioMixMatchCriterion &nCriterion = (*nCriteria)[i]; + + jobject jCriterion = env->GetObjectArrayElement(jCriteria, i); + + nCriterion.mRule = env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mRule); + + const uint32_t match_rule = nCriterion.mRule & ~RULE_EXCLUSION_MASK; + switch (match_rule) { + case RULE_MATCH_UID: + nCriterion.mValue.mUid = + env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp); + break; + case RULE_MATCH_USERID: + nCriterion.mValue.mUserId = + env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp); + break; + case RULE_MATCH_AUDIO_SESSION_ID: { + jint jAudioSessionId = + env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp); + nCriterion.mValue.mAudioSessionId = static_cast<audio_session_t>(jAudioSessionId); + } break; + case RULE_MATCH_ATTRIBUTE_USAGE: + case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: { + jobject jAttributes = + env->GetObjectField(jCriterion, gAudioMixMatchCriterionFields.mAttr); + + auto paa = JNIAudioAttributeHelper::makeUnique(); + jint jStatus = JNIAudioAttributeHelper::nativeFromJava(env, jAttributes, paa.get()); + if (jStatus != AUDIO_JAVA_SUCCESS) { + return jStatus; + } + if (match_rule == RULE_MATCH_ATTRIBUTE_USAGE) { + nCriterion.mValue.mUsage = paa->usage; + } else { + nCriterion.mValue.mSource = paa->source; + } + env->DeleteLocalRef(jAttributes); + } break; + } + env->DeleteLocalRef(jCriterion); + } + env->DeleteLocalRef(jCriteria); + return AUDIO_JAVA_SUCCESS; +} + +static jint convertAudioMixToNative(JNIEnv *env, AudioMix *nAudioMix, const jobject jAudioMix) { nAudioMix->mMixType = env->GetIntField(jAudioMix, gAudioMixFields.mMixType); nAudioMix->mRouteFlags = env->GetIntField(jAudioMix, gAudioMixFields.mRouteFlags); nAudioMix->mDeviceType = @@ -2094,69 +2151,16 @@ static jint convertAudioMixToNative(JNIEnv *env, env->DeleteLocalRef(jFormat); jobject jRule = env->GetObjectField(jAudioMix, gAudioMixFields.mRule); - jobject jRuleCriteria = env->GetObjectField(jRule, gAudioMixingRuleFields.mCriteria); nAudioMix->mAllowPrivilegedMediaPlaybackCapture = env->GetBooleanField(jRule, gAudioMixingRuleFields.mAllowPrivilegedPlaybackCapture); nAudioMix->mVoiceCommunicationCaptureAllowed = env->GetBooleanField(jRule, gAudioMixingRuleFields.mVoiceCommunicationCaptureAllowed); - env->DeleteLocalRef(jRule); - jobjectArray jCriteria = static_cast<jobjectArray>( - env->CallObjectMethod(jRuleCriteria, gArrayListMethods.toArray)); - env->DeleteLocalRef(jRuleCriteria); - - jint numCriteria = env->GetArrayLength(jCriteria); - if (numCriteria > MAX_CRITERIA_PER_MIX) { - numCriteria = MAX_CRITERIA_PER_MIX; - } - for (jint i = 0; i < numCriteria; i++) { - AudioMixMatchCriterion nCriterion; + jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria)); - jobject jCriterion = env->GetObjectArrayElement(jCriteria, i); - - nCriterion.mRule = env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mRule); - - const uint32_t match_rule = nCriterion.mRule & ~RULE_EXCLUSION_MASK; - switch (match_rule) { - case RULE_MATCH_UID: - nCriterion.mValue.mUid = env->GetIntField(jCriterion, - gAudioMixMatchCriterionFields.mIntProp); - break; - case RULE_MATCH_USERID: - nCriterion.mValue.mUserId = - env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp); - break; - case RULE_MATCH_AUDIO_SESSION_ID: { - jint jAudioSessionId = - env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp); - nCriterion.mValue.mAudioSessionId = static_cast<audio_session_t>(jAudioSessionId); - } break; - case RULE_MATCH_ATTRIBUTE_USAGE: - case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: { - jobject jAttributes = env->GetObjectField(jCriterion, gAudioMixMatchCriterionFields.mAttr); - - auto paa = JNIAudioAttributeHelper::makeUnique(); - jint jStatus = JNIAudioAttributeHelper::nativeFromJava(env, jAttributes, paa.get()); - if (jStatus != AUDIO_JAVA_SUCCESS) { - return jStatus; - } - if (match_rule == RULE_MATCH_ATTRIBUTE_USAGE) { - nCriterion.mValue.mUsage = paa->usage; - } else { - nCriterion.mValue.mSource = paa->source; - } - env->DeleteLocalRef(jAttributes); - } - break; - } - - nAudioMix->mCriteria.push_back(nCriterion); - env->DeleteLocalRef(jCriterion); - } - - env->DeleteLocalRef(jCriteria); + env->DeleteLocalRef(jRule); - return AUDIO_JAVA_SUCCESS; + return status; } static jint @@ -2200,6 +2204,45 @@ android_media_AudioSystem_registerPolicyMixes(JNIEnv *env, jobject clazz, return nativeToJavaStatus(status); } +static jint android_media_AudioSystem_updatePolicyMixes(JNIEnv *env, jobject clazz, + jobjectArray mixes, + jobjectArray updatedMixingRules) { + if (mixes == nullptr || updatedMixingRules == nullptr) { + return AUDIO_JAVA_BAD_VALUE; + } + + jsize updatesCount = env->GetArrayLength(mixes); + if (updatesCount == 0 || updatesCount != env->GetArrayLength(updatedMixingRules)) { + return AUDIO_JAVA_BAD_VALUE; + } + + std::vector<std::pair<AudioMix, std::vector<AudioMixMatchCriterion>>> updates(updatesCount); + for (int i = 0; i < updatesCount; i++) { + jobject jAudioMix = env->GetObjectArrayElement(mixes, i); + jobject jAudioMixingRule = env->GetObjectArrayElement(updatedMixingRules, i); + if (!env->IsInstanceOf(jAudioMix, gAudioMixClass) || + !env->IsInstanceOf(jAudioMixingRule, gAudioMixingRuleClass)) { + return AUDIO_JAVA_BAD_VALUE; + } + + jint ret; + if ((ret = convertAudioMixToNative(env, &updates[i].first, jAudioMix)) != + AUDIO_JAVA_SUCCESS) { + return ret; + } + if ((ret = convertAudioMixingRuleToNative(env, jAudioMixingRule, &updates[i].second)) != + AUDIO_JAVA_SUCCESS) { + return ret; + } + } + + ALOGV("AudioSystem::updatePolicyMixes numMixes %d", updatesCount); + int status = AudioSystem::updatePolicyMixes(updates); + ALOGV("AudioSystem::updatePolicyMixes returned %d", status); + + return nativeToJavaStatus(status); +} + static jint android_media_AudioSystem_setUidDeviceAffinities(JNIEnv *env, jobject clazz, jint uid, jintArray deviceTypes, jobjectArray deviceAddresses) { AudioDeviceTypeAddrVector deviceVector; @@ -3158,6 +3201,10 @@ static const JNINativeMethod gMethods[] = MAKE_AUDIO_SYSTEM_METHOD(getAudioHwSyncForSession), MAKE_JNI_NATIVE_METHOD("registerPolicyMixes", "(Ljava/util/ArrayList;Z)I", android_media_AudioSystem_registerPolicyMixes), + MAKE_JNI_NATIVE_METHOD("updatePolicyMixes", + "([Landroid/media/audiopolicy/AudioMix;[Landroid/media/audiopolicy/" + "AudioMixingRule;)I", + android_media_AudioSystem_updatePolicyMixes), MAKE_JNI_NATIVE_METHOD("setUidDeviceAffinities", "(I[I[Ljava/lang/String;)I", android_media_AudioSystem_setUidDeviceAffinities), MAKE_AUDIO_SYSTEM_METHOD(removeUidDeviceAffinities), diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 3f013de8dad6..61b5fd5fb0ec 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -29,6 +29,7 @@ import android.content.pm.PackageManager; import android.media.audio.common.AidlConversion; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; +import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioProductStrategy; import android.os.Build; import android.os.IBinder; @@ -1955,6 +1956,11 @@ public class AudioSystem /** @hide */ public static native int registerPolicyMixes(ArrayList<AudioMix> mixes, boolean register); + /** @hide */ + public static native int updatePolicyMixes( + AudioMix[] mixes, + AudioMixingRule[] updatedMixingRules); + /** @hide see AudioPolicy.setUidDeviceAffinities() */ public static native int setUidDeviceAffinities(int uid, @NonNull int[] types, @NonNull String[] addresses); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index e45ef404995c..0e7718b060bc 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -54,6 +54,8 @@ import android.media.IVolumeController; import android.media.PlayerBase; import android.media.VolumeInfo; import android.media.VolumePolicy; +import android.media.audiopolicy.AudioMix; +import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; @@ -356,6 +358,11 @@ interface IAudioService { int removeMixForPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb); + @EnforcePermission("MODIFY_AUDIO_ROUTING") + int updateMixingRulesForPolicy(in AudioMix[] mixesToUpdate, + in AudioMixingRule[] updatedMixingRules, + in IAudioPolicyCallback pcb); + int setFocusPropertiesForPolicy(int duckingBehavior, in IAudioPolicyCallback pcb); void setVolumePolicy(in VolumePolicy policy); diff --git a/media/java/android/media/audiopolicy/AudioMix.aidl b/media/java/android/media/audiopolicy/AudioMix.aidl new file mode 100644 index 000000000000..d17a644c93a1 --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioMix.aidl @@ -0,0 +1,3 @@ +package android.media.audiopolicy; + +parcelable AudioMix;
\ No newline at end of file diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index d0270d3d3246..48b476651c91 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -21,12 +21,15 @@ import static android.media.AudioSystem.isRemoteSubmixDevice; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; import com.android.internal.annotations.VisibleForTesting; @@ -38,12 +41,12 @@ import java.util.Objects; * @hide */ @SystemApi -public class AudioMix { +public class AudioMix implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private AudioMixingRule mRule; + private @NonNull AudioMixingRule mRule; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private AudioFormat mFormat; + private @NonNull AudioFormat mFormat; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private int mRouteFlags; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -54,7 +57,7 @@ public class AudioMix { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) int mCallbackFlags; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - String mDeviceAddress; + @NonNull String mDeviceAddress; // initialized in constructor, read by AudioPolicyConfig @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -63,10 +66,11 @@ public class AudioMix { /** * All parameters are guaranteed valid through the Builder. */ - private AudioMix(AudioMixingRule rule, AudioFormat format, int routeFlags, int callbackFlags, - int deviceType, String deviceAddress) { - mRule = rule; - mFormat = format; + private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format, + int routeFlags, int callbackFlags, + int deviceType, @Nullable String deviceAddress) { + mRule = Objects.requireNonNull(rule); + mFormat = Objects.requireNonNull(format); mRouteFlags = routeFlags; mMixType = rule.getTargetMixType(); mCallbackFlags = callbackFlags; @@ -187,6 +191,15 @@ public class AudioMix { } /** @hide */ + public void setAudioMixingRule(@NonNull AudioMixingRule rule) { + if (mRule.getTargetMixType() != rule.getTargetMixType()) { + throw new UnsupportedOperationException( + "Target mix role of updated rule doesn't match the mix role of the AudioMix"); + } + mRule = Objects.requireNonNull(rule); + } + + /** @hide */ public String getRegistration() { return mDeviceAddress; } @@ -269,6 +282,49 @@ public class AudioMix { return Objects.hash(mRouteFlags, mRule, mMixType, mFormat); } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + // write mix route flags + dest.writeInt(mRouteFlags); + // write callback flags + dest.writeInt(mCallbackFlags); + // write device information + dest.writeInt(mDeviceSystemType); + dest.writeString8(mDeviceAddress); + mFormat.writeToParcel(dest, flags); + mRule.writeToParcel(dest, flags); + } + + public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() { + /** + * Rebuilds an AudioMix previously stored with writeToParcel(). + * + * @param p Parcel object to read the AudioMix from + * @return a new AudioMix created from the data in the parcel + */ + public AudioMix createFromParcel(Parcel p) { + final AudioMix.Builder mixBuilder = new AudioMix.Builder(); + // read mix route flags + mixBuilder.setRouteFlags(p.readInt()); + // read callback flags + mixBuilder.setCallbackFlags(p.readInt()); + // read device information + mixBuilder.setDevice(p.readInt(), p.readString8()); + mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p)); + mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p)); + return mixBuilder.build(); + } + + public AudioMix[] newArray(int size) { + return new AudioMix[size]; + } + }; + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) @@ -298,7 +354,7 @@ public class AudioMix { * @param rule a non-null {@link AudioMixingRule} instance. * @throws IllegalArgumentException */ - public Builder(AudioMixingRule rule) + public Builder(@NonNull AudioMixingRule rule) throws IllegalArgumentException { if (rule == null) { throw new IllegalArgumentException("Illegal null AudioMixingRule argument"); @@ -313,7 +369,7 @@ public class AudioMix { * @return the same Builder instance. * @throws IllegalArgumentException */ - Builder setMixingRule(AudioMixingRule rule) + Builder setMixingRule(@NonNull AudioMixingRule rule) throws IllegalArgumentException { if (rule == null) { throw new IllegalArgumentException("Illegal null AudioMixingRule argument"); @@ -358,7 +414,7 @@ public class AudioMix { * @return the same Builder instance. * @throws IllegalArgumentException */ - public Builder setFormat(AudioFormat format) + public Builder setFormat(@NonNull AudioFormat format) throws IllegalArgumentException { if (format == null) { throw new IllegalArgumentException("Illegal null AudioFormat argument"); diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.aidl b/media/java/android/media/audiopolicy/AudioMixingRule.aidl new file mode 100644 index 000000000000..5c06538b74c2 --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioMixingRule.aidl @@ -0,0 +1,3 @@ +package android.media.audiopolicy; + +parcelable AudioMixingRule;
\ No newline at end of file diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index 9c0b825fbfa3..e5debb8132ea 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -26,8 +26,11 @@ import android.media.AudioAttributes; import android.media.MediaRecorder; import android.os.Build; import android.os.Parcel; +import android.os.Parcelable; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Collection; @@ -50,7 +53,7 @@ import java.util.Set; * </pre> */ @SystemApi -public class AudioMixingRule { +public class AudioMixingRule implements Parcelable { private AudioMixingRule(int mixType, Collection<AudioMixMatchCriterion> criteria, boolean allowPrivilegedMediaPlaybackCapture, @@ -130,7 +133,7 @@ public class AudioMixingRule { RULE_EXCLUSION_MASK | RULE_MATCH_AUDIO_SESSION_ID; /** @hide */ - public static final class AudioMixMatchCriterion { + public static final class AudioMixMatchCriterion implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) final AudioAttributes mAttr; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -139,18 +142,44 @@ public class AudioMixingRule { final int mRule; /** input parameters must be valid */ - AudioMixMatchCriterion(AudioAttributes attributes, int rule) { + @VisibleForTesting + public AudioMixMatchCriterion(AudioAttributes attributes, int rule) { mAttr = attributes; mIntProp = Integer.MIN_VALUE; mRule = rule; } /** input parameters must be valid */ - AudioMixMatchCriterion(Integer intProp, int rule) { + @VisibleForTesting + public AudioMixMatchCriterion(Integer intProp, int rule) { mAttr = null; mIntProp = intProp.intValue(); mRule = rule; } + private AudioMixMatchCriterion(@NonNull Parcel in) { + Objects.requireNonNull(in); + mRule = in.readInt(); + final int match_rule = mRule & ~RULE_EXCLUSION_MASK; + switch (match_rule) { + case RULE_MATCH_ATTRIBUTE_USAGE: + case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: + mAttr = AudioAttributes.CREATOR.createFromParcel(in); + mIntProp = Integer.MIN_VALUE; + break; + case RULE_MATCH_UID: + case RULE_MATCH_USERID: + case RULE_MATCH_AUDIO_SESSION_ID: + mIntProp = in.readInt(); + mAttr = null; + break; + default: + // assume there was in int value to read as for now they come in pair + in.readInt(); + throw new IllegalArgumentException( + "Illegal rule value " + mRule + " in parcel"); + } + } + @Override public int hashCode() { return Objects.hash(mAttr, mIntProp, mRule); @@ -170,7 +199,13 @@ public class AudioMixingRule { && Objects.equals(mAttr, other.mAttr); } - void writeToParcel(Parcel dest) { + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mRule); final int match_rule = mRule & ~RULE_EXCLUSION_MASK; switch (match_rule) { @@ -190,6 +225,22 @@ public class AudioMixingRule { } } + public static final @NonNull Parcelable.Creator<AudioMixMatchCriterion> CREATOR = + new Parcelable.Creator<>() { + /** + * Rebuilds an AudioMixMatchCriterion previously stored with writeToParcel(). + * + * @param p Parcel object to read the AudioMix from + * @return a new AudioMixMatchCriterion created from the data in the parcel + */ + public AudioMixMatchCriterion createFromParcel(Parcel p) { + return new AudioMixMatchCriterion(p); + } + public AudioMixMatchCriterion[] newArray(int size) { + return new AudioMixMatchCriterion[size]; + } + }; + public AudioAttributes getAudioAttributes() { return mAttr; } public int getIntProp() { return mIntProp; } public int getRule() { return mRule; } @@ -605,13 +656,14 @@ public class AudioMixingRule { if (!(property instanceof AudioAttributes)) { throw new IllegalArgumentException("Invalid AudioAttributes argument"); } - return addRuleInternal((AudioAttributes) property, null, rule); + return addRuleInternal( + new AudioMixMatchCriterion((AudioAttributes) property, rule)); } else { // implies integer match rule if (!(property instanceof Integer)) { throw new IllegalArgumentException("Invalid Integer argument"); } - return addRuleInternal(null, (Integer) property, rule); + return addRuleInternal(new AudioMixMatchCriterion((Integer) property, rule)); } } @@ -636,12 +688,13 @@ public class AudioMixingRule { * @return the same Builder instance. * @throws IllegalArgumentException */ - private Builder addRuleInternal(AudioAttributes attrToMatch, Integer intProp, int rule) + private Builder addRuleInternal(AudioMixMatchCriterion criterion) throws IllegalArgumentException { // If mix type is invalid and added rule is valid only for the players / recorders, // adjust the mix type accordingly. // Otherwise, if the mix type was already deduced or set explicitly, verify the rule // is valid for the mix type. + final int rule = criterion.mRule; if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) { if (isPlayerRule(rule)) { mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; @@ -655,51 +708,16 @@ public class AudioMixingRule { } synchronized (mCriteria) { int oppositeRule = rule ^ RULE_EXCLUSION_MASK; - if (mCriteria.stream().anyMatch(criterion -> criterion.mRule == oppositeRule)) { + if (mCriteria.stream().anyMatch( + otherCriterion -> otherCriterion.mRule == oppositeRule)) { throw new IllegalArgumentException("AudioMixingRule cannot contain RULE_MATCH_*" + " and RULE_EXCLUDE_* for the same dimension."); } - int ruleWithoutExclusion = rule & ~RULE_EXCLUSION_MASK; - switch (ruleWithoutExclusion) { - case RULE_MATCH_ATTRIBUTE_USAGE: - case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: - mCriteria.add(new AudioMixMatchCriterion(attrToMatch, rule)); - break; - case RULE_MATCH_UID: - case RULE_MATCH_USERID: - case RULE_MATCH_AUDIO_SESSION_ID: - mCriteria.add(new AudioMixMatchCriterion(intProp, rule)); - break; - default: - throw new IllegalStateException("Unreachable code in addRuleInternal()"); - } + mCriteria.add(criterion); } return this; } - Builder addRuleFromParcel(Parcel in) throws IllegalArgumentException { - final int rule = in.readInt(); - final int match_rule = rule & ~RULE_EXCLUSION_MASK; - AudioAttributes attr = null; - Integer intProp = null; - switch (match_rule) { - case RULE_MATCH_ATTRIBUTE_USAGE: - case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: - attr = AudioAttributes.CREATOR.createFromParcel(in); - break; - case RULE_MATCH_UID: - case RULE_MATCH_USERID: - case RULE_MATCH_AUDIO_SESSION_ID: - intProp = new Integer(in.readInt()); - break; - default: - // assume there was in int value to read as for now they come in pair - in.readInt(); - throw new IllegalArgumentException("Illegal rule value " + rule + " in parcel"); - } - return addRuleInternal(attr, intProp, rule); - } - /** * Combines all of the matching and exclusion rules that have been set and return a new * {@link AudioMixingRule} object. @@ -717,4 +735,52 @@ public class AudioMixingRule { mVoiceCommunicationCaptureAllowed); } } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + // write opt-out respect + dest.writeBoolean(mAllowPrivilegedPlaybackCapture); + // write voice communication capture allowed flag + dest.writeBoolean(mVoiceCommunicationCaptureAllowed); + // write specified mix type + dest.writeInt(mTargetMixType); + // write mix rules + dest.writeInt(mCriteria.size()); + for (AudioMixingRule.AudioMixMatchCriterion criterion : mCriteria) { + criterion.writeToParcel(dest, flags); + } + } + + public static final @NonNull Parcelable.Creator<AudioMixingRule> CREATOR = + new Parcelable.Creator<>() { + + @Override + public AudioMixingRule createFromParcel(Parcel source) { + AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder(); + // read opt-out respect + ruleBuilder.allowPrivilegedPlaybackCapture(source.readBoolean()); + // read voice capture allowed flag + ruleBuilder.voiceCommunicationCaptureAllowed(source.readBoolean()); + // read specified mix type + ruleBuilder.setTargetMixRole(source.readInt()); + // read mix rules + int nbRules = source.readInt(); + for (int j = 0; j < nbRules; j++) { + // read the matching rules + ruleBuilder.addRuleInternal( + AudioMixMatchCriterion.CREATOR.createFromParcel(source)); + } + return ruleBuilder.build(); + } + + @Override + public AudioMixingRule[] newArray(int size) { + return new AudioMixingRule[size]; + } + }; } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 3e5de821b47d..e9a6ed4dcf08 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -44,6 +44,7 @@ import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; +import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -408,6 +409,39 @@ public class AudioPolicy { } /** + * Update {@link AudioMixingRule}-s of already registered {@link AudioMix}-es. + * + * @param mixingRuleUpdates - {@link List} of {@link Pair}-s, each pair containing + * {@link AudioMix} to update and its new corresponding {@link AudioMixingRule}. + * + * @return {@link AudioManager#SUCCESS} if the update was successful, + * {@link AudioManager#ERROR} otherwise. + */ + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int updateMixingRules( + @NonNull List<Pair<AudioMix, AudioMixingRule>> mixingRuleUpdates) { + Objects.requireNonNull(mixingRuleUpdates); + + IAudioService service = getService(); + try { + synchronized (mLock) { + final int status = service.updateMixingRulesForPolicy( + mixingRuleUpdates.stream().map(p -> p.first).toArray(AudioMix[]::new), + mixingRuleUpdates.stream().map(p -> p.second).toArray( + AudioMixingRule[]::new), + cb()); + if (status == AudioManager.SUCCESS) { + mConfig.updateMixingRules(mixingRuleUpdates); + } + return status; + } + } catch (RemoteException e) { + Log.e(TAG, "Received remote exeception in updateMixingRules call: ", e); + return AudioManager.ERROR; + } + } + + /** * @hide * Configures the audio framework so that all audio streams originating from the given UID * can only come from a set of audio devices. diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index 7a85d21bf144..d277c7dfbea4 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -17,16 +17,17 @@ package android.media.audiopolicy; import android.annotation.NonNull; -import android.media.AudioFormat; import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -85,72 +86,20 @@ public class AudioPolicyConfig implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mMixes.size()); for (AudioMix mix : mMixes) { - // write mix route flags - dest.writeInt(mix.getRouteFlags()); - // write callback flags - dest.writeInt(mix.mCallbackFlags); - // write device information - dest.writeInt(mix.mDeviceSystemType); - dest.writeString(mix.mDeviceAddress); - // write mix format - dest.writeInt(mix.getFormat().getSampleRate()); - dest.writeInt(mix.getFormat().getEncoding()); - dest.writeInt(mix.getFormat().getChannelMask()); - // write opt-out respect - dest.writeBoolean(mix.getRule().allowPrivilegedMediaPlaybackCapture()); - // write voice communication capture allowed flag - dest.writeBoolean(mix.getRule().voiceCommunicationCaptureAllowed()); - // write specified mix type - dest.writeInt(mix.getRule().getTargetMixRole()); - // write mix rules - final ArrayList<AudioMixMatchCriterion> criteria = mix.getRule().getCriteria(); - dest.writeInt(criteria.size()); - for (AudioMixMatchCriterion criterion : criteria) { - criterion.writeToParcel(dest); - } + mix.writeToParcel(dest, flags); } } private AudioPolicyConfig(Parcel in) { - mMixes = new ArrayList<AudioMix>(); int nbMixes = in.readInt(); + mMixes = new ArrayList<>(nbMixes); for (int i = 0 ; i < nbMixes ; i++) { - final AudioMix.Builder mixBuilder = new AudioMix.Builder(); - // read mix route flags - int routeFlags = in.readInt(); - mixBuilder.setRouteFlags(routeFlags); - // read callback flags - mixBuilder.setCallbackFlags(in.readInt()); - // read device information - mixBuilder.setDevice(in.readInt(), in.readString()); - // read mix format - int sampleRate = in.readInt(); - int encoding = in.readInt(); - int channelMask = in.readInt(); - final AudioFormat format = new AudioFormat.Builder().setSampleRate(sampleRate) - .setChannelMask(channelMask).setEncoding(encoding).build(); - mixBuilder.setFormat(format); - - AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder(); - // read opt-out respect - ruleBuilder.allowPrivilegedPlaybackCapture(in.readBoolean()); - // read voice capture allowed flag - ruleBuilder.voiceCommunicationCaptureAllowed(in.readBoolean()); - // read specified mix type - ruleBuilder.setTargetMixRole(in.readInt()); - // read mix rules - int nbRules = in.readInt(); - for (int j = 0 ; j < nbRules ; j++) { - // read the matching rules - ruleBuilder.addRuleFromParcel(in); - } - mixBuilder.setMixingRule(ruleBuilder.build()); - mMixes.add(mixBuilder.build()); + mMixes.add(AudioMix.CREATOR.createFromParcel(in)); } } - public static final @android.annotation.NonNull Parcelable.Creator<AudioPolicyConfig> CREATOR - = new Parcelable.Creator<AudioPolicyConfig>() { + public static final @android.annotation.NonNull Parcelable.Creator<AudioPolicyConfig> CREATOR = + new Parcelable.Creator<>() { /** * Rebuilds an AudioPolicyConfig previously stored with writeToParcel(). * @param p Parcel object to read the AudioPolicyConfig from @@ -309,6 +258,23 @@ public class AudioPolicyConfig implements Parcelable { } } + /** + * Update audio mixing rules for already registered {@link AudioMix}-es. + * + * @param audioMixingRuleUpdates {@link List} of {@link Pair}-s containing {@link AudioMix} to + * be updated and the new {@link AudioMixingRule}. + */ + public void updateMixingRules( + @NonNull List<Pair<AudioMix, AudioMixingRule>> audioMixingRuleUpdates) { + Objects.requireNonNull(audioMixingRuleUpdates).forEach( + update -> updateMixingRule(update.first, update.second)); + } + + private void updateMixingRule(AudioMix audioMixToUpdate, AudioMixingRule audioMixingRule) { + mMixes.stream().filter(audioMixToUpdate::equals).findAny().ifPresent( + mix -> mix.setAudioMixingRule(audioMixingRule)); + } + private static String mixTypeId(int type) { if (type == AudioMix.MIX_TYPE_PLAYERS) return "p"; else if (type == AudioMix.MIX_TYPE_RECORDERS) return "r"; diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java deleted file mode 100644 index a26398ac198a..000000000000 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2023 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.audiopolicytest; - -import static android.media.AudioFormat.CHANNEL_OUT_MONO; -import static android.media.AudioFormat.CHANNEL_OUT_STEREO; -import static android.media.AudioFormat.ENCODING_PCM_16BIT; -import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; -import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import android.media.AudioFormat; -import android.media.AudioSystem; -import android.media.audiopolicy.AudioMix; -import android.media.audiopolicy.AudioMixingRule; -import android.media.audiopolicy.AudioPolicyConfig; -import android.os.Parcel; -import android.platform.test.annotations.Presubmit; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.google.common.testing.EqualsTester; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -/** - * Unit tests for AudioMix. - * - * Run with "atest AudioMixUnitTests". - */ -@Presubmit -@RunWith(AndroidJUnit4.class) -public class AudioMixUnitTests { - private static final AudioFormat OUTPUT_FORMAT_STEREO_44KHZ_PCM = - new AudioFormat.Builder() - .setSampleRate(44000) - .setChannelMask(CHANNEL_OUT_STEREO) - .setEncoding(ENCODING_PCM_16BIT).build(); - private static final AudioFormat OUTPUT_FORMAT_MONO_16KHZ_PCM = - new AudioFormat.Builder() - .setSampleRate(16000) - .setChannelMask(CHANNEL_OUT_MONO) - .setEncoding(ENCODING_PCM_16BIT).build(); - private static final AudioFormat INPUT_FORMAT_MONO_16KHZ_PCM = - new AudioFormat.Builder() - .setSampleRate(16000) - .setChannelMask(AudioFormat.CHANNEL_IN_MONO) - .setEncoding(ENCODING_PCM_16BIT).build(); - - @Test - public void testEquals() { - final EqualsTester equalsTester = new EqualsTester(); - - // --- Equality group 1 - final AudioMix playbackAudioMixWithSessionId42AndUid123 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) - .addMixRule(RULE_MATCH_UID, 123).build()) - .setFormat(OUTPUT_FORMAT_STEREO_44KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - final AudioMix playbackAudioMixWithUid123AndSessionId42 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 123) - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42).build()) - .setFormat(OUTPUT_FORMAT_STEREO_44KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - equalsTester.addEqualityGroup( - playbackAudioMixWithSessionId42AndUid123, - playbackAudioMixWithUid123AndSessionId42, - writeToAndFromParcel(playbackAudioMixWithSessionId42AndUid123), - writeToAndFromParcel(playbackAudioMixWithUid123AndSessionId42)); - - // --- Equality group 2 - final AudioMix recordingAudioMixWithSessionId42AndUid123 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_INJECTOR) - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) - .addMixRule(RULE_MATCH_UID, 123).build()) - .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - final AudioMix recordingAudioMixWithUid123AndSessionId42 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_INJECTOR) - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) - .addMixRule(RULE_MATCH_UID, 123).build()) - .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - equalsTester.addEqualityGroup(recordingAudioMixWithSessionId42AndUid123, - recordingAudioMixWithUid123AndSessionId42, - writeToAndFromParcel(recordingAudioMixWithSessionId42AndUid123), - writeToAndFromParcel(recordingAudioMixWithUid123AndSessionId42)); - - // --- Equality group 3 - final AudioMix recordingAudioMixWithSessionId42AndUid123Render = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) - .addMixRule(RULE_MATCH_UID, 123).build()) - .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags( - AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER).build(); - equalsTester.addEqualityGroup(recordingAudioMixWithSessionId42AndUid123Render, - writeToAndFromParcel(recordingAudioMixWithSessionId42AndUid123Render)); - - // --- Equality group 4 - final AudioMix playbackAudioMixWithUid123 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 123).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - equalsTester.addEqualityGroup(playbackAudioMixWithUid123, - writeToAndFromParcel(playbackAudioMixWithUid123)); - - // --- Equality group 5 - final AudioMix playbackAudioMixWithUid42 = - new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - equalsTester.addEqualityGroup(playbackAudioMixWithUid42, - writeToAndFromParcel(playbackAudioMixWithUid42)); - - equalsTester.testEquals(); - } - - @Test - public void buildRenderToRemoteSubmix_success() { - final String deviceAddress = "address"; - final AudioMix audioMix = new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) - .setDevice(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX, /*address=*/deviceAddress).build(); - - assertEquals(deviceAddress, audioMix.getRegistration()); - assertEquals(OUTPUT_FORMAT_MONO_16KHZ_PCM, audioMix.getFormat()); - assertEquals(AudioMix.ROUTE_FLAG_RENDER, audioMix.getRouteFlags()); - } - - @Test - public void buildLoopbackAndRenderToRemoteSubmix_success() { - final String deviceAddress = "address"; - final AudioMix audioMix = new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK_RENDER) - .setDevice(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX, /*address=*/deviceAddress).build(); - - assertEquals(deviceAddress, audioMix.getRegistration()); - assertEquals(OUTPUT_FORMAT_MONO_16KHZ_PCM, audioMix.getFormat()); - assertEquals(AudioMix.ROUTE_FLAG_LOOP_BACK_RENDER, audioMix.getRouteFlags()); - } - - @Test - public void buildRenderToSpeaker_success() { - final AudioMix audioMix = new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) - .setDevice(AudioSystem.DEVICE_OUT_SPEAKER, /*address=*/"").build(); - - assertEquals(OUTPUT_FORMAT_MONO_16KHZ_PCM, audioMix.getFormat()); - assertEquals(AudioMix.ROUTE_FLAG_RENDER, audioMix.getRouteFlags()); - } - - @Test - public void buildLoopbackForPlayerMix_success() { - final AudioMix audioMix = new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - - assertEquals(OUTPUT_FORMAT_MONO_16KHZ_PCM, audioMix.getFormat()); - assertEquals(AudioMix.ROUTE_FLAG_LOOP_BACK, audioMix.getRouteFlags()); - } - - @Test - public void buildLoopbackForInjectorMix_success() { - final AudioMix audioMix = new AudioMix.Builder(new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_INJECTOR) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); - - assertEquals(OUTPUT_FORMAT_MONO_16KHZ_PCM, audioMix.getFormat()); - assertEquals(AudioMix.ROUTE_FLAG_LOOP_BACK, audioMix.getRouteFlags()); - } - - @Test - public void buildLoopbackWithIncompatibleDevice_throws() { - assertThrows(IllegalArgumentException.class, () -> new AudioMix.Builder( - new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) - .setDevice(AudioSystem.DEVICE_OUT_SPEAKER, /*address=*/"").build()); - } - - @Test - public void buildRenderWithoutDevice_throws() { - assertThrows(IllegalArgumentException.class, () -> new AudioMix.Builder( - new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER).build()); - } - - @Test - public void buildRenderWithInputDevice_throws() { - assertThrows(IllegalArgumentException.class, () -> new AudioMix.Builder( - new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_PLAYERS) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) - .setDevice(AudioSystem.DEVICE_IN_BUILTIN_MIC, /*address=*/"").build()); - } - - @Test - public void buildRenderWithInjectorMix_throws() { - assertThrows(IllegalArgumentException.class, () -> new AudioMix.Builder( - new AudioMixingRule.Builder() - .setTargetMixRole(MIX_ROLE_INJECTOR) - .addMixRule(RULE_MATCH_UID, 42).build()) - .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) - .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) - .setDevice(AudioSystem.DEVICE_OUT_SPEAKER, /*address=*/"").build()); - } - - - - private static AudioMix writeToAndFromParcel(AudioMix audioMix) { - AudioPolicyConfig apc = new AudioPolicyConfig(new ArrayList<>(List.of(audioMix))); - Parcel parcel = Parcel.obtain(); - apc.writeToParcel(parcel, /*flags=*/0); - parcel.setDataPosition(0); - AudioMix unmarshalledMix = - AudioPolicyConfig.CREATOR.createFromParcel(parcel).getMixes().get(0); - parcel.recycle(); - return unmarshalledMix; - } -} diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java deleted file mode 100644 index 3cbfd50ff859..000000000000 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * 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.audiopolicytest; - -import static android.media.AudioAttributes.USAGE_MEDIA; -import static android.media.MediaRecorder.AudioSource.VOICE_RECOGNITION; -import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; -import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; -import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET; -import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE; -import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_AUDIO_SESSION_ID; -import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_UID; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID; -import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - - -import android.media.AudioAttributes; -import android.media.audiopolicy.AudioMixingRule; -import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; -import android.platform.test.annotations.Presubmit; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.hamcrest.CustomTypeSafeMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Unit tests for AudioPolicy. - * - * Run with "atest AudioMixingRuleUnitTests". - */ -@Presubmit -@RunWith(AndroidJUnit4.class) -public class AudioMixingRuleUnitTests { - private static final AudioAttributes USAGE_MEDIA_AUDIO_ATTRIBUTES = - new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build(); - private static final AudioAttributes CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES = - new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build(); - private static final int TEST_UID = 42; - private static final int OTHER_UID = 77; - private static final int TEST_SESSION_ID = 1234; - - @Test - public void testConstructValidRule() { - AudioMixingRule rule = new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .addMixRule(RULE_MATCH_UID, TEST_UID) - .excludeMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID) - .build(); - - // Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS, - // since the rules are valid for both MIX_ROLE_PLAYERS & MIX_ROLE_INJECTOR. - assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); - assertThat(rule.getCriteria(), containsInAnyOrder( - isAudioMixMatchUsageCriterion(USAGE_MEDIA), - isAudioMixMatchUidCriterion(TEST_UID), - isAudioMixExcludeSessionCriterion(TEST_SESSION_ID))); - } - - @Test - public void testConstructRuleWithConflictingCriteriaFails() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .addMixRule(RULE_MATCH_UID, TEST_UID) - // Conflicts with previous criterion. - .addMixRule(RULE_EXCLUDE_UID, OTHER_UID) - .build()); - } - - @Test - public void testRuleBuilderDedupsCriteria() { - AudioMixingRule rule = new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .addMixRule(RULE_MATCH_UID, TEST_UID) - // Identical to previous criterion. - .addMixRule(RULE_MATCH_UID, TEST_UID) - // Identical to first criterion. - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .build(); - - assertThat(rule.getCriteria(), hasSize(2)); - assertThat(rule.getCriteria(), containsInAnyOrder( - isAudioMixMatchUsageCriterion(USAGE_MEDIA), - isAudioMixMatchUidCriterion(TEST_UID))); - } - - @Test - public void failsWhenAddAttributeRuleCalledWithInvalidType() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - // Rule match attribute usage requires AudioAttributes, not - // just the int enum value of the usage. - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) - .build()); - } - - @Test - public void failsWhenExcludeAttributeRuleCalledWithInvalidType() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - // Rule match attribute usage requires AudioAttributes, not - // just the int enum value of the usage. - .excludeMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) - .build()); - } - - @Test - public void failsWhenAddIntRuleCalledWithInvalidType() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - // Rule match uid requires Integer not AudioAttributes. - .addMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .build()); - } - - @Test - public void failsWhenExcludeIntRuleCalledWithInvalidType() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - // Rule match uid requires Integer not AudioAttributes. - .excludeMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) - .build()); - } - - @Test - public void injectorMixTypeDeductionWithGenericRuleSucceeds() { - AudioMixingRule rule = new AudioMixingRule.Builder() - // UID rule can be used both with MIX_ROLE_PLAYERS and MIX_ROLE_INJECTOR. - .addMixRule(RULE_MATCH_UID, TEST_UID) - // Capture preset rule is only valid for injector, MIX_ROLE_INJECTOR should - // be deduced. - .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, - CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) - .build(); - - assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); - assertThat(rule.getCriteria(), containsInAnyOrder( - isAudioMixMatchUidCriterion(TEST_UID), - isAudioMixMatchCapturePresetCriterion(VOICE_RECOGNITION))); - } - - @Test - public void settingTheMixTypeToIncompatibleInjectorMixFails() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, - CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) - // Capture preset cannot be defined for MIX_ROLE_PLAYERS. - .setTargetMixRole(MIX_ROLE_PLAYERS) - .build()); - } - - @Test - public void addingPlayersOnlyRuleWithInjectorsOnlyRuleFails() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder() - // MIX_ROLE_PLAYERS only rule. - .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) - // MIX ROLE_INJECTOR only rule. - .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, - CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) - .build()); - } - - @Test - public void sessionIdRuleCompatibleWithPlayersMix() { - int sessionId = 42; - AudioMixingRule rule = new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, sessionId) - .setTargetMixRole(MIX_ROLE_PLAYERS) - .build(); - - assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); - assertThat(rule.getCriteria(), containsInAnyOrder(isAudioMixSessionCriterion(sessionId))); - } - - @Test - public void sessionIdRuleCompatibleWithInjectorMix() { - AudioMixingRule rule = new AudioMixingRule.Builder() - .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID) - .setTargetMixRole(MIX_ROLE_INJECTOR) - .build(); - - assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); - assertThat(rule.getCriteria(), - containsInAnyOrder(isAudioMixSessionCriterion(TEST_SESSION_ID))); - } - - @Test - public void audioMixingRuleWithNoRulesFails() { - assertThrows(IllegalArgumentException.class, - () -> new AudioMixingRule.Builder().build()); - } - - - private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) { - return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { - @Override - public boolean matchesSafely(AudioMixMatchCriterion item) { - int expectedRule = exclude ? RULE_EXCLUDE_UID : RULE_MATCH_UID; - return item.getRule() == expectedRule && item.getIntProp() == uid; - } - - @Override - public void describeMismatchSafely( - AudioMixMatchCriterion item, Description mismatchDescription) { - mismatchDescription.appendText( - String.format("is not %s criterion with uid %d", - exclude ? "exclude" : "match", uid)); - } - }; - } - - private static Matcher isAudioMixMatchUidCriterion(int uid) { - return isAudioMixUidCriterion(uid, /*exclude=*/ false); - } - - private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) { - return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { - @Override - public boolean matchesSafely(AudioMixMatchCriterion item) { - int expectedRule = exclude - ? RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET - : RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; - AudioAttributes attributes = item.getAudioAttributes(); - return item.getRule() == expectedRule - && attributes != null && attributes.getCapturePreset() == audioSource; - } - - @Override - public void describeMismatchSafely( - AudioMixMatchCriterion item, Description mismatchDescription) { - mismatchDescription.appendText( - String.format("is not %s criterion with capture preset %d", - exclude ? "exclude" : "match", audioSource)); - } - }; - } - - private static Matcher isAudioMixMatchCapturePresetCriterion(int audioSource) { - return isAudioMixCapturePresetCriterion(audioSource, /*exclude=*/ false); - } - - private static Matcher isAudioMixUsageCriterion(int usage, boolean exclude) { - return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("usage mix criterion") { - @Override - public boolean matchesSafely(AudioMixMatchCriterion item) { - int expectedRule = - exclude ? RULE_EXCLUDE_ATTRIBUTE_USAGE : RULE_MATCH_ATTRIBUTE_USAGE; - AudioAttributes attributes = item.getAudioAttributes(); - return item.getRule() == expectedRule - && attributes != null && attributes.getUsage() == usage; - } - - @Override - public void describeMismatchSafely( - AudioMixMatchCriterion item, Description mismatchDescription) { - mismatchDescription.appendText( - String.format("is not %s criterion with usage %d", - exclude ? "exclude" : "match", usage)); - } - }; - } - - private static Matcher isAudioMixMatchUsageCriterion(int usage) { - return isAudioMixUsageCriterion(usage, /*exclude=*/ false); - } - - private static Matcher isAudioMixSessionCriterion(int sessionId, boolean exclude) { - return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("sessionId mix criterion") { - @Override - public boolean matchesSafely(AudioMixMatchCriterion item) { - int excludeRule = - exclude ? RULE_EXCLUDE_AUDIO_SESSION_ID : RULE_MATCH_AUDIO_SESSION_ID; - return item.getRule() == excludeRule && item.getIntProp() == sessionId; - } - - @Override - public void describeMismatchSafely( - AudioMixMatchCriterion item, Description mismatchDescription) { - mismatchDescription.appendText( - String.format("is not %s criterion with session id %d", - exclude ? "exclude" : "match", sessionId)); - } - }; - } - - private static Matcher isAudioMixSessionCriterion(int sessionId) { - return isAudioMixSessionCriterion(sessionId, /*exclude=*/ false); - } - - private static Matcher isAudioMixExcludeSessionCriterion(int sessionId) { - return isAudioMixSessionCriterion(sessionId, /*exclude=*/ true); - } - -} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index e7bbf97e3b51..f68078a8a340 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -22,6 +22,7 @@ import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED import android.os.Handler +import android.os.Trace import android.util.Log import android.view.Display import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -34,9 +35,14 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Provides a [Flow] of [Display] as returned by [DisplayManager]. */ @@ -49,7 +55,25 @@ interface DisplayRepository { * * When `null`, it means there is no pending display waiting to be enabled. */ - val pendingDisplayId: Flow<Int?> + val pendingDisplay: Flow<PendingDisplay?> + + /** Represents a connected display that has not been enabled yet. */ + interface PendingDisplay { + /** Id of the pending display. */ + val id: Int + + /** Enables the display, making it available to the system. */ + suspend fun enable() + + /** + * Ignores the pending display. When called, this specific display id doesn't appear as + * pending anymore until the display is disconnected and reconnected again. + */ + suspend fun ignore() + + /** Disables the display, making it unavailable to the system. */ + suspend fun disable() + } } @SysUISingleton @@ -62,7 +86,8 @@ constructor( @Background backgroundCoroutineDispatcher: CoroutineDispatcher ) : DisplayRepository { - override val displays: Flow<Set<Display>> = + // Displays are enabled only after receiving them in [onDisplayAdded] + private val enabledDisplays: StateFlow<Set<Display>> = conflatedCallbackFlow { val callback = object : DisplayListener { @@ -99,27 +124,38 @@ constructor( displayManager.displays?.toSet() ?: emptySet() } - override val pendingDisplayId: Flow<Int?> = + /** Propagate to the listeners only enabled displays */ + override val displays: Flow<Set<Display>> = enabledDisplays + + private val enabledDisplayIds: Flow<Set<Int>> = + enabledDisplays + .map { enabledDisplaysSet -> enabledDisplaysSet.map { it.displayId }.toSet() } + .debugLog("enabledDisplayIds") + + private val ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) + + /* keeps connected displays until they are disconnected. */ + private val connectedDisplayIds: StateFlow<Set<Int>> = conflatedCallbackFlow { val callback = object : DisplayConnectionListener { - private val pendingIds = mutableSetOf<Int>() + private val connectedIds = mutableSetOf<Int>() override fun onDisplayConnected(id: Int) { - pendingIds += id - trySend(id) + if (DEBUG) { + Log.d(TAG, "$id connected") + } + connectedIds += id + ignoredDisplayIds.value -= id + trySend(connectedIds.toSet()) } override fun onDisplayDisconnected(id: Int) { - if (id in pendingIds) { - pendingIds -= id - trySend(null) - } else { - Log.e( - TAG, - "onDisplayDisconnected received for unknown display. " + - "id=$id, knownIds=$pendingIds" - ) + connectedIds -= id + if (DEBUG) { + Log.d(TAG, "$id disconnected. Connected ids: $connectedIds") } + ignoredDisplayIds.value -= id + trySend(connectedIds.toSet()) } } displayManager.registerDisplayListener( @@ -130,15 +166,80 @@ constructor( awaitClose { displayManager.unregisterDisplayListener(callback) } } .distinctUntilChanged() + .debugLog("connectedDisplayIds") .flowOn(backgroundCoroutineDispatcher) .stateIn( applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = emptySet() ) + /** + * Pending displays are the ones connected, but not enabled and not ignored. A connected display + * is ignored after the user makes the decision to use it or not. For now, the initial decision + * from the user is final and not reversible. + */ + private val pendingDisplayIds: Flow<Set<Int>> = + combine(enabledDisplayIds, connectedDisplayIds, ignoredDisplayIds) { + enabledDisplaysIds, + connectedDisplayIds, + ignoredDisplayIds -> + if (DEBUG) { + Log.d( + TAG, + "combining enabled: $enabledDisplaysIds, " + + "connected: $connectedDisplayIds, ignored: $ignoredDisplayIds" + ) + } + connectedDisplayIds - enabledDisplaysIds - ignoredDisplayIds + } + .debugLog("pendingDisplayIds") + + override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> = + pendingDisplayIds + .map { pendingDisplayIds -> + val id = pendingDisplayIds.maxOrNull() ?: return@map null + object : DisplayRepository.PendingDisplay { + override val id = id + override suspend fun enable() { + traceSection("DisplayRepository#enable($id)") { + displayManager.enableConnectedDisplay(id) + } + // After the display has been enabled, it is automatically ignored. + ignore() + } + + override suspend fun ignore() { + traceSection("DisplayRepository#ignore($id)") { + ignoredDisplayIds.value += id + } + } + + override suspend fun disable() { + ignore() + traceSection("DisplayRepository#disable($id)") { + displayManager.disableConnectedDisplay(id) + } + } + } + } + .debugLog("pendingDisplay") + + private fun <T> Flow<T>.debugLog(flowName: String): Flow<T> { + return if (DEBUG) { + this.onEach { + Log.d(TAG, "$flowName: $it") + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, "$TAG#$flowName", 0) + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "$TAG#$flowName", "$it", 0) + } + } else { + this + } + } + private companion object { const val TAG = "DisplayRepository" + val DEBUG = Log.isLoggable(TAG, Log.DEBUG) } } diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt index ef6fa26420a3..11ed96d5c7eb 100644 --- a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt @@ -16,14 +16,12 @@ package com.android.systemui.display.domain.interactor -import android.hardware.display.DisplayManager import android.view.Display import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.util.traceSection import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -52,13 +50,18 @@ interface ConnectedDisplayInteractor { CONNECTED_SECURE, } - /** Represents a connected display that has not been enabled yet. */ + /** Represents a connected display that has not been enabled yet for the UI layer. */ interface PendingDisplay { /** Enables the display, making it available to the system. */ - fun enable() + suspend fun enable() - /** Disables the display, making it unavailable to the system. */ - fun disable() + /** + * Ignores the pending display. + * + * When called, this specific display id doesn't appear as pending anymore until the display + * is disconnected and reconnected again. + */ + suspend fun ignore() } } @@ -66,7 +69,6 @@ interface ConnectedDisplayInteractor { class ConnectedDisplayInteractorImpl @Inject constructor( - private val displayManager: DisplayManager, keyguardRepository: KeyguardRepository, displayRepository: DisplayRepository, ) : ConnectedDisplayInteractor { @@ -92,28 +94,19 @@ constructor( // Provides the pending display only if the lockscreen is unlocked override val pendingDisplay: Flow<PendingDisplay?> = - displayRepository.pendingDisplayId.combine(keyguardRepository.isKeyguardUnlocked) { - pendingDisplayId, - keyguardUnlocked -> - if (pendingDisplayId != null && keyguardUnlocked) { - pendingDisplayId.toPendingDisplay() + displayRepository.pendingDisplay.combine(keyguardRepository.isKeyguardShowing) { + repositoryPendingDisplay, + keyguardShowing -> + if (repositoryPendingDisplay != null && !keyguardShowing) { + repositoryPendingDisplay.toInteractorPendingDisplay() } else { null } } - private fun Int.toPendingDisplay() = + private fun DisplayRepository.PendingDisplay.toInteractorPendingDisplay(): PendingDisplay = object : PendingDisplay { - val id = this@toPendingDisplay - override fun enable() { - traceSection("DisplayRepository#enable($id)") { - displayManager.enableConnectedDisplay(id) - } - } - override fun disable() { - traceSection("DisplayRepository#enable($id)") { - displayManager.disableConnectedDisplay(id) - } - } + override suspend fun enable() = this@toInteractorPendingDisplay.enable() + override suspend fun ignore() = this@toInteractorPendingDisplay.ignore() } } diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt index 174c6ff04a7d..ecc9d0ef7810 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt @@ -24,15 +24,22 @@ import android.view.WindowManager import android.widget.TextView import com.android.systemui.R -/** Dialog used to decide what to do with a connected display. */ +/** + * Dialog used to decide what to do with a connected display. + * + * [onCancelMirroring] is called **only** if mirroring didn't start, or when the dismiss button is + * pressed. + */ class MirroringConfirmationDialog( context: Context, private val onStartMirroringClickListener: View.OnClickListener, - private val onDismissClickListener: View.OnClickListener, + private val onCancelMirroring: View.OnClickListener, ) : Dialog(context, R.style.Theme_SystemUI_Dialog) { private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView + private var enabledPressed = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window?.apply { @@ -45,10 +52,15 @@ class MirroringConfirmationDialog( mirrorButton = requireViewById<TextView>(R.id.enable_display).apply { setOnClickListener(onStartMirroringClickListener) + enabledPressed = true } dismissButton = - requireViewById<TextView>(R.id.cancel).apply { - setOnClickListener(onDismissClickListener) + requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } + + setOnDismissListener { + if (!enabledPressed) { + onCancelMirroring.onClick(null) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt index ece33b7f6032..86ef439361b0 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt @@ -19,13 +19,16 @@ import android.app.Dialog import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.ui.view.MirroringConfirmationDialog import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch /** * Shows/hides a dialog to allow the user to decide whether to use the external display for @@ -38,6 +41,7 @@ constructor( private val context: Context, private val connectedDisplayInteractor: ConnectedDisplayInteractor, @Application private val scope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher ) { private var dialog: Dialog? = null @@ -61,10 +65,13 @@ constructor( MirroringConfirmationDialog( context, onStartMirroringClickListener = { - pendingDisplay.enable() + scope.launch(bgDispatcher) { pendingDisplay.enable() } hideDialog() }, - onDismissClickListener = { hideDialog() } + onCancelMirroring = { + scope.launch(bgDispatcher) { pendingDisplay.ignore() } + hideDialog() + } ) .apply { show() } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 0fb1b4804958..d8b31a229261 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -43,6 +43,10 @@ object Flags { val ADD_TRANSIENT_HUN_IN_STACK_STATE_ANIMATOR = unreleasedFlag("add_transient_hun_in_stack_state_animator", teamfood = false) + // TODO(b/298308067): Tracking Bug + val SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX = + unreleasedFlag("swipe_uncleared_transient_view_fix", teamfood = false) + // TODO(b/254512751): Tracking Bug val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = unreleasedFlag("notification_pipeline_developer_logging") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 6bc9abf13cf7..257006e13201 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -126,11 +126,11 @@ constructor( } override fun start() { + bindKeyguardRootView() if (featureFlags.isEnabled(Flags.LAZY_INFLATE_KEYGUARD)) { keyguardRootView.removeAllViews() initializeViews() } else { - bindKeyguardRootView() val notificationPanel = notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup unbindKeyguardBottomArea(notificationPanel) diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 8d3b7451c90b..1d820a14be4e 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -70,6 +70,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.preference.PreferenceManager; @@ -85,9 +86,11 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; +import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dump.DumpManager; import com.android.systemui.people.NotificationHelper; import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; @@ -99,6 +102,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.wm.shell.bubbles.Bubbles; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -119,7 +123,8 @@ import javax.inject.Inject; /** Manager for People Space widget. */ @SysUISingleton -public class PeopleSpaceWidgetManager { +public class PeopleSpaceWidgetManager implements Dumpable { + private static final String TAG = "PeopleSpaceWidgetMgr"; private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; @@ -160,7 +165,8 @@ public class PeopleSpaceWidgetManager { CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, NotificationManager notificationManager, - BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor) { + BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor, + DumpManager dumpManager) { if (DEBUG) Log.d(TAG, "constructor"); mContext = context; mAppWidgetManager = AppWidgetManager.getInstance(context); @@ -180,6 +186,7 @@ public class PeopleSpaceWidgetManager { mManager = this; mBroadcastDispatcher = broadcastDispatcher; mBgExecutor = bgExecutor; + dumpManager.registerNormalDumpable(TAG, this); } /** Initializes {@PeopleSpaceWidgetManager}. */ @@ -1364,4 +1371,40 @@ public class PeopleSpaceWidgetManager { .filter(id -> !TextUtils.isEmpty(id)) .collect(Collectors.toSet()); } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + Trace.traceBegin(Trace.TRACE_TAG_APP, TAG + ".dump"); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + Map<String, ?> all = sp.getAll(); + pw.println("People widget list:"); + for (Map.Entry<String, ?> entry : all.entrySet()) { + String key = entry.getKey(); + PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry); + switch (keyType) { + case WIDGET_ID: + SharedPreferences widgetSp = mContext.getSharedPreferences(key, + Context.MODE_PRIVATE); + pw.print("People widget (valid) ["); + pw.print(key); + pw.print("] shortcut id: \""); + pw.print(widgetSp.getString(SHORTCUT_ID, EMPTY_STRING)); + pw.print("\", user id: "); + pw.print(widgetSp.getInt(USER_ID, INVALID_USER_ID)); + pw.print(", package: "); + pw.println(widgetSp.getString(PACKAGE_NAME, EMPTY_STRING)); + break; + case PEOPLE_TILE_KEY: + case CONTACT_URI: + pw.print("Extra data ["); + pw.print(key); + pw.print(" : "); + pw.print((Set<String>) entry.getValue()); + pw.println("]"); + break; + } + } + + Trace.traceEnd(Trace.TRACE_TAG_APP); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index db7c003ee545..4bd380e927c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -52,6 +52,7 @@ class DisplayRepositoryTest : SysuiTestCase() { private val displayManager = mock<DisplayManager>() private val displayListener = kotlinArgumentCaptor<DisplayManager.DisplayListener>() + private val connectedDisplayListener = kotlinArgumentCaptor<DisplayManager.DisplayListener>() private val testHandler = FakeHandler(Looper.getMainLooper()) private val testScope = TestScope(UnconfinedTestDispatcher()) @@ -114,7 +115,7 @@ class DisplayRepositoryTest : SysuiTestCase() { // Let's make sure it has *NOT* been unregistered, as there is still a subscriber. setDisplays(1) - displayListener.value.onDisplayAdded(1) + sendOnDisplayAdded(1) assertThat(firstSubscriber?.ids()).containsExactly(1) } @@ -127,7 +128,7 @@ class DisplayRepositoryTest : SysuiTestCase() { val value by latestDisplayFlowValue() setDisplays(1) - displayListener.value.onDisplayAdded(1) + sendOnDisplayAdded(1) assertThat(value?.ids()).containsExactly(1) } @@ -138,13 +139,13 @@ class DisplayRepositoryTest : SysuiTestCase() { val value by latestDisplayFlowValue() setDisplays(1, 2, 3, 4) - displayListener.value.onDisplayAdded(1) - displayListener.value.onDisplayAdded(2) - displayListener.value.onDisplayAdded(3) - displayListener.value.onDisplayAdded(4) + sendOnDisplayAdded(1) + sendOnDisplayAdded(2) + sendOnDisplayAdded(3) + sendOnDisplayAdded(4) setDisplays(1, 2, 3) - displayListener.value.onDisplayRemoved(4) + sendOnDisplayRemoved(4) assertThat(value?.ids()).containsExactly(1, 2, 3) } @@ -155,10 +156,10 @@ class DisplayRepositoryTest : SysuiTestCase() { val value by latestDisplayFlowValue() setDisplays(1, 2, 3, 4) - displayListener.value.onDisplayAdded(1) - displayListener.value.onDisplayAdded(2) - displayListener.value.onDisplayAdded(3) - displayListener.value.onDisplayAdded(4) + sendOnDisplayAdded(1) + sendOnDisplayAdded(2) + sendOnDisplayAdded(3) + sendOnDisplayAdded(4) displayListener.value.onDisplayChanged(4) @@ -168,22 +169,22 @@ class DisplayRepositoryTest : SysuiTestCase() { @Test fun onDisplayConnected_pendingDisplayReceived() = testScope.runTest { - val pendingDisplay by latestPendingDisplayFlowValue() + val pendingDisplay by lastPendingDisplay() - displayListener.value.onDisplayConnected(1) + sendOnDisplayConnected(1) - assertThat(pendingDisplay).isEqualTo(1) + assertThat(pendingDisplay!!.id).isEqualTo(1) } @Test fun onDisplayDisconnected_pendingDisplayNull() = testScope.runTest { - val pendingDisplay by latestPendingDisplayFlowValue() - displayListener.value.onDisplayConnected(1) + val pendingDisplay by lastPendingDisplay() + sendOnDisplayConnected(1) assertThat(pendingDisplay).isNotNull() - displayListener.value.onDisplayDisconnected(1) + sendOnDisplayDisconnected(1) assertThat(pendingDisplay).isNull() } @@ -191,24 +192,162 @@ class DisplayRepositoryTest : SysuiTestCase() { @Test fun onDisplayDisconnected_unknownDisplay_doesNotSendNull() = testScope.runTest { - val pendingDisplay by latestPendingDisplayFlowValue() - displayListener.value.onDisplayConnected(1) + val pendingDisplay by lastPendingDisplay() + sendOnDisplayConnected(1) assertThat(pendingDisplay).isNotNull() - displayListener.value.onDisplayDisconnected(2) + sendOnDisplayDisconnected(2) assertThat(pendingDisplay).isNotNull() } @Test - fun onDisplayConnected_multipleTimes_sendsOnlyTheLastOne() = + fun onDisplayConnected_multipleTimes_sendsOnlyTheMaximum() = testScope.runTest { - val pendingDisplay by latestPendingDisplayFlowValue() - displayListener.value.onDisplayConnected(1) - displayListener.value.onDisplayConnected(2) + val pendingDisplay by lastPendingDisplay() - assertThat(pendingDisplay).isEqualTo(2) + sendOnDisplayConnected(1) + sendOnDisplayConnected(2) + + assertThat(pendingDisplay!!.id).isEqualTo(2) + } + + @Test + fun onPendingDisplay_enable_displayEnabled() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + pendingDisplay!!.enable() + + verify(displayManager).enableConnectedDisplay(eq(1)) + } + + @Test + fun onPendingDisplay_enableBySysui_disabledBySomeoneElse_pendingDisplayStillIgnored() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + pendingDisplay!!.enable() + // to mock the display being really enabled: + sendOnDisplayAdded(1) + + // Simulate the display being disabled by someone else. Now, sysui will have it in the + // "pending displays" list again, but it should be ignored. + sendOnDisplayRemoved(1) + + assertThat(pendingDisplay).isNull() + } + + @Test + fun onPendingDisplay_ignoredBySysui_enabledDisabledBySomeoneElse_pendingDisplayStillIgnored() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + pendingDisplay!!.ignore() + + // to mock the display being enabled and disabled by someone else: + sendOnDisplayAdded(1) + sendOnDisplayRemoved(1) + + // Sysui already decided to ignore it, so the pending display should be null. + assertThat(pendingDisplay).isNull() + } + + @Test + fun onPendingDisplay_disable_displayDisabled() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + pendingDisplay!!.disable() + + verify(displayManager).disableConnectedDisplay(eq(1)) + } + + @Test + fun onPendingDisplay_ignore_pendingDisplayNull() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + sendOnDisplayConnected(1) + + pendingDisplay!!.ignore() + + assertThat(pendingDisplay).isNull() + verify(displayManager, never()).disableConnectedDisplay(eq(1)) + verify(displayManager, never()).enableConnectedDisplay(eq(1)) + } + + @Test + fun onPendingDisplay_enabled_pendingDisplayNull() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + assertThat(pendingDisplay).isNotNull() + + setDisplays(1) + sendOnDisplayAdded(1) + + assertThat(pendingDisplay).isNull() + } + + @Test + fun onPendingDisplay_multipleConnected_oneEnabled_pendingDisplayNotNull() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1) + sendOnDisplayConnected(2) + + assertThat(pendingDisplay).isNotNull() + + setDisplays(1) + sendOnDisplayAdded(1) + + assertThat(pendingDisplay).isNotNull() + assertThat(pendingDisplay!!.id).isEqualTo(2) + + setDisplays(1, 2) + sendOnDisplayAdded(2) + + assertThat(pendingDisplay).isNull() + } + + @Test + fun pendingDisplay_connectedDisconnectedAndReconnected_expectedPendingDisplayState() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + // Plug the cable + sendOnDisplayConnected(1) + + // Enable it + assertThat(pendingDisplay).isNotNull() + pendingDisplay!!.enable() + + // Enabled + verify(displayManager).enableConnectedDisplay(1) + setDisplays(1) + sendOnDisplayAdded(1) + + // No more pending displays + assertThat(pendingDisplay).isNull() + + // Let's disconnect the cable + setDisplays() + sendOnDisplayRemoved(1) + sendOnDisplayDisconnected(1) + + assertThat(pendingDisplay).isNull() + + // Let's reconnect it + sendOnDisplayConnected(1) + + assertThat(pendingDisplay).isNotNull() } private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } @@ -216,28 +355,47 @@ class DisplayRepositoryTest : SysuiTestCase() { // Wrapper to capture the displayListener. private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> { val flowValue = collectLastValue(displayRepository.displays) + captureAddedRemovedListener() + return flowValue + } + + private fun TestScope.lastPendingDisplay(): FlowValue<DisplayRepository.PendingDisplay?> { + val flowValue = collectLastValue(displayRepository.pendingDisplay) + captureAddedRemovedListener() verify(displayManager) .registerDisplayListener( - displayListener.capture(), + connectedDisplayListener.capture(), eq(testHandler), - eq( - DisplayManager.EVENT_FLAG_DISPLAY_ADDED or - DisplayManager.EVENT_FLAG_DISPLAY_CHANGED or - DisplayManager.EVENT_FLAG_DISPLAY_REMOVED - ) + eq(DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) ) return flowValue } - private fun TestScope.latestPendingDisplayFlowValue(): FlowValue<Int?> { - val flowValue = collectLastValue(displayRepository.pendingDisplayId) + private fun captureAddedRemovedListener() { verify(displayManager) .registerDisplayListener( displayListener.capture(), eq(testHandler), - eq(DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) + eq( + DisplayManager.EVENT_FLAG_DISPLAY_ADDED or + DisplayManager.EVENT_FLAG_DISPLAY_CHANGED or + DisplayManager.EVENT_FLAG_DISPLAY_REMOVED + ) ) - return flowValue + } + private fun sendOnDisplayAdded(id: Int) { + displayListener.value.onDisplayAdded(id) + } + private fun sendOnDisplayRemoved(id: Int) { + displayListener.value.onDisplayRemoved(id) + } + + private fun sendOnDisplayDisconnected(id: Int) { + connectedDisplayListener.value.onDisplayDisconnected(id) + } + + private fun sendOnDisplayConnected(id: Int) { + connectedDisplayListener.value.onDisplayConnected(id) } private fun setDisplays(displays: List<Display>) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt index 50617a16ce0b..26ee09412687 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.display.domain.interactor -import android.hardware.display.DisplayManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -27,12 +26,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.display.data.repository.FakeDisplayRepository +import com.android.systemui.display.data.repository.createPendingDisplay import com.android.systemui.display.data.repository.display import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -41,7 +39,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -49,20 +46,15 @@ import org.mockito.Mockito @SmallTest class ConnectedDisplayInteractorTest : SysuiTestCase() { - private val displayManager = mock<DisplayManager>() private val fakeDisplayRepository = FakeDisplayRepository() private val fakeKeyguardRepository = FakeKeyguardRepository() private val connectedDisplayStateProvider: ConnectedDisplayInteractor = - ConnectedDisplayInteractorImpl( - displayManager, - fakeKeyguardRepository, - fakeDisplayRepository - ) + ConnectedDisplayInteractorImpl(fakeKeyguardRepository, fakeDisplayRepository) private val testScope = TestScope(UnconfinedTestDispatcher()) @Before fun setup() { - fakeKeyguardRepository.setKeyguardUnlocked(true) + fakeKeyguardRepository.setKeyguardShowing(false) } @Test @@ -148,7 +140,7 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { fun pendingDisplay_propagated() = testScope.runTest { val value by lastPendingDisplay() - val pendingDisplayId = 4 + val pendingDisplayId = createPendingDisplay() fakeDisplayRepository.emit(pendingDisplayId) @@ -156,51 +148,29 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { } @Test - fun onPendingDisplay_enable_displayEnabled() = + fun onPendingDisplay_keyguardShowing_returnsPendingDisplay() = testScope.runTest { + fakeKeyguardRepository.setKeyguardShowing(true) val pendingDisplay by lastPendingDisplay() - fakeDisplayRepository.emit(1) - pendingDisplay!!.enable() - - Mockito.verify(displayManager).enableConnectedDisplay(eq(1)) - } - - @Test - fun onPendingDisplay_disable_displayDisabled() = - testScope.runTest { - val pendingDisplay by lastPendingDisplay() - - fakeDisplayRepository.emit(1) - pendingDisplay!!.disable() - - Mockito.verify(displayManager).disableConnectedDisplay(eq(1)) - } - - @Test - fun onPendingDisplay_keyguardUnlocked_returnsPendingDisplay() = - testScope.runTest { - fakeKeyguardRepository.setKeyguardUnlocked(false) - val pendingDisplay by lastPendingDisplay() - - fakeDisplayRepository.emit(1) + fakeDisplayRepository.emit(createPendingDisplay()) assertThat(pendingDisplay).isNull() - fakeKeyguardRepository.setKeyguardUnlocked(true) + fakeKeyguardRepository.setKeyguardShowing(false) assertThat(pendingDisplay).isNotNull() } @Test - fun onPendingDisplay_keyguardLocked_returnsNull() = + fun onPendingDisplay_keyguardShowing_returnsNull() = testScope.runTest { - fakeKeyguardRepository.setKeyguardUnlocked(true) + fakeKeyguardRepository.setKeyguardShowing(false) val pendingDisplay by lastPendingDisplay() - fakeDisplayRepository.emit(1) + fakeDisplayRepository.emit(createPendingDisplay()) assertThat(pendingDisplay).isNotNull() - fakeKeyguardRepository.setKeyguardUnlocked(false) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(pendingDisplay).isNull() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt index 705964736a49..46f758216aae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt @@ -68,6 +68,28 @@ class MirroringConfirmationDialogTest : SysuiTestCase() { verify(onStartMirroringCallback, never()).onClick(any()) } + @Test + fun onCancel_afterEnablingMirroring_cancelCallbackNotCalled() { + dialog.show() + dialog.requireViewById<View>(R.id.enable_display).callOnClick() + + dialog.cancel() + + verify(onCancelCallback, never()).onClick(any()) + verify(onStartMirroringCallback).onClick(any()) + } + + @Test + fun onDismiss_afterEnablingMirroring_cancelCallbackNotCalled() { + dialog.show() + dialog.requireViewById<View>(R.id.enable_display).callOnClick() + + dialog.dismiss() + + verify(onCancelCallback, never()).onClick(any()) + verify(onStartMirroringCallback).onClick(any()) + } + @After fun teardown() { if (::dialog.isInitialized) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 6f51d1b60822..2ac625d68bfe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -30,20 +30,24 @@ fun display(type: Int, flags: Int = 0, id: Int = 0): Display { } } +/** Creates a mock [DisplayRepository.PendingDisplay]. */ +fun createPendingDisplay(id: Int = 0): DisplayRepository.PendingDisplay = + mock<DisplayRepository.PendingDisplay> { whenever(this.id).thenReturn(id) } + /** Fake [DisplayRepository] implementation for testing. */ class FakeDisplayRepository : DisplayRepository { private val flow = MutableSharedFlow<Set<Display>>() - private val pendingDisplayFlow = MutableSharedFlow<Int?>() + private val pendingDisplayFlow = MutableSharedFlow<DisplayRepository.PendingDisplay?>() /** Emits [value] as [displays] flow value. */ suspend fun emit(value: Set<Display>) = flow.emit(value) - /** Emits [value] as [pendingDisplayId] flow value. */ - suspend fun emit(value: Int?) = pendingDisplayFlow.emit(value) + /** Emits [value] as [pendingDisplay] flow value. */ + suspend fun emit(value: DisplayRepository.PendingDisplay?) = pendingDisplayFlow.emit(value) override val displays: Flow<Set<Display>> get() = flow - override val pendingDisplayId: Flow<Int?> + override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> get() = pendingDisplayFlow } diff --git a/services/companion/java/com/android/server/companion/virtual/Android.bp b/services/companion/java/com/android/server/companion/virtual/Android.bp index 7a7d3763ea52..6526c78d5124 100644 --- a/services/companion/java/com/android/server/companion/virtual/Android.bp +++ b/services/companion/java/com/android/server/companion/virtual/Android.bp @@ -1,9 +1,6 @@ java_aconfig_library { name: "virtualdevice_flags_lib", aconfig_declarations: "virtualdevice_flags", - static_libs: [ - "android.companion.virtual.flags-aconfig-java", - ], } aconfig_declarations { diff --git a/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java index 66755688fb19..ce4067cf3786 100644 --- a/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java +++ b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java @@ -19,6 +19,7 @@ package com.android.server.companion.virtual; import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -146,6 +147,7 @@ class CameraAccessController extends CameraManager.AvailabilityCallback implemen * * @param runningUids uids of the application running on the virtual display */ + @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) public void blockCameraAccessIfNeeded(Set<Integer> runningUids) { synchronized (mLock) { for (int i = 0; i < mAppsToBlockOnVirtualDevice.size(); i++) { @@ -181,6 +183,7 @@ class CameraAccessController extends CameraManager.AvailabilityCallback implemen } @Override + @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { synchronized (mLock) { InjectionSessionData data = mPackageToSessionData.get(packageName); @@ -243,6 +246,7 @@ class CameraAccessController extends CameraManager.AvailabilityCallback implemen /** * Turns on blocking for a particular camera and package. */ + @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) private void startBlocking(String packageName, String cameraId) { try { Slog.d( diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 307f7bfc1fd5..eeaa423b1aef 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -16,6 +16,8 @@ package com.android.server.companion.virtual; +import static android.text.TextUtils.formatSimple; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.StringDef; @@ -350,7 +352,7 @@ class InputController { } private static String createPhys(@PhysType String type) { - return String.format("virtual%s:%d", type, sNextPhysId.getAndIncrement()); + return formatSimple("virtual%s:%d", type, sNextPhysId.getAndIncrement()); } private void setUniqueIdAssociation(int displayId, String phys) { diff --git a/services/companion/java/com/android/server/companion/virtual/SensorController.java b/services/companion/java/com/android/server/companion/virtual/SensorController.java index a831401168af..e9241dddcde9 100644 --- a/services/companion/java/com/android/server/companion/virtual/SensorController.java +++ b/services/companion/java/com/android/server/companion/virtual/SensorController.java @@ -323,8 +323,5 @@ public class SensorController { SensorCreationException(String message) { super(message); } - SensorCreationException(String message, Exception cause) { - super(message, cause); - } } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index f23fe2a7bce4..b8b42fba71ab 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.annotation.UserIdInt; import android.app.Activity; @@ -78,12 +79,14 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.WindowManager; import android.widget.Toast; + import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.BlockedAppStreamingActivity; @@ -93,7 +96,6 @@ import com.android.server.companion.virtual.audio.VirtualAudioController; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -460,6 +462,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override + @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) public void onRunningAppsChanged(ArraySet<Integer> runningUids) { mCameraAccessController.blockCameraAccessIfNeeded(runningUids); mRunningAppsChangedCallback.accept(runningUids); @@ -718,11 +721,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub try { synchronized (mVirtualDeviceLock) { mDefaultShowPointerIcon = showPointerIcon; - for (int i = 0; i < mVirtualDisplays.size(); i++) { - final int displayId = mVirtualDisplays.keyAt(i); - mInputController.setShowPointerIcon(mDefaultShowPointerIcon, displayId); - } } + getDisplayIds().forEach( + displayId -> mInputController.setShowPointerIcon(showPointerIcon, displayId)); } finally { Binder.restoreCallingIdentity(ident); } @@ -830,6 +831,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub this, gwpc, packageName); gwpc.setDisplayId(displayId); + boolean showPointer; synchronized (mVirtualDeviceLock) { if (mVirtualDisplays.contains(displayId)) { gwpc.unregisterRunningAppsChangedListener(this); @@ -839,11 +841,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub PowerManager.WakeLock wakeLock = createAndAcquireWakeLockForDisplay(displayId); mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock)); + showPointer = mDefaultShowPointerIcon; } final long token = Binder.clearCallingIdentity(); try { - mInputController.setShowPointerIcon(mDefaultShowPointerIcon, displayId); + mInputController.setShowPointerIcon(showPointer, displayId); mInputController.setPointerAcceleration(1f, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); @@ -869,6 +872,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) private void onActivityBlocked(int displayId, ActivityInfo activityInfo) { Intent intent = BlockedAppStreamingActivity.createIntent( activityInfo, mAssociationInfo.getDisplayName()); @@ -950,6 +954,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } + @SuppressWarnings("AndroidFrameworkRequiresPermission") private void checkVirtualInputDeviceDisplayIdAssociation(int displayId) { if (mContext.checkCallingPermission(android.Manifest.permission.INJECT_EVENTS) == PackageManager.PERMISSION_GRANTED) { @@ -1031,8 +1036,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub */ void showToastWhereUidIsRunning(int uid, String text, @Toast.Duration int duration, Looper looper) { - ArrayList<Integer> displayIdsForUid = getDisplayIdsWhereUidIsRunning(uid); - if (displayIdsForUid.isEmpty()) { + IntArray displayIdsForUid = getDisplayIdsWhereUidIsRunning(uid); + if (displayIdsForUid.size() == 0) { return; } DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); @@ -1045,8 +1050,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } - private ArrayList<Integer> getDisplayIdsWhereUidIsRunning(int uid) { - ArrayList<Integer> displayIdsForUid = new ArrayList<>(); + private IntArray getDisplayIdsWhereUidIsRunning(int uid) { + IntArray displayIdsForUid = new IntArray(); synchronized (mVirtualDeviceLock) { for (int i = 0; i < mVirtualDisplays.size(); i++) { if (mVirtualDisplays.valueAt(i).getWindowPolicyController().containsUid(uid)) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index a13587859038..4da929816d2f 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -23,6 +23,7 @@ import static com.android.server.wm.ActivityInterceptorCallback.VIRTUAL_DEVICE_S import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.app.ActivityOptions; import android.companion.AssociationInfo; @@ -96,6 +97,7 @@ public class VirtualDeviceManagerService extends SystemService { private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener = new CompanionDeviceManager.OnAssociationsChangedListener() { @Override + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) { syncVirtualDevicesToCdmAssociations(associations); } @@ -241,6 +243,7 @@ public class VirtualDeviceManagerService extends SystemService { return true; } + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) { Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>(); synchronized (mVirtualDeviceManagerLock) { @@ -266,6 +269,7 @@ public class VirtualDeviceManagerService extends SystemService { } } + @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) private void registerCdmAssociationListener() { final CompanionDeviceManager cdm = getContext().getSystemService( CompanionDeviceManager.class); @@ -273,6 +277,7 @@ public class VirtualDeviceManagerService extends SystemService { mCdmAssociationListener); } + @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) private void unregisterCdmAssociationListener() { final CompanionDeviceManager cdm = getContext().getSystemService( CompanionDeviceManager.class); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3d5b7fa292e8..0aa9cc11c432 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -140,6 +140,7 @@ import android.media.VolumeInfo; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; +import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; @@ -12050,6 +12051,49 @@ public class AudioService extends IAudioService.Stub } } + /** + * Update {@link AudioMixingRule}-s for already registered {@link AudioMix}-es. + * + * @param mixesToUpdate - array of already registered {@link AudioMix}-es to update. + * @param updatedMixingRules - array of {@link AudioMixingRule}-s corresponding to + * {@code mixesToUpdate} mixes. The array must be same size as + * {@code mixesToUpdate} and i-th {@link AudioMixingRule} must + * correspond to i-th {@link AudioMix} from mixesToUpdate array. + * @param pcb - {@link IAudioPolicyCallback} corresponding to the registered + * {@link AudioPolicy} all {@link AudioMix}-es for {@code mixesToUpdate} + * are part of. + * @return {@link AudioManager#SUCCESS} iff the mixing rules were updated successfully, + * {@link AudioManager#ERROR} otherwise. + */ + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int updateMixingRulesForPolicy( + @NonNull AudioMix[] mixesToUpdate, + @NonNull AudioMixingRule[] updatedMixingRules, + @NonNull IAudioPolicyCallback pcb) { + super.updateMixingRulesForPolicy_enforcePermission(); + Objects.requireNonNull(mixesToUpdate); + Objects.requireNonNull(updatedMixingRules); + Objects.requireNonNull(pcb); + if (mixesToUpdate.length != updatedMixingRules.length) { + Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules " + + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length + + ", updatedMixingRules.length = " + updatedMixingRules.length + ")."); + return AudioManager.ERROR; + } + if (DEBUG_AP) { + Log.d(TAG, "updateMixingRules for " + pcb.asBinder() + "with mix rules: "); + } + synchronized (mAudioPolicies) { + final AudioPolicyProxy app = + checkUpdateForPolicy(pcb, "Cannot add AudioMix in audio policy"); + if (app == null) { + return AudioManager.ERROR; + } + return app.updateMixingRules(mixesToUpdate, updatedMixingRules) == AudioSystem.SUCCESS + ? AudioManager.SUCCESS : AudioManager.ERROR; + } + } + /** see AudioPolicy.setUidDeviceAffinity() */ public int setUidDeviceAffinity(IAudioPolicyCallback pcb, int uid, @NonNull int[] deviceTypes, @NonNull String[] deviceAddresses) { @@ -12787,6 +12831,35 @@ public class AudioService extends IAudioService.Stub } + @AudioSystem.AudioSystemError int updateMixingRules( + @NonNull AudioMix[] mixesToUpdate, + @NonNull AudioMixingRule[] updatedMixingRules) { + Objects.requireNonNull(mixesToUpdate); + Objects.requireNonNull(updatedMixingRules); + + if (mixesToUpdate.length != updatedMixingRules.length) { + Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules " + + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length + + ", updatedMixingRules.length = " + updatedMixingRules.length + ")."); + return AudioSystem.BAD_VALUE; + } + + synchronized (mMixes) { + try (SafeCloseable unused = ClearCallingIdentityContext.create()) { + int ret = mAudioSystem.updateMixingRules(mixesToUpdate, updatedMixingRules); + if (ret == AudioSystem.SUCCESS) { + for (int i = 0; i < mixesToUpdate.length; i++) { + AudioMix audioMixToUpdate = mixesToUpdate[i]; + AudioMixingRule audioMixingRule = updatedMixingRules[i]; + mMixes.stream().filter(audioMixToUpdate::equals).findAny().ifPresent( + mix -> mix.setAudioMixingRule(audioMixingRule)); + } + } + return ret; + } + } + } + int setUidDeviceAffinities(int uid, @NonNull int[] types, @NonNull String[] addresses) { final Integer Uid = new Integer(uid); if (mUidDeviceAffinities.remove(Uid) != null) { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index e70b6497538e..4f46dd13f973 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -26,6 +26,7 @@ import android.media.IDevicesForAttributesCallback; import android.media.ISoundDose; import android.media.ISoundDoseCallback; import android.media.audiopolicy.AudioMix; +import android.media.audiopolicy.AudioMixingRule; import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -601,6 +602,21 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * Update already {@link AudioMixingRule}-s for already registered {@link AudioMix}-es. + * + * @param mixes - array of registered {@link AudioMix}-es to update. + * @param updatedMixingRules - array of {@link AudioMixingRule}-s corresponding to + * {@code mixesToUpdate} mixes. The array must be same size as + * {@code mixesToUpdate} and i-th {@link AudioMixingRule} must + * correspond to i-th {@link AudioMix} from mixesToUpdate array. + */ + public int updateMixingRules(@NonNull AudioMix[] mixes, + @NonNull AudioMixingRule[] updatedMixingRules) { + invalidateRoutingCache(); + return AudioSystem.updatePolicyMixes(mixes, updatedMixingRules); + } + + /** * Same as {@link AudioSystem#setUidDeviceAffinities(int, int[], String[])} * @param uid * @param types diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 1cfc7d76919a..69a6c1357350 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -33,6 +33,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_APEX; +import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; @@ -1507,7 +1508,7 @@ public class ComputerEngine implements Computer { resolveExternalPackageName(p); return packageInfo; - } else if ((flags & MATCH_UNINSTALLED_PACKAGES) != 0 + } else if ((flags & (MATCH_UNINSTALLED_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0 && PackageUserStateUtils.isAvailable(state, flags)) { PackageInfo pi = new PackageInfo(); pi.packageName = ps.getPackageName(); @@ -1516,6 +1517,7 @@ public class ComputerEngine implements Computer { pi.sharedUserId = (sharedUser != null) ? sharedUser.getName() : null; pi.firstInstallTime = state.getFirstInstallTimeMillis(); pi.lastUpdateTime = ps.getLastUpdateTime(); + pi.isArchived = isArchived(state); ApplicationInfo ai = new ApplicationInfo(); ai.packageName = ps.getPackageName(); @@ -1614,7 +1616,7 @@ public class ComputerEngine implements Computer { return generatePackageInfo(ps, flags, userId); } - if (!matchFactoryOnly && (flags & MATCH_KNOWN_PACKAGES) != 0) { + if (!matchFactoryOnly && (flags & (MATCH_KNOWN_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0) { final PackageStateInternal ps = mSettings.getPackage(packageName); if (ps == null) return null; if (filterSharedLibPackage(ps, filterCallingUid, userId, flags)) { @@ -1678,9 +1680,11 @@ public class ComputerEngine implements Computer { final boolean listUninstalled = (flags & MATCH_KNOWN_PACKAGES) != 0; final boolean listApex = (flags & MATCH_APEX) != 0; final boolean listFactory = (flags & MATCH_FACTORY_ONLY) != 0; + // Only list archived apps, not fully uninstalled ones. Other entries are unaffected. + final boolean listArchivedOnly = !listUninstalled && (flags & MATCH_ARCHIVED_PACKAGES) != 0; ArrayList<PackageInfo> list; - if (listUninstalled) { + if (listUninstalled || listArchivedOnly) { list = new ArrayList<>(mSettings.getPackages().size()); for (PackageStateInternal ps : mSettings.getPackages().values()) { if (listFactory) { @@ -1696,6 +1700,9 @@ public class ComputerEngine implements Computer { if (!listApex && ps.getPkg() != null && ps.getPkg().isApex()) { continue; } + if (listArchivedOnly && !isArchived(ps.getUserStateOrDefault(userId))) { + continue; + } if (filterSharedLibPackage(ps, callingUid, userId, flags)) { continue; } @@ -1739,6 +1746,13 @@ public class ComputerEngine implements Computer { return new ParceledListSlice<>(list); } + // TODO(b/288142708) Check for userState.isInstalled() here once this bug is fixed. + // If an app has isInstalled() == true - it should not be filtered above in any case, currently + // it is. + private static boolean isArchived(PackageUserStateInternal userState) { + return userState.getArchiveState() != null; + } + public final ResolveInfo createForwardingResolveInfoUnchecked(WatchedIntentFilter filter, int sourceUserId, int targetUserId) { ResolveInfo forwardingResolveInfo = new ResolveInfo(); @@ -2612,7 +2626,7 @@ public class ComputerEngine implements Computer { return UserHandle.getUid(userId, p.getUid()); } } - if ((flags & MATCH_KNOWN_PACKAGES) != 0) { + if ((flags & (MATCH_KNOWN_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0) { final PackageStateInternal ps = mSettings.getPackage(packageName); if (ps != null && PackageStateUtils.isMatch(ps, flags) && !shouldFilterApplication(ps, callingUid, userId)) { @@ -3671,7 +3685,7 @@ public class ComputerEngine implements Computer { ps.getAppId())); } } - if ((flags & MATCH_KNOWN_PACKAGES) != 0) { + if ((flags & (MATCH_KNOWN_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0) { if (PackageStateUtils.isMatch(ps, flags) && !shouldFilterApplication(ps, callingUid, userId)) { return mPermissionManager.getGidsForUid( @@ -4525,7 +4539,8 @@ public class ComputerEngine implements Computer { flags = updateFlagsForPackage(flags, userId); enforceCrossUserPermission(Binder.getCallingUid(), userId, true /* requireFullPermission */, false /* checkShell */, "get packages holding permissions"); - final boolean listUninstalled = (flags & MATCH_KNOWN_PACKAGES) != 0; + final boolean listUninstalled = + (flags & (MATCH_KNOWN_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0; ArrayList<PackageInfo> list = new ArrayList<>(); boolean[] tmpBools = new boolean[permissions.length]; diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 27812dffd215..4eceb7738836 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -802,9 +802,7 @@ public class PackageInfoUtils { // If available for the target user, or trying to match uninstalled packages and it's // a system app. return PackageUserStateUtils.isAvailable(state, flags) - || (pkgSetting.isSystem() - && ((flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0 - || (flags & PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS) != 0)); + || (pkgSetting.isSystem() && matchUninstalledOrHidden(flags)); } private static boolean checkUseInstalledOrHidden(long flags, @@ -819,9 +817,15 @@ public class PackageInfoUtils { // If available for the target user, or trying to match uninstalled packages and it's // a system app. return PackageUserStateUtils.isAvailable(state, flags) - || (appInfo != null && appInfo.isSystemApp() - && ((flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0 - || (flags & PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS) != 0)); + || (appInfo != null && appInfo.isSystemApp() && matchUninstalledOrHidden(flags)); + } + + private static boolean matchUninstalledOrHidden(long flags) { + return (flags + & (PackageManager.MATCH_KNOWN_PACKAGES + | PackageManager.MATCH_ARCHIVED_PACKAGES + | PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS)) + != 0; } private static void assignFieldsComponentInfoParsedMainComponent( diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateUtils.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateUtils.java index e3424534c275..54f7ebca1b66 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageUserStateUtils.java +++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateUtils.java @@ -87,9 +87,10 @@ public class PackageUserStateUtils { // still return true if the caller requested MATCH_UNINSTALLED_PACKAGES final boolean matchAnyUser = (flags & PackageManager.MATCH_ANY_USER) != 0; final boolean matchUninstalled = (flags & PackageManager.MATCH_UNINSTALLED_PACKAGES) != 0; + final boolean matchArchived = (flags & PackageManager.MATCH_ARCHIVED_PACKAGES) != 0; return matchAnyUser || (state.isInstalled() - && (!state.isHidden() || matchUninstalled)); + && (!state.isHidden() || matchUninstalled || matchArchived)); } public static boolean reportIfDebug(boolean result, long flags) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 90e67df65fec..582536b662ce 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2929,7 +2929,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A reparent(newTaskFrag, position); } - private boolean isHomeIntent(Intent intent) { + static boolean isHomeIntent(Intent intent) { return ACTION_MAIN.equals(intent.getAction()) && (intent.hasCategory(CATEGORY_HOME) || intent.hasCategory(CATEGORY_SECONDARY_HOME)) diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 1eb56f1b7d1c..a5b1132fe499 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -55,6 +55,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.util.Pair; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; @@ -77,7 +78,6 @@ class ActivityStartInterceptor { private final ActivityTaskManagerService mService; private final ActivityTaskSupervisor mSupervisor; - private final RootWindowContainer mRootWindowContainer; private final Context mServiceContext; // UserManager cannot be final as it's not ready when this class is instantiated during boot @@ -110,17 +110,23 @@ class ActivityStartInterceptor { TaskFragment mInTaskFragment; ActivityOptions mActivityOptions; + /* + * Note that this is just a hint of what the launch display area will be as it is + * based only on the information at the early pre-interception stage of starting the + * intent. The real launch display area calculated later may be different from this one. + */ + TaskDisplayArea mPresumableLaunchDisplayArea; + ActivityStartInterceptor( ActivityTaskManagerService service, ActivityTaskSupervisor supervisor) { - this(service, supervisor, service.mRootWindowContainer, service.mContext); + this(service, supervisor, service.mContext); } @VisibleForTesting ActivityStartInterceptor(ActivityTaskManagerService service, ActivityTaskSupervisor supervisor, - RootWindowContainer root, Context context) { + Context context) { mService = service; mSupervisor = supervisor; - mRootWindowContainer = root; mServiceContext = context; } @@ -162,7 +168,7 @@ class ActivityStartInterceptor { /** * A helper function to obtain the targeted {@link TaskFragment} during * {@link #intercept(Intent, ResolveInfo, ActivityInfo, String, Task, TaskFragment, int, int, - * ActivityOptions)} if any. + * ActivityOptions, TaskDisplayArea)} if any. */ @Nullable private TaskFragment getLaunchTaskFragment() { @@ -187,7 +193,7 @@ class ActivityStartInterceptor { */ boolean intercept(Intent intent, ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, Task inTask, TaskFragment inTaskFragment, int callingPid, int callingUid, - ActivityOptions activityOptions) { + ActivityOptions activityOptions, TaskDisplayArea presumableLaunchDisplayArea) { mUserManager = UserManager.get(mServiceContext); mIntent = intent; @@ -199,6 +205,7 @@ class ActivityStartInterceptor { mInTask = inTask; mInTaskFragment = inTaskFragment; mActivityOptions = activityOptions; + mPresumableLaunchDisplayArea = presumableLaunchDisplayArea; if (interceptQuietProfileIfNeeded()) { // If work profile is turned off, skip the work challenge since the profile can only @@ -221,6 +228,11 @@ class ActivityStartInterceptor { if (interceptLockedManagedProfileIfNeeded()) { return true; } + if (interceptHomeIfNeeded()) { + // Replace primary home intents directed at displays that do not support primary home + // but support secondary home with the relevant secondary home activity. + return true; + } final SparseArray<ActivityInterceptorCallback> callbacks = mService.getActivityInterceptorCallbacks(); @@ -470,6 +482,47 @@ class ActivityStartInterceptor { return true; } + private boolean interceptHomeIfNeeded() { + if (mPresumableLaunchDisplayArea == null || mService.mRootWindowContainer == null) { + return false; + } + if (!ActivityRecord.isHomeIntent(mIntent)) { + return false; + } + if (!mIntent.hasCategory(Intent.CATEGORY_HOME)) { + // Already a secondary home intent, leave it alone. + return false; + } + if (mService.mRootWindowContainer.shouldPlacePrimaryHomeOnDisplay( + mPresumableLaunchDisplayArea.getDisplayId())) { + // Primary home can be launched to the display area. + return false; + } + if (!mService.mRootWindowContainer.shouldPlaceSecondaryHomeOnDisplayArea( + mPresumableLaunchDisplayArea)) { + // Secondary home cannot be launched on the display area. + return false; + } + + // At this point we have a primary home intent for a display that does not support primary + // home activity but it supports secondary home one. So replace it with secondary home. + Pair<ActivityInfo, Intent> info = mService.mRootWindowContainer + .resolveSecondaryHomeActivity(mUserId, mPresumableLaunchDisplayArea); + mIntent = info.second; + // The new task flag is needed because the home activity should already be in the root task + // and should not be moved to the caller's task. Also, activities cannot change their type, + // e.g. a standard activity cannot become a home activity. + mIntent.addFlags(FLAG_ACTIVITY_NEW_TASK); + mCallingPid = mRealCallingPid; + mCallingUid = mRealCallingUid; + mResolvedType = null; + + mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, /* flags= */ 0, + mRealCallingUid, mRealCallingPid); + mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, /*profilerInfo=*/ null); + return true; + } + private boolean isPackageSuspended() { return mAInfo != null && mAInfo.applicationInfo != null && (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) != 0; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 1bc78d6e0820..458d1e8fa04b 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1154,10 +1154,12 @@ class ActivityStarter { } } + final TaskDisplayArea suggestedLaunchDisplayArea = + computeSuggestedLaunchDisplayArea(inTask, sourceRecord, checkedOptions); mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage, callingFeatureId); if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment, - callingPid, callingUid, checkedOptions)) { + callingPid, callingUid, checkedOptions, suggestedLaunchDisplayArea)) { // activity start was intercepted, e.g. because the target user is currently in quiet // mode (turn off work) or the target application is suspended intent = mInterceptor.mIntent; @@ -1890,6 +1892,15 @@ class ActivityStarter { mPreferredWindowingMode = mLaunchParams.mWindowingMode; } + private TaskDisplayArea computeSuggestedLaunchDisplayArea( + Task task, ActivityRecord source, ActivityOptions options) { + mSupervisor.getLaunchParamsController().calculate(task, /*layout=*/null, + /*activity=*/ null, source, options, mRequest, PHASE_DISPLAY, mLaunchParams); + return mLaunchParams.hasPreferredTaskDisplayArea() + ? mLaunchParams.mPreferredTaskDisplayArea + : mRootWindowContainer.getDefaultTaskDisplayArea(); + } + @VisibleForTesting int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) { if (r.packageName == null) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 42c363085017..53d1adfefbb9 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2648,9 +2648,15 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAmInternal.enforceCallingPermission(Manifest.permission.UPDATE_LOCK_TASK_PACKAGES, "updateLockTaskPackages()"); } - synchronized (mGlobalLock) { - ProtoLog.w(WM_DEBUG_LOCKTASK, "Allowlisting %d:%s", userId, Arrays.toString(packages)); - getLockTaskController().updateLockTaskPackages(userId, packages); + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + ProtoLog.w(WM_DEBUG_LOCKTASK, "Allowlisting %d:%s", userId, + Arrays.toString(packages)); + getLockTaskController().updateLockTaskPackages(userId, packages); + } + } finally { + Binder.restoreCallingIdentity(origId); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 57f8268b2fdc..d56acaa00f00 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1608,6 +1608,19 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } /** + * Check if the display is valid for primary home activity. + * + * @param displayId The target display ID + * @return {@code true} if allowed to launch, {@code false} otherwise. + */ + boolean shouldPlacePrimaryHomeOnDisplay(int displayId) { + // No restrictions to default display, vr 2d display or main display for visible users. + return displayId == DEFAULT_DISPLAY || (displayId != INVALID_DISPLAY + && (displayId == mService.mVr2dDisplayId + || mWmService.shouldPlacePrimaryHomeOnDisplay(displayId))); + } + + /** * Check if the display area is valid for secondary home activity. * * @param taskDisplayArea The target display area. @@ -1680,10 +1693,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> final int displayId = taskDisplayArea != null ? taskDisplayArea.getDisplayId() : INVALID_DISPLAY; - if (displayId == DEFAULT_DISPLAY || (displayId != INVALID_DISPLAY - && (displayId == mService.mVr2dDisplayId - || mWmService.shouldPlacePrimaryHomeOnDisplay(displayId)))) { - // No restrictions to default display, vr 2d display or main display for visible users. + if (shouldPlacePrimaryHomeOnDisplay(displayId)) { return true; } diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java index ad46770432a1..fa620db23306 100644 --- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java @@ -118,12 +118,14 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { root = activity; } - if (root == null) { + if (root == null && phase != PHASE_DISPLAY) { // There is a case that can lead us here. The caller is moving the top activity that is // in a task that has multiple activities to PIP mode. For that the caller is creating a // new task to host the activity so that we only move the top activity to PIP mode and // keep other activities in the previous task. There is no point to apply the launch // logic in this case. + // However, for PHASE_DISPLAY the root may be null, but we still want to get a hint of + // what the suggested launch display area would be. return RESULT_SKIP; } @@ -395,8 +397,9 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { } private TaskDisplayArea getPreferredLaunchTaskDisplayArea(@Nullable Task task, - @Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams, - @NonNull ActivityRecord activityRecord, @Nullable Request request) { + @Nullable ActivityOptions options, @Nullable ActivityRecord source, + @Nullable LaunchParams currentParams, @Nullable ActivityRecord activityRecord, + @Nullable Request request) { TaskDisplayArea taskDisplayArea = null; final WindowContainerToken optionLaunchTaskDisplayAreaToken = options != null @@ -438,8 +441,7 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { // If the source activity is a no-display activity, pass on the launch display area token // from source activity as currently preferred. - if (taskDisplayArea == null && source != null - && source.noDisplay) { + if (taskDisplayArea == null && source != null && source.noDisplay) { taskDisplayArea = source.mHandoverTaskDisplayArea; if (taskDisplayArea != null) { if (DEBUG) appendLog("display-area-from-no-display-source=" + taskDisplayArea); @@ -478,21 +480,24 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { } } - if (taskDisplayArea == null) { + if (taskDisplayArea == null && currentParams != null) { taskDisplayArea = currentParams.mPreferredTaskDisplayArea; + if (DEBUG) appendLog("display-area-from-current-params=" + taskDisplayArea); } // Re-route to default display if the device didn't declare support for multi-display if (taskDisplayArea != null && !mSupervisor.mService.mSupportsMultiDisplay && taskDisplayArea.getDisplayId() != DEFAULT_DISPLAY) { taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + if (DEBUG) appendLog("display-area-from-no-multidisplay=" + taskDisplayArea); } // Re-route to default display if the home activity doesn't support multi-display - if (taskDisplayArea != null && activityRecord.isActivityTypeHome() + if (taskDisplayArea != null && activityRecord != null && activityRecord.isActivityTypeHome() && !mSupervisor.mRootWindowContainer.canStartHomeOnDisplayArea(activityRecord.info, taskDisplayArea, false /* allowInstrumenting */)) { taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + if (DEBUG) appendLog("display-area-from-home=" + taskDisplayArea); } return (taskDisplayArea != null) @@ -516,34 +521,56 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { * @return {@link TaskDisplayArea} to house the task */ private TaskDisplayArea getFallbackDisplayAreaForActivity( - @NonNull ActivityRecord activityRecord, @Nullable Request request) { - - WindowProcessController controllerFromLaunchingRecord = mSupervisor.mService - .getProcessController(activityRecord.launchedFromPid, - activityRecord.launchedFromUid); - final TaskDisplayArea displayAreaForLaunchingRecord = controllerFromLaunchingRecord == null - ? null : controllerFromLaunchingRecord.getTopActivityDisplayArea(); - if (displayAreaForLaunchingRecord != null) { - return displayAreaForLaunchingRecord; - } + @Nullable ActivityRecord activityRecord, @Nullable Request request) { + if (activityRecord != null) { + WindowProcessController controllerFromLaunchingRecord = + mSupervisor.mService.getProcessController( + activityRecord.launchedFromPid, activityRecord.launchedFromUid); + if (controllerFromLaunchingRecord != null) { + final TaskDisplayArea taskDisplayAreaForLaunchingRecord = + controllerFromLaunchingRecord.getTopActivityDisplayArea(); + if (taskDisplayAreaForLaunchingRecord != null) { + if (DEBUG) { + appendLog("display-area-for-launching-record=" + + taskDisplayAreaForLaunchingRecord); + } + return taskDisplayAreaForLaunchingRecord; + } + } - WindowProcessController controllerFromProcess = mSupervisor.mService.getProcessController( - activityRecord.getProcessName(), activityRecord.getUid()); - final TaskDisplayArea displayAreaForRecord = controllerFromProcess == null ? null - : controllerFromProcess.getTopActivityDisplayArea(); - if (displayAreaForRecord != null) { - return displayAreaForRecord; + WindowProcessController controllerFromProcess = + mSupervisor.mService.getProcessController( + activityRecord.getProcessName(), activityRecord.getUid()); + if (controllerFromProcess != null) { + final TaskDisplayArea displayAreaForRecord = + controllerFromProcess.getTopActivityDisplayArea(); + if (displayAreaForRecord != null) { + if (DEBUG) appendLog("display-area-for-record=" + displayAreaForRecord); + return displayAreaForRecord; + } + } } - WindowProcessController controllerFromRequest = request == null ? null : mSupervisor - .mService.getProcessController(request.realCallingPid, request.realCallingUid); - final TaskDisplayArea displayAreaFromSourceProcess = controllerFromRequest == null ? null - : controllerFromRequest.getTopActivityDisplayArea(); - if (displayAreaFromSourceProcess != null) { - return displayAreaFromSourceProcess; + if (request != null) { + WindowProcessController controllerFromRequest = + mSupervisor.mService.getProcessController( + request.realCallingPid, request.realCallingUid); + if (controllerFromRequest != null) { + final TaskDisplayArea displayAreaFromSourceProcess = + controllerFromRequest.getTopActivityDisplayArea(); + if (displayAreaFromSourceProcess != null) { + if (DEBUG) { + appendLog("display-area-source-process=" + displayAreaFromSourceProcess); + } + return displayAreaFromSourceProcess; + } + } } - return mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + final TaskDisplayArea defaultTaskDisplayArea = + mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + if (DEBUG) appendLog("display-area-from-default-fallback=" + defaultTaskDisplayArea); + return defaultTaskDisplayArea; } private boolean canInheritWindowingModeFromSource(@NonNull DisplayContent display, diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index bcb0c6b5c269..0989db4c25ac 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -59,6 +59,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.testing.DexmakerShareClassLoaderRule; +import android.util.Pair; import android.util.SparseArray; import androidx.test.filters.SmallTest; @@ -128,6 +129,8 @@ public class ActivityStartInterceptorTest { private ActivityManagerInternal mAmInternal; @Mock private LockTaskController mLockTaskController; + @Mock + private TaskDisplayArea mTaskDisplayArea; private ActivityStartInterceptor mInterceptor; private ActivityInfo mAInfo = new ActivityInfo(); @@ -139,8 +142,8 @@ public class ActivityStartInterceptorTest { public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); mService.mAmInternal = mAmInternal; - mInterceptor = new ActivityStartInterceptor( - mService, mSupervisor, mRootWindowContainer, mContext); + mService.mRootWindowContainer = mRootWindowContainer; + mInterceptor = new ActivityStartInterceptor(mService, mSupervisor, mContext); mInterceptor.setStates(TEST_USER_ID, TEST_REAL_CALLING_PID, TEST_REAL_CALLING_UID, TEST_START_FLAGS, TEST_CALLING_PACKAGE, null); @@ -201,7 +204,7 @@ public class ActivityStartInterceptorTest { .thenReturn(PLATFORM_PACKAGE_NAME); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // THEN the returned intent is the admin support intent assertEquals(ADMIN_SUPPORT_INTENT, mInterceptor.mIntent); @@ -212,7 +215,7 @@ public class ActivityStartInterceptorTest { final String suspendingPackage = "com.test.suspending.package"; final SuspendDialogInfo dialogInfo = suspendPackage(suspendingPackage); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // Check intent parameters assertEquals(dialogInfo, @@ -243,7 +246,7 @@ public class ActivityStartInterceptorTest { TEST_USER_ID, TEST_PACKAGE_NAME, LOCK_TASK_LAUNCH_MODE_DEFAULT)) .thenReturn(false); - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); assertTrue(BlockedAppActivity.createIntent(TEST_USER_ID, TEST_PACKAGE_NAME) .filterEquals(mInterceptor.mIntent)); @@ -257,7 +260,8 @@ public class ActivityStartInterceptorTest { when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); // THEN calling intercept returns false because package also has to be suspended. - assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertFalse( + mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); } @Test @@ -268,7 +272,7 @@ public class ActivityStartInterceptorTest { when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // THEN the returned intent is the quiet mode intent assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) @@ -284,7 +288,7 @@ public class ActivityStartInterceptorTest { when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // THEN the returned intent is the quiet mode intent assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) @@ -300,7 +304,7 @@ public class ActivityStartInterceptorTest { when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // THEN the returned intent is the quiet mode intent assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) @@ -313,7 +317,7 @@ public class ActivityStartInterceptorTest { when(mAmInternal.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true); // THEN calling intercept returns true - mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null); // THEN the returned intent is the confirm credentials intent assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent)); @@ -329,7 +333,7 @@ public class ActivityStartInterceptorTest { mAInfo.flags |= ActivityInfo.FLAG_SHOW_WHEN_LOCKED; // THEN calling intercept returns true - mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null, null); // THEN the returned intent is original intent assertSame(originalIntent, mInterceptor.mIntent); @@ -345,7 +349,7 @@ public class ActivityStartInterceptorTest { mAInfo.directBootAware = false; // THEN calling intercept returns true - mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null); // THEN the returned intent is the confirm credentials intent assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent)); @@ -362,7 +366,7 @@ public class ActivityStartInterceptorTest { mAInfo.directBootAware = true; // THEN calling intercept returns true - mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null, null); // THEN the returned intent is original intent assertSame(originalIntent, mInterceptor.mIntent); @@ -375,7 +379,7 @@ public class ActivityStartInterceptorTest { .thenReturn("This app is bad"); // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); // THEN the returned intent is the harmful app warning intent assertEquals(HarmfulAppWarningActivity.class.getName(), @@ -383,11 +387,40 @@ public class ActivityStartInterceptorTest { } @Test + public void testHomeIntentInterception() { + // GIVEN a primary home intent and a display area that doesn't support it but supports + // secondary home activities + Intent originalIntent = new Intent(Intent.ACTION_MAIN); + originalIntent.addCategory(Intent.CATEGORY_HOME); + + Intent expectedIntent = new Intent(Intent.ACTION_MAIN); + expectedIntent.addCategory(Intent.CATEGORY_SECONDARY_HOME); + expectedIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + final int secondaryDisplayId = 7; + when(mTaskDisplayArea.getDisplayId()).thenReturn(secondaryDisplayId); + when(mRootWindowContainer.shouldPlacePrimaryHomeOnDisplay(eq(secondaryDisplayId))) + .thenReturn(false); + when(mRootWindowContainer.shouldPlaceSecondaryHomeOnDisplayArea(eq(mTaskDisplayArea))) + .thenReturn(true); + when(mRootWindowContainer.resolveSecondaryHomeActivity( + eq(TEST_USER_ID), eq(mTaskDisplayArea))) + .thenReturn(Pair.create(null, expectedIntent)); + + // THEN calling intercept returns true + assertTrue(mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, + null, mTaskDisplayArea)); + + // THEN the returned intent is the secondary home intent + assertSame(expectedIntent, mInterceptor.mIntent); + } + + @Test public void testNoInterception() { // GIVEN that none of the interception conditions are met // THEN calling intercept returns false - assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); } public void addMockInterceptorCallback( @@ -420,7 +453,7 @@ public class ActivityStartInterceptorTest { new Intent("android.test.foo"), ActivityOptions.makeBasic().setLaunchDisplayId(3)); - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); assertEquals(3, mInterceptor.mActivityOptions.getLaunchDisplayId()); } @@ -429,7 +462,7 @@ public class ActivityStartInterceptorTest { public void testInterceptionCallback_singleCallbackReturnsNull() { addMockInterceptorCallback(null, null); - assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); } @Test @@ -437,7 +470,7 @@ public class ActivityStartInterceptorTest { addMockInterceptorCallback(null, null); addMockInterceptorCallback(new Intent("android.test.second"), null); - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); assertEquals("android.test.second", mInterceptor.mIntent.getAction()); } @@ -447,7 +480,7 @@ public class ActivityStartInterceptorTest { new Intent("android.test.foo"), ActivityOptions.makeBasic().setLaunchDisplayId(3), true); ActivityInfo aInfo = mAInfo; - assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null, null)); assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); assertEquals(3, mInterceptor.mActivityOptions.getLaunchDisplayId()); assertEquals(aInfo, mInterceptor.mAInfo); // mAInfo should not be resolved @@ -459,7 +492,7 @@ public class ActivityStartInterceptorTest { new Intent("android.test.foo"), ActivityOptions.makeBasic().setLaunchDisplayId(3)); ActivityInfo aInfo = mAInfo; - assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null)); + assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null, null)); assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); assertEquals(3, mInterceptor.mActivityOptions.getLaunchDisplayId()); assertNotEquals(aInfo, mInterceptor.mAInfo); // mAInfo should be resolved after intercept @@ -488,7 +521,7 @@ public class ActivityStartInterceptorTest { when(packageManagerMock.getSdkSandboxPackageName()).thenReturn(sandboxPackageNameMock); Intent intent = new Intent().setAction(ACTION_START_SANDBOXED_ACTIVITY); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); verify(spyCallback, times(1)).onInterceptActivityLaunch( any(ActivityInterceptorCallback.ActivityInterceptorInfo.class)); @@ -505,7 +538,7 @@ public class ActivityStartInterceptorTest { when(packageManagerMock.getSdkSandboxPackageName()).thenReturn(sandboxPackageNameMock); Intent intent = new Intent().setPackage(sandboxPackageNameMock); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); verify(spyCallback, times(1)).onInterceptActivityLaunch( any(ActivityInterceptorCallback.ActivityInterceptorInfo.class)); @@ -522,7 +555,7 @@ public class ActivityStartInterceptorTest { when(packageManagerMock.getSdkSandboxPackageName()).thenReturn(sandboxPackageNameMock); Intent intent = new Intent().setComponent(new ComponentName(sandboxPackageNameMock, "")); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); verify(spyCallback, times(1)).onInterceptActivityLaunch( any(ActivityInterceptorCallback.ActivityInterceptorInfo.class)); @@ -539,23 +572,23 @@ public class ActivityStartInterceptorTest { when(packageManagerMock.getSdkSandboxPackageName()).thenReturn(sandboxPackageNameMock); // Intent: null - mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null); // Action: null, Package: null, ComponentName: null Intent intent = new Intent(); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); // Wrong Action intent = new Intent().setAction(Intent.ACTION_VIEW); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); // Wrong Package intent = new Intent().setPackage("Random"); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); // Wrong ComponentName's package intent = new Intent().setComponent(new ComponentName("Random", "")); - mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null); + mInterceptor.intercept(intent, null, mAInfo, null, null, null, 0, 0, null, null); verify(spyCallback, never()).onInterceptActivityLaunch( any(ActivityInterceptorCallback.ActivityInterceptorInfo.class)); |