summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp14
-rw-r--r--core/api/system-current.txt20
-rw-r--r--core/java/android/app/assist/AssistStructure.java3
-rw-r--r--core/java/android/content/pm/PackageInfo.java14
-rw-r--r--core/java/android/content/pm/PackageManager.java17
-rw-r--r--core/java/android/util/FeatureFlagUtils.java9
-rw-r--r--core/java/android/view/inputmethod/flags.aconfig8
-rw-r--r--core/jni/android_media_AudioSystem.cpp167
-rw-r--r--media/java/android/media/AudioSystem.java6
-rw-r--r--media/java/android/media/IAudioService.aidl7
-rw-r--r--media/java/android/media/audiopolicy/AudioMix.aidl3
-rw-r--r--media/java/android/media/audiopolicy/AudioMix.java78
-rw-r--r--media/java/android/media/audiopolicy/AudioMixingRule.aidl3
-rw-r--r--media/java/android/media/audiopolicy/AudioMixingRule.java158
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicy.java34
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicyConfig.java82
-rw-r--r--media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java274
-rw-r--r--media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java322
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt133
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt228
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt54
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt12
-rw-r--r--services/companion/java/com/android/server/companion/virtual/Android.bp3
-rw-r--r--services/companion/java/com/android/server/companion/virtual/CameraAccessController.java4
-rw-r--r--services/companion/java/com/android/server/companion/virtual/InputController.java4
-rw-r--r--services/companion/java/com/android/server/companion/virtual/SensorController.java3
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java25
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java5
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java73
-rw-r--r--services/core/java/com/android/server/audio/AudioSystemAdapter.java16
-rw-r--r--services/core/java/com/android/server/pm/ComputerEngine.java27
-rw-r--r--services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java16
-rw-r--r--services/core/java/com/android/server/pm/pkg/PackageUserStateUtils.java3
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java2
-rw-r--r--services/core/java/com/android/server/wm/ActivityStartInterceptor.java65
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java13
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java12
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java18
-rw-r--r--services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java87
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java89
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));