diff options
88 files changed, 1992 insertions, 898 deletions
diff --git a/cmds/idmap2/libidmap2/ResourceMapping.cpp b/cmds/idmap2/libidmap2/ResourceMapping.cpp index 44acbcaf8ace..f82c8f1af713 100644 --- a/cmds/idmap2/libidmap2/ResourceMapping.cpp +++ b/cmds/idmap2/libidmap2/ResourceMapping.cpp @@ -61,8 +61,7 @@ Result<Unit> CheckOverlayable(const LoadedPackage& target_package, const ResourceId& target_resource) { static constexpr const PolicyBitmask sDefaultPolicies = PolicyFlags::ODM_PARTITION | PolicyFlags::OEM_PARTITION | PolicyFlags::SYSTEM_PARTITION | - PolicyFlags::VENDOR_PARTITION | PolicyFlags::PRODUCT_PARTITION | PolicyFlags::SIGNATURE | - PolicyFlags::ACTOR_SIGNATURE; + PolicyFlags::VENDOR_PARTITION | PolicyFlags::PRODUCT_PARTITION | PolicyFlags::SIGNATURE; // If the resource does not have an overlayable definition, allow the resource to be overlaid if // the overlay is preinstalled or signed with the same signature as the target. diff --git a/cmds/idmap2/tests/ResourceMappingTests.cpp b/cmds/idmap2/tests/ResourceMappingTests.cpp index 5754eaf078a9..de039f440e33 100644 --- a/cmds/idmap2/tests/ResourceMappingTests.cpp +++ b/cmds/idmap2/tests/ResourceMappingTests.cpp @@ -287,26 +287,66 @@ TEST(ResourceMappingTests, ResourcesFromApkAssetsNoDefinedOverlayableAndNoTarget R::overlay::string::str4, false /* rewrite */)); } - -// Overlays that are pre-installed or are signed with the same signature as the target/actor can +// Overlays that are neither pre-installed nor signed with the same signature as the target cannot // overlay packages that have not defined overlayable resources. -TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPolicies) { - constexpr PolicyBitmask kDefaultPolicies = - PolicyFlags::SIGNATURE | PolicyFlags::ACTOR_SIGNATURE | PolicyFlags::PRODUCT_PARTITION | - PolicyFlags::SYSTEM_PARTITION | PolicyFlags::VENDOR_PARTITION | PolicyFlags::ODM_PARTITION | - PolicyFlags::OEM_PARTITION; +TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPoliciesPublicFail) { + auto resources = TestGetResourceMapping("/target/target-no-overlayable.apk", + "/overlay/overlay-no-name.apk", PolicyFlags::PUBLIC, + /* enforce_overlayable */ true); + + ASSERT_TRUE(resources) << resources.GetErrorMessage(); + ASSERT_EQ(resources->GetTargetToOverlayMap().size(), 0U); +} - for (PolicyBitmask policy = 1U << (sizeof(PolicyBitmask) * 8 - 1); policy > 0; - policy = policy >> 1U) { +// Overlays that are pre-installed or are signed with the same signature as the target can overlay +// packages that have not defined overlayable resources. +TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPolicies) { + auto CheckEntries = [&](const PolicyBitmask& fulfilled_policies) -> void { auto resources = TestGetResourceMapping("/target/target-no-overlayable.apk", "/system-overlay-invalid/system-overlay-invalid.apk", - policy, /* enforce_overlayable */ true); - ASSERT_TRUE(resources) << resources.GetErrorMessage(); + fulfilled_policies, + /* enforce_overlayable */ true); - const size_t expected_overlaid = (policy & kDefaultPolicies) != 0 ? 10U : 0U; - ASSERT_EQ(expected_overlaid, resources->GetTargetToOverlayMap().size()) - << "Incorrect number of resources overlaid through policy " << policy; - } + ASSERT_TRUE(resources) << resources.GetErrorMessage(); + auto& res = *resources; + ASSERT_EQ(resources->GetTargetToOverlayMap().size(), 10U); + ASSERT_RESULT(MappingExists(res, R::target::string::not_overlayable, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::not_overlayable, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::other, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::other, false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_actor, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_actor, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_odm, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_odm, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_oem, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_oem, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_product, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_product, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_public, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_public, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_signature, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_signature, + false /* rewrite */)); + ASSERT_RESULT(MappingExists(res, R::target::string::policy_system, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_system, + false /* rewrite */)); + ASSERT_RESULT(MappingExists( + res, R::target::string::policy_system_vendor, Res_value::TYPE_REFERENCE, + R::system_overlay_invalid::string::policy_system_vendor, false /* rewrite */)); + }; + + CheckEntries(PolicyFlags::SIGNATURE); + CheckEntries(PolicyFlags::PRODUCT_PARTITION); + CheckEntries(PolicyFlags::SYSTEM_PARTITION); + CheckEntries(PolicyFlags::VENDOR_PARTITION); + CheckEntries(PolicyFlags::ODM_PARTITION); + CheckEntries(PolicyFlags::OEM_PARTITION); } } // namespace android::idmap2 diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 9abae528b474..81d059ed84d9 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -446,6 +446,9 @@ message Atom { 277 [(module) = "settings"]; CellBroadcastMessageFiltered cb_message_filtered = 278 [(module) = "cellbroadcast"]; + TvTunerDvrStatus tv_tuner_dvr_status = 279 [(module) = "framework"]; + TvCasSessionOpenStatus tv_cas_session_open_status = + 280 [(module) = "framework"]; // StatsdStats tracks platform atoms with ids upto 500. // Update StatsdStats::kMaxPushedAtomId when atom ids here approach that value. @@ -9242,6 +9245,58 @@ message TvTunerStateChanged { // new state optional State state = 2; } + +/** + * Logs the status of a dvr playback or record. + * This is atom ID 279. + * + * Logged from: + * frameworks/base/media/java/android/media/tv/tuner/dvr + */ +message TvTunerDvrStatus { + enum Type { + UNKNOWN_TYPE = 0; + PLAYBACK = 1; // is a playback + RECORD = 2; // is a record + } + enum State { + UNKNOWN_STATE = 0; + STARTED = 1; // DVR is started + STOPPED = 2; // DVR is stopped + } + // The uid of the application that sent this custom atom. + optional int32 uid = 1 [(is_uid) = true]; + // DVR type + optional Type type = 2; + // DVR state + optional State state = 3; + // Identify the segment of a record or playback + optional int32 segment_id = 4; + // indicate how many overflow or underflow happened between started to stopped + optional int32 overflow_underflow_count = 5; +} + +/** + * Logs when a cas session opened through MediaCas. + * This is atom ID 280. + * + * Logged from: + * frameworks/base/media/java/android/media/MediaCas.java + */ +message TvCasSessionOpenStatus { + enum State { + UNKNOWN = 0; + SUCCEEDED = 1; // indicate that the session is opened successfully. + FAILED = 2; // indicate that the session isn’t opened successfully. + } + // The uid of the application that sent this custom atom. + optional int32 uid = 1 [(is_uid) = true]; + // Cas system Id + optional int32 cas_system_id = 2; + // State of the session + optional State state = 3; +} + /** * Logs when an app is frozen or unfrozen. * diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d275159e9f87..108b9eec34fb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3252,18 +3252,56 @@ public final class ActivityThread extends ClientTransactionHandler { @Override public void handleFixedRotationAdjustments(@NonNull IBinder token, @Nullable FixedRotationAdjustments fixedRotationAdjustments) { - final Consumer<DisplayAdjustments> override = fixedRotationAdjustments != null - ? displayAdjustments -> displayAdjustments.setFixedRotationAdjustments( - fixedRotationAdjustments) - : null; + handleFixedRotationAdjustments(token, fixedRotationAdjustments, null /* overrideConfig */); + } + + /** + * Applies the rotation adjustments to override display information in resources belong to the + * provided token. If the token is activity token, the adjustments also apply to application + * because the appearance of activity is usually more sensitive to the application resources. + * + * @param token The token to apply the adjustments. + * @param fixedRotationAdjustments The information to override the display adjustments of + * corresponding resources. If it is null, the exiting override + * will be cleared. + * @param overrideConfig The override configuration of activity. It is used to override + * application configuration. If it is non-null, it means the token is + * confirmed as activity token. Especially when launching new activity, + * {@link #mActivities} hasn't put the new token. + */ + private void handleFixedRotationAdjustments(@NonNull IBinder token, + @Nullable FixedRotationAdjustments fixedRotationAdjustments, + @Nullable Configuration overrideConfig) { + // The element of application configuration override is set only if the application + // adjustments are needed, because activity already has its own override configuration. + final Configuration[] appConfigOverride; + final Consumer<DisplayAdjustments> override; + if (fixedRotationAdjustments != null) { + appConfigOverride = new Configuration[1]; + override = displayAdjustments -> { + displayAdjustments.setFixedRotationAdjustments(fixedRotationAdjustments); + if (appConfigOverride[0] != null) { + displayAdjustments.getConfiguration().updateFrom(appConfigOverride[0]); + } + }; + } else { + appConfigOverride = null; + override = null; + } if (!mResourcesManager.overrideTokenDisplayAdjustments(token, override)) { // No resources are associated with the token. return; } - if (mActivities.get(token) == null) { - // Only apply the override to application for activity token because the appearance of - // activity is usually more sensitive to the application resources. - return; + if (overrideConfig == null) { + final ActivityClientRecord r = mActivities.get(token); + if (r == null) { + // It is not an activity token. Nothing to do for application. + return; + } + overrideConfig = r.overrideConfig; + } + if (appConfigOverride != null) { + appConfigOverride[0] = overrideConfig; } // Apply the last override to application resources for compatibility. Because the Resources @@ -3503,7 +3541,8 @@ public final class ActivityThread extends ClientTransactionHandler { // The rotation adjustments must be applied before creating the activity, so the activity // can get the adjusted display info during creation. if (r.mPendingFixedRotationAdjustments != null) { - handleFixedRotationAdjustments(r.token, r.mPendingFixedRotationAdjustments); + handleFixedRotationAdjustments(r.token, r.mPendingFixedRotationAdjustments, + r.overrideConfig); r.mPendingFixedRotationAdjustments = null; } diff --git a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java index f64560a14832..fb8fd74545c7 100644 --- a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java +++ b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java @@ -302,7 +302,14 @@ public class ParsedActivityUtils { } String permission = array.getNonConfigurationString(permissionAttr, 0); - activity.setPermission(permission != null ? permission : pkg.getPermission()); + if (isAlias) { + // An alias will override permissions to allow referencing an Activity through its alias + // without needing the original permission. If an alias needs the same permission, + // it must be re-declared. + activity.setPermission(permission); + } else { + activity.setPermission(permission != null ? permission : pkg.getPermission()); + } final boolean setExported = array.hasValue(exportedAttr); if (setExported) { diff --git a/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java b/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java index b37b61757053..6811e06fbe7e 100644 --- a/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java +++ b/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java @@ -20,7 +20,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.PackageManager; import android.content.pm.parsing.ParsingPackage; +import android.content.pm.parsing.ParsingPackageUtils; import android.content.pm.parsing.ParsingUtils; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -29,9 +32,6 @@ import android.text.TextUtils; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; -import android.content.pm.parsing.ParsingPackageUtils; -import android.content.pm.parsing.result.ParseInput; -import android.content.pm.parsing.result.ParseResult; /** @hide */ class ParsedComponentUtils { @@ -60,16 +60,27 @@ class ParsedComponentUtils { component.setName(className); component.setPackageName(packageName); - if (useRoundIcon) { - component.icon = array.getResourceId(roundIconAttr, 0); + int roundIconVal = useRoundIcon ? array.getResourceId(roundIconAttr, 0) : 0; + if (roundIconVal != 0) { + component.icon = roundIconVal; + component.nonLocalizedLabel = null; + } else { + int iconVal = array.getResourceId(iconAttr, 0); + if (iconVal != 0) { + component.icon = iconVal; + component.nonLocalizedLabel = null; + } } - if (component.icon == 0) { - component.icon = array.getResourceId(iconAttr, 0); + int logoVal = array.getResourceId(logoAttr, 0); + if (logoVal != 0) { + component.logo = logoVal; } - component.logo = array.getResourceId(logoAttr, 0); - component.banner = array.getResourceId(bannerAttr, 0); + int bannerVal = array.getResourceId(bannerAttr, 0); + if (bannerVal != 0) { + component.banner = bannerVal; + } if (descriptionAttr != null) { component.descriptionRes = array.getResourceId(descriptionAttr, 0); diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index 6bc962b67576..208406566e52 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -18,6 +18,7 @@ package android.hardware.hdmi; import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,6 +31,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Binder; import android.os.RemoteException; import android.os.SystemProperties; import android.util.ArrayMap; @@ -40,6 +42,7 @@ import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executor; /** * The {@link HdmiControlManager} class is used to send HDMI control messages @@ -818,6 +821,24 @@ public final class HdmiControlManager { mHdmiControlStatusChangeListeners = new ArrayMap<>(); /** + * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled). + * @hide + */ + public interface HdmiCecVolumeControlFeatureListener { + /** + * Called when the HDMI Control (CEC) volume control feature is enabled/disabled. + * + * @param enabled status of HDMI CEC volume control feature + * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()} + **/ + void onHdmiCecVolumeControlFeature(boolean enabled); + } + + private final ArrayMap<HdmiCecVolumeControlFeatureListener, + IHdmiCecVolumeControlFeatureListener> + mHdmiCecVolumeControlFeatureListeners = new ArrayMap<>(); + + /** * Listener used to get vendor-specific commands. */ public interface VendorCommandListener { @@ -979,4 +1000,76 @@ public final class HdmiControlManager { }; } + /** + * Adds a listener to get informed of changes to the state of the HDMI CEC volume control + * feature. + * + * Upon adding a listener, the current state of the HDMI CEC volume control feature will be + * sent immediately. + * + * <p>To stop getting the notification, + * use {@link #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener)}. + * + * @param listener {@link HdmiCecVolumeControlFeatureListener} instance + * @hide + * @see #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener) + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void addHdmiCecVolumeControlFeatureListener(@NonNull @CallbackExecutor Executor executor, + @NonNull HdmiCecVolumeControlFeatureListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + if (mHdmiCecVolumeControlFeatureListeners.containsKey(listener)) { + Log.e(TAG, "listener is already registered"); + return; + } + IHdmiCecVolumeControlFeatureListener wrappedListener = + createHdmiCecVolumeControlFeatureListenerWrapper(executor, listener); + mHdmiCecVolumeControlFeatureListeners.put(listener, wrappedListener); + try { + mService.addHdmiCecVolumeControlFeatureListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a listener to stop getting informed of changes to the state of the HDMI CEC volume + * control feature. + * + * @param listener {@link HdmiCecVolumeControlFeatureListener} instance to be removed + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void removeHdmiCecVolumeControlFeatureListener( + HdmiCecVolumeControlFeatureListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + IHdmiCecVolumeControlFeatureListener wrappedListener = + mHdmiCecVolumeControlFeatureListeners.remove(listener); + if (wrappedListener == null) { + Log.e(TAG, "tried to remove not-registered listener"); + return; + } + try { + mService.removeHdmiCecVolumeControlFeatureListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private IHdmiCecVolumeControlFeatureListener createHdmiCecVolumeControlFeatureListenerWrapper( + Executor executor, final HdmiCecVolumeControlFeatureListener listener) { + return new android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener.Stub() { + @Override + public void onHdmiCecVolumeControlFeature(boolean enabled) { + Binder.clearCallingIdentity(); + executor.execute(() -> listener.onHdmiCecVolumeControlFeature(enabled)); + } + }; + } } diff --git a/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl new file mode 100644 index 000000000000..873438bb1d20 --- /dev/null +++ b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 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 android.hardware.hdmi; + +/** + * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled). + * @hide + */ +oneway interface IHdmiCecVolumeControlFeatureListener { + + /** + * Called when the HDMI Control (CEC) volume control feature is enabled/disabled. + * + * @param enabled status of HDMI CEC volume control feature + * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()} + **/ + void onHdmiCecVolumeControlFeature(boolean enabled); +} diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl index 3582a927ff46..4c724ef62ea9 100644 --- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -18,6 +18,7 @@ package android.hardware.hdmi; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; +import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlStatusChangeListener; import android.hardware.hdmi.IHdmiDeviceEventListener; @@ -44,6 +45,8 @@ interface IHdmiControlService { void queryDisplayStatus(IHdmiControlCallback callback); void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); void removeHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); + void addHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener); + void removeHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener); void addHotplugEventListener(IHdmiHotplugEventListener listener); void removeHotplugEventListener(IHdmiHotplugEventListener listener); void addDeviceEventListener(IHdmiDeviceEventListener listener); diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 4ca48cb3e57c..e8806a03d00e 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1365,6 +1365,7 @@ public class StorageManager { String[] packageNames = ActivityThread.getPackageManager().getPackagesForUid( android.os.Process.myUid()); if (packageNames == null || packageNames.length <= 0) { + Log.w(TAG, "Missing package names; no storage volumes available"); return new StorageVolume[0]; } packageName = packageNames[0]; @@ -1372,6 +1373,7 @@ public class StorageManager { final int uid = ActivityThread.getPackageManager().getPackageUid(packageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId); if (uid <= 0) { + Log.w(TAG, "Missing UID; no storage volumes available"); return new StorageVolume[0]; } return storageManager.getVolumeList(uid, packageName, flags); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 52764d568f29..e10fceaa5bc7 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14254,15 +14254,6 @@ public final class Settings { public static final String KERNEL_CPU_THREAD_READER = "kernel_cpu_thread_reader"; /** - * Persistent user id that is last logged in to. - * - * They map to user ids, for example, 10, 11, 12. - * - * @hide - */ - public static final String LAST_ACTIVE_USER_ID = "last_active_persistent_user_id"; - - /** * Whether we've enabled native flags health check on this device. Takes effect on * reboot. The value "1" enables native flags health check; otherwise it's disabled. * @hide diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java index d01bc2524332..8383072a48e3 100644 --- a/core/java/android/service/controls/Control.java +++ b/core/java/android/service/controls/Control.java @@ -73,25 +73,37 @@ public final class Control implements Parcelable { }) public @interface Status {}; + /** + * Reserved for use with the {@link StatelessBuilder}, and while loading. When state is + * requested via {@link ControlsProviderService#createPublisherFor}, use other status codes + * to indicate the proper device state. + */ public static final int STATUS_UNKNOWN = 0; /** - * The device corresponding to the {@link Control} is responding correctly. + * Used to indicate that the state of the device was successfully retrieved. This includes + * all scenarios where the device may have a warning for the user, such as "Lock jammed", + * or "Vacuum stuck". Any information for the user should be set through + * {@link StatefulBuilder#setStatusText}. */ public static final int STATUS_OK = 1; /** - * The device corresponding to the {@link Control} cannot be found or was removed. + * The device corresponding to the {@link Control} cannot be found or was removed. The user + * will be alerted and directed to the application to resolve. */ public static final int STATUS_NOT_FOUND = 2; /** - * The device corresponding to the {@link Control} is in an error state. + * Used to indicate that there was a temporary error while loading the device state. A default + * error message will be displayed in place of any custom text that was set through + * {@link StatefulBuilder#setStatusText}. */ public static final int STATUS_ERROR = 3; /** - * The {@link Control} is currently disabled. + * The {@link Control} is currently disabled. A default error message will be displayed in + * place of any custom text that was set through {@link StatefulBuilder#setStatusText}. */ public static final int STATUS_DISABLED = 4; diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index e4fbf9f0e187..9b293eb463e5 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -340,6 +340,10 @@ public class PhoneStateListener { /** * Listen for display info changed event. * + * Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE + * READ_PHONE_STATE} or that the calling app has carrier privileges (see + * {@link TelephonyManager#hasCarrierPrivileges}). + * * @see #onDisplayInfoChanged */ public static final int LISTEN_DISPLAY_INFO_CHANGED = 0x00100000; diff --git a/core/java/android/window/VirtualDisplayTaskEmbedder.java b/core/java/android/window/VirtualDisplayTaskEmbedder.java index d2614da31ff9..9ccb4c172158 100644 --- a/core/java/android/window/VirtualDisplayTaskEmbedder.java +++ b/core/java/android/window/VirtualDisplayTaskEmbedder.java @@ -365,8 +365,8 @@ public class VirtualDisplayTaskEmbedder extends TaskEmbedder { // Found the topmost stack on target display. Now check if the topmost task's // description changed. if (taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) { - mHost.onTaskBackgroundColorChanged(VirtualDisplayTaskEmbedder.this, - taskInfo.taskDescription.getBackgroundColor()); + mHost.post(()-> mHost.onTaskBackgroundColorChanged(VirtualDisplayTaskEmbedder.this, + taskInfo.taskDescription.getBackgroundColor())); } } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 049a76c89815..8f3edb8f1787 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2654,6 +2654,7 @@ public class ChooserActivity extends ResolverActivity implements if (recyclerView.getVisibility() == View.VISIBLE) { int directShareHeight = 0; rowsToShow = Math.min(4, rowsToShow); + boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); mLastNumberOfChildren = recyclerView.getChildCount(); for (int i = 0, childCount = recyclerView.getChildCount(); i < childCount && rowsToShow > 0; i++) { @@ -2664,6 +2665,9 @@ public class ChooserActivity extends ResolverActivity implements } int height = child.getHeight(); offset += height; + if (shouldShowExtraRow) { + offset += height; + } if (gridAdapter.getTargetType( recyclerView.getChildAdapterPosition(child)) @@ -2699,6 +2703,18 @@ public class ChooserActivity extends ResolverActivity implements } /** + * If we have a tabbed view and are showing 1 row in the current profile and an empty + * state screen in the other profile, to prevent cropping of the empty state screen we show + * a second row in the current profile. + */ + private boolean shouldShowExtraRow(int rowsToShow) { + return shouldShowTabs() + && rowsToShow == 1 + && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen( + mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); + } + + /** * Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle * does not match either the personal or work user handle. **/ diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index c75f72bdc765..0d2dbefb9cd7 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -1223,7 +1223,6 @@ public class ConversationLayout extends FrameLayout mExpandButtonContainer.setVisibility(VISIBLE); mExpandButtonInnerContainer.setOnClickListener(onClickListener); } else { - // TODO: handle content paddings to end of layout mExpandButtonContainer.setVisibility(GONE); } updateContentEndPaddings(); diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 3d8cae8e74d0..5c444bda1838 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -1744,6 +1744,8 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE; } android_mallopt(M_SET_HEAP_TAGGING_LEVEL, &heap_tagging_level, sizeof(heap_tagging_level)); + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART runtime. + runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK; bool forceEnableGwpAsan = false; switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { @@ -1756,6 +1758,8 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: android_mallopt(M_INITIALIZE_GWP_ASAN, &forceEnableGwpAsan, sizeof(forceEnableGwpAsan)); } + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART runtime. + runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK; if (NeedsNoRandomizeWorkaround()) { // Work around ARM kernel ASLR lossage (http://b/5817320). diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2cad9e6888a6..b79c9e804f89 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4456,11 +4456,4 @@ <bool name="config_pdp_reject_enable_retry">false</bool> <!-- pdp data reject retry delay in ms --> <integer name="config_pdp_reject_retry_delay_ms">-1</integer> - - <!-- Package name that is recognized as an actor for the packages listed in - @array/config_overlayableConfiguratorTargets. If an overlay targeting one of the listed - targets is signed with the same signature as the configurator, the overlay will be granted - the "actor" policy. --> - <string name="config_overlayableConfigurator" translatable="false" /> - <string-array name="config_overlayableConfiguratorTargets" translatable="false" /> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f30d482e06b2..53cdeb5e5c1c 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4033,8 +4033,5 @@ <java-symbol type="string" name="config_pdp_reject_service_not_subscribed" /> <java-symbol type="string" name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" /> - <java-symbol type="string" name="config_overlayableConfigurator" /> - <java-symbol type="array" name="config_overlayableConfiguratorTargets" /> - <java-symbol type="array" name="config_notificationMsgPkgsAllowedAsConvos" /> </resources> diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java index 7cd2f3b4c2ab..a4f206586625 100644 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java @@ -363,6 +363,16 @@ public class HdmiAudioSystemClientTest { public boolean isHdmiCecVolumeControlEnabled() { return true; } + + @Override + public void addHdmiCecVolumeControlFeatureListener( + IHdmiCecVolumeControlFeatureListener listener) { + } + + @Override + public void removeHdmiCecVolumeControlFeatureListener( + IHdmiCecVolumeControlFeatureListener listener) { + } } } diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java index fe33cd80f735..4b8173732b4d 100644 --- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java +++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java @@ -29,11 +29,12 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; -import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.WindowManager; @@ -91,8 +92,7 @@ public final class ScreenshotHelperTest { @Test public void testProvidedImageScreenshot() { mScreenshotHelper.provideScreenshot( - Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888), new Rect(), - Insets.of(0, 0, 0, 0), 1, + new Bundle(), new Rect(), Insets.of(0, 0, 0, 0), 1, 1, new ComponentName("", ""), WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null); } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index aa4e9f20c7fc..9b503eba5c68 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -378,6 +378,9 @@ applications that come with the platform <permission name="android.permission.SET_WALLPAPER" /> <permission name="android.permission.SET_WALLPAPER_COMPONENT" /> <permission name="android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE" /> + <!-- Permissions required for Incremental CTS tests --> + <permission name="com.android.permission.USE_INSTALLER_V2"/> + <permission name="android.permission.LOADER_USAGE_STATS"/> <!-- Permission required to test system only camera devices. --> <permission name="android.permission.SYSTEM_CAMERA" /> <!-- Permission required to test ExplicitHealthCheckServiceImpl. --> diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index c652628eb425..590def4d4ced 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -20,6 +20,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; +import android.app.ActivityManager; import android.content.Context; import android.hardware.cas.V1_0.HidlCasPluginDescriptor; import android.hardware.cas.V1_0.ICas; @@ -43,6 +44,8 @@ import android.os.RemoteException; import android.util.Log; import android.util.Singleton; +import com.android.internal.util.FrameworkStatsLog; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -122,6 +125,7 @@ public final class MediaCas implements AutoCloseable { private String mTvInputServiceSessionId; private int mClientId; private int mCasSystemId; + private int mUserId; private TunerResourceManager mTunerResourceManager = null; private final Map<Session, Integer> mSessionMap = new HashMap<>(); @@ -673,6 +677,8 @@ public final class MediaCas implements AutoCloseable { */ public MediaCas(int CA_system_id) throws UnsupportedCasException { try { + mCasSystemId = CA_system_id; + mUserId = ActivityManager.getCurrentUser(); IMediaCasService service = getService(); android.hardware.cas.V1_2.IMediaCasService serviceV12 = android.hardware.cas.V1_2.IMediaCasService.castFrom(service); @@ -721,7 +727,6 @@ public final class MediaCas implements AutoCloseable { this(casSystemId); Objects.requireNonNull(context, "context must not be null"); - mCasSystemId = casSystemId; mTunerResourceManager = (TunerResourceManager) context.getSystemService(Context.TV_TUNER_RESOURCE_MGR_SERVICE); if (mTunerResourceManager != null) { @@ -925,10 +930,18 @@ public final class MediaCas implements AutoCloseable { mICas.openSession(cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); addSessionToResourceMap(cb.mSession, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); return cb.mSession; } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } + Log.d(TAG, "Write Stats Log for fail to Open Session."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED); return null; } @@ -964,10 +977,18 @@ public final class MediaCas implements AutoCloseable { mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); addSessionToResourceMap(cb.mSession, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); return cb.mSession; } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } + Log.d(TAG, "Write Stats Log for fail to Open Session."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED); return null; } diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 981bf7af9f25..05c6e3ad9392 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -137,7 +137,7 @@ public abstract class MediaRoute2ProviderService extends Service { private final AtomicBoolean mStatePublishScheduled = new AtomicBoolean(false); private MediaRoute2ProviderServiceStub mStub; private IMediaRoute2ProviderServiceCallback mRemoteCallback; - private MediaRoute2ProviderInfo mProviderInfo; + private volatile MediaRoute2ProviderInfo mProviderInfo; @GuardedBy("mSessionLock") private ArrayMap<String, RoutingSessionInfo> mSessionInfo = new ArrayMap<>(); @@ -167,8 +167,8 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Called when a volume setting is requested on a route of the provider * - * @param requestId the id of this request - * @param routeId the id of the route + * @param requestId the ID of this request + * @param routeId the ID of the route * @param volume the target volume * @see MediaRoute2Info.Builder#setVolume(int) */ @@ -178,8 +178,8 @@ public abstract class MediaRoute2ProviderService extends Service { * Called when {@link MediaRouter2.RoutingController#setVolume(int)} is called on * a routing session of the provider * - * @param requestId the id of this request - * @param sessionId the id of the routing session + * @param requestId the ID of this request + * @param sessionId the ID of the routing session * @param volume the target volume * @see RoutingSessionInfo.Builder#setVolume(int) */ @@ -188,7 +188,7 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Gets information of the session with the given id. * - * @param sessionId id of the session + * @param sessionId the ID of the session * @return information of the session with the given id. * null if the session is released or ID is not valid. */ @@ -218,7 +218,7 @@ public abstract class MediaRoute2ProviderService extends Service { * If this session is created without any creation request, use {@link #REQUEST_ID_NONE} * as the request ID. * - * @param requestId id of the previous request to create this session provided in + * @param requestId the ID of the previous request to create this session provided in * {@link #onCreateSession(long, String, String, Bundle)}. Can be * {@link #REQUEST_ID_NONE} if this session is created without any request. * @param sessionInfo information of the new session. @@ -237,18 +237,15 @@ public abstract class MediaRoute2ProviderService extends Service { return; } mSessionInfo.put(sessionInfo.getId(), sessionInfo); - } - if (mRemoteCallback == null) { - return; - } - try { - // TODO(b/157873487): Calling binder calls in multiple thread may cause timing issue. - // Consider to change implementations to avoid the problems. - // For example, post binder calls, always send all sessions at once, etc. - mRemoteCallback.notifySessionCreated(requestId, sessionInfo); - } catch (RemoteException ex) { - Log.w(TAG, "Failed to notify session created."); + if (mRemoteCallback == null) { + return; + } + try { + mRemoteCallback.notifySessionCreated(requestId, sessionInfo); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to notify session created."); + } } } @@ -267,22 +264,22 @@ public abstract class MediaRoute2ProviderService extends Service { Log.w(TAG, "Ignoring unknown session info."); return; } - } - if (mRemoteCallback == null) { - return; - } - try { - mRemoteCallback.notifySessionUpdated(sessionInfo); - } catch (RemoteException ex) { - Log.w(TAG, "Failed to notify session info changed."); + if (mRemoteCallback == null) { + return; + } + try { + mRemoteCallback.notifySessionUpdated(sessionInfo); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to notify session info changed."); + } } } /** * Notifies that the session is released. * - * @param sessionId id of the released session. + * @param sessionId the ID of the released session. * @see #onReleaseSession(long, String) */ public final void notifySessionReleased(@NonNull String sessionId) { @@ -292,20 +289,20 @@ public abstract class MediaRoute2ProviderService extends Service { RoutingSessionInfo sessionInfo; synchronized (mSessionLock) { sessionInfo = mSessionInfo.remove(sessionId); - } - if (sessionInfo == null) { - Log.w(TAG, "Ignoring unknown session info."); - return; - } + if (sessionInfo == null) { + Log.w(TAG, "Ignoring unknown session info."); + return; + } - if (mRemoteCallback == null) { - return; - } - try { - mRemoteCallback.notifySessionReleased(sessionInfo); - } catch (RemoteException ex) { - Log.w(TAG, "Failed to notify session info changed."); + if (mRemoteCallback == null) { + return; + } + try { + mRemoteCallback.notifySessionReleased(sessionInfo); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to notify session info changed."); + } } } @@ -348,9 +345,9 @@ public abstract class MediaRoute2ProviderService extends Service { * If you can't create the session or want to reject the request, call * {@link #notifyRequestFailed(long, int)} with the given {@code requestId}. * - * @param requestId the id of this request + * @param requestId the ID of this request * @param packageName the package name of the application that selected the route - * @param routeId the id of the route initially being connected + * @param routeId the ID of the route initially being connected * @param sessionHints an optional bundle of app-specific arguments sent by * {@link MediaRouter2}, or null if none. The contents of this bundle * may affect the result of session creation. @@ -372,8 +369,8 @@ public abstract class MediaRoute2ProviderService extends Service { * Note: Calling {@link #notifySessionReleased(String)} will <em>NOT</em> trigger * this method to be called. * - * @param requestId the id of this request - * @param sessionId id of the session being released. + * @param requestId the ID of this request + * @param sessionId the ID of the session being released. * @see #notifySessionReleased(String) * @see #getSessionInfo(String) */ @@ -384,9 +381,9 @@ public abstract class MediaRoute2ProviderService extends Service { * After the route is selected, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * - * @param requestId the id of this request - * @param sessionId id of the session - * @param routeId id of the route + * @param requestId the ID of this request + * @param sessionId the ID of the session + * @param routeId the ID of the route */ public abstract void onSelectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); @@ -396,9 +393,9 @@ public abstract class MediaRoute2ProviderService extends Service { * After the route is deselected, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * - * @param requestId the id of this request - * @param sessionId id of the session - * @param routeId id of the route + * @param requestId the ID of this request + * @param sessionId the ID of the session + * @param routeId the ID of the route */ public abstract void onDeselectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); @@ -408,9 +405,9 @@ public abstract class MediaRoute2ProviderService extends Service { * After the transfer is finished, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * - * @param requestId the id of this request - * @param sessionId id of the session - * @param routeId id of the route + * @param requestId the ID of this request + * @param sessionId the ID of the session + * @param routeId the ID of the route */ public abstract void onTransferToRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); @@ -475,13 +472,39 @@ public abstract class MediaRoute2ProviderService extends Service { final class MediaRoute2ProviderServiceStub extends IMediaRoute2ProviderService.Stub { MediaRoute2ProviderServiceStub() { } - boolean checkCallerisSystem() { + private boolean checkCallerIsSystem() { return Binder.getCallingUid() == Process.SYSTEM_UID; } + private boolean checkSessionIdIsValid(String sessionId, String description) { + if (TextUtils.isEmpty(sessionId)) { + Log.w(TAG, description + ": Ignoring empty sessionId from system service."); + return false; + } + if (getSessionInfo(sessionId) == null) { + Log.w(TAG, description + ": Ignoring unknown session from system service. " + + "sessionId=" + sessionId); + return false; + } + return true; + } + + private boolean checkRouteIdIsValid(String routeId, String description) { + if (TextUtils.isEmpty(routeId)) { + Log.w(TAG, description + ": Ignoring empty routeId from system service."); + return false; + } + if (mProviderInfo == null || mProviderInfo.getRoute(routeId) == null) { + Log.w(TAG, description + ": Ignoring unknown route from system service. " + + "routeId=" + routeId); + return false; + } + return true; + } + @Override public void setCallback(IMediaRoute2ProviderServiceCallback callback) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::setCallback, @@ -490,7 +513,7 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } mHandler.sendMessage(obtainMessage( @@ -500,7 +523,10 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void setRouteVolume(long requestId, String routeId, int volume) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { + return; + } + if (!checkRouteIdIsValid(routeId, "setRouteVolume")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetRouteVolume, @@ -510,7 +536,10 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void requestCreateSession(long requestId, String packageName, String routeId, @Nullable Bundle requestCreateSession) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { + return; + } + if (!checkRouteIdIsValid(routeId, "requestCreateSession")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onCreateSession, @@ -518,14 +547,13 @@ public abstract class MediaRoute2ProviderService extends Service { requestCreateSession)); } - //TODO(b/157873546): Ignore requests with unknown session ID. -> For all similar commands. @Override public void selectRoute(long requestId, String sessionId, String routeId) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } - if (TextUtils.isEmpty(sessionId)) { - Log.w(TAG, "selectRoute: Ignoring empty sessionId from system service."); + if (!checkSessionIdIsValid(sessionId, "selectRoute") + || !checkRouteIdIsValid(routeId, "selectRoute")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelectRoute, @@ -534,11 +562,11 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void deselectRoute(long requestId, String sessionId, String routeId) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } - if (TextUtils.isEmpty(sessionId)) { - Log.w(TAG, "deselectRoute: Ignoring empty sessionId from system service."); + if (!checkSessionIdIsValid(sessionId, "deselectRoute") + || !checkRouteIdIsValid(routeId, "deselectRoute")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onDeselectRoute, @@ -547,11 +575,11 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void transferToRoute(long requestId, String sessionId, String routeId) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } - if (TextUtils.isEmpty(sessionId)) { - Log.w(TAG, "transferToRoute: Ignoring empty sessionId from system service."); + if (!checkSessionIdIsValid(sessionId, "transferToRoute") + || !checkRouteIdIsValid(routeId, "transferToRoute")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onTransferToRoute, @@ -560,7 +588,10 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void setSessionVolume(long requestId, String sessionId, int volume) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { + return; + } + if (!checkSessionIdIsValid(sessionId, "setSessionVolume")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetSessionVolume, @@ -569,11 +600,10 @@ public abstract class MediaRoute2ProviderService extends Service { @Override public void releaseSession(long requestId, String sessionId) { - if (!checkCallerisSystem()) { + if (!checkCallerIsSystem()) { return; } - if (TextUtils.isEmpty(sessionId)) { - Log.w(TAG, "releaseSession: Ignoring empty sessionId from system service."); + if (!checkSessionIdIsValid(sessionId, "releaseSession")) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession, diff --git a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java index 68071b0b0fe3..bb00bb3b8d56 100644 --- a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java +++ b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java @@ -20,12 +20,16 @@ import android.annotation.BytesLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.app.ActivityManager; import android.hardware.tv.tuner.V1_0.Constants; import android.media.tv.tuner.Tuner; import android.media.tv.tuner.Tuner.Result; import android.media.tv.tuner.TunerUtils; import android.media.tv.tuner.filter.Filter; import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -72,9 +76,15 @@ public class DvrPlayback implements AutoCloseable { */ public static final int PLAYBACK_STATUS_FULL = Constants.PlaybackStatus.SPACE_FULL; + private static final String TAG = "TvTunerPlayback"; + private long mNativeContext; private OnPlaybackStatusChangedListener mListener; private Executor mExecutor; + private int mUserId; + private static int sInstantId = 0; + private int mSegmentId = 0; + private int mUnderflow; private native int nativeAttachFilter(Filter filter); private native int nativeDetachFilter(Filter filter); @@ -88,6 +98,9 @@ public class DvrPlayback implements AutoCloseable { private native long nativeRead(byte[] bytes, long offset, long size); private DvrPlayback() { + mUserId = ActivityManager.getCurrentUser(); + mSegmentId = (sInstantId & 0x0000ffff) << 16; + sInstantId++; } /** @hide */ @@ -98,6 +111,9 @@ public class DvrPlayback implements AutoCloseable { } private void onPlaybackStatusChanged(int status) { + if (status == PLAYBACK_STATUS_EMPTY) { + mUnderflow++; + } if (mExecutor != null && mListener != null) { mExecutor.execute(() -> mListener.onPlaybackStatusChanged(status)); } @@ -154,6 +170,13 @@ public class DvrPlayback implements AutoCloseable { */ @Result public int start() { + mSegmentId = (mSegmentId & 0xffff0000) | (((mSegmentId & 0x0000ffff) + 1) & 0x0000ffff); + mUnderflow = 0; + Log.d(TAG, "Write Stats Log for Playback."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__PLAYBACK, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STARTED, mSegmentId, 0); return nativeStartDvr(); } @@ -167,6 +190,11 @@ public class DvrPlayback implements AutoCloseable { */ @Result public int stop() { + Log.d(TAG, "Write Stats Log for Playback."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__PLAYBACK, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STOPPED, mSegmentId, mUnderflow); return nativeStopDvr(); } diff --git a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java index 198bd0f4e78e..887116725961 100644 --- a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java +++ b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java @@ -19,14 +19,19 @@ package android.media.tv.tuner.dvr; import android.annotation.BytesLong; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.app.ActivityManager; import android.media.tv.tuner.Tuner; import android.media.tv.tuner.Tuner.Result; import android.media.tv.tuner.TunerUtils; import android.media.tv.tuner.filter.Filter; import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.internal.util.FrameworkStatsLog; import java.util.concurrent.Executor; + /** * Digital Video Record (DVR) recorder class which provides record control on Demux's output buffer. * @@ -34,9 +39,14 @@ import java.util.concurrent.Executor; */ @SystemApi public class DvrRecorder implements AutoCloseable { + private static final String TAG = "TvTunerRecord"; private long mNativeContext; private OnRecordStatusChangedListener mListener; private Executor mExecutor; + private int mUserId; + private static int sInstantId = 0; + private int mSegmentId = 0; + private int mOverflow; private native int nativeAttachFilter(Filter filter); private native int nativeDetachFilter(Filter filter); @@ -50,6 +60,9 @@ public class DvrRecorder implements AutoCloseable { private native long nativeWrite(byte[] bytes, long offset, long size); private DvrRecorder() { + mUserId = ActivityManager.getCurrentUser(); + mSegmentId = (sInstantId & 0x0000ffff) << 16; + sInstantId++; } /** @hide */ @@ -60,6 +73,9 @@ public class DvrRecorder implements AutoCloseable { } private void onRecordStatusChanged(int status) { + if (status == Filter.STATUS_OVERFLOW) { + mOverflow++; + } if (mExecutor != null && mListener != null) { mExecutor.execute(() -> mListener.onRecordStatusChanged(status)); } @@ -112,6 +128,13 @@ public class DvrRecorder implements AutoCloseable { */ @Result public int start() { + mSegmentId = (mSegmentId & 0xffff0000) | (((mSegmentId & 0x0000ffff) + 1) & 0x0000ffff); + mOverflow = 0; + Log.d(TAG, "Write Stats Log for Record."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STARTED, mSegmentId, 0); return nativeStartDvr(); } @@ -124,6 +147,11 @@ public class DvrRecorder implements AutoCloseable { */ @Result public int stop() { + Log.d(TAG, "Write Stats Log for Playback."); + FrameworkStatsLog + .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD, + FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STOPPED, mSegmentId, mOverflow); return nativeStopDvr(); } diff --git a/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml b/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml index fa5f9bdfe07b..2c9aaa5e9f95 100644 --- a/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml +++ b/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="search_menu" msgid="1914043873178389845">"جستجوی تنظیمات"</string> + <string name="search_menu" msgid="1914043873178389845">"تنظیمات جستجو"</string> </resources> diff --git a/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml b/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml index 111cf5a15dba..14b7b2f62eee 100644 --- a/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml +++ b/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="search_menu" msgid="1914043873178389845">"Mga setting ng paghahanap"</string> + <string name="search_menu" msgid="1914043873178389845">"Maghanap sa mga setting"</string> </resources> diff --git a/packages/SettingsLib/res/values-bn/arrays.xml b/packages/SettingsLib/res/values-bn/arrays.xml index a131a3b1ad91..b19cde4f2778 100644 --- a/packages/SettingsLib/res/values-bn/arrays.xml +++ b/packages/SettingsLib/res/values-bn/arrays.xml @@ -40,7 +40,7 @@ <item msgid="8339720953594087771">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> এর সাথে কানেক্ট হচ্ছে…"</item> <item msgid="3028983857109369308">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> দিয়ে যাচাইকরণ করা হচ্ছে..."</item> <item msgid="4287401332778341890">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> থেকে আইপি অ্যাড্রেস জানা হচ্ছে…"</item> - <item msgid="1043944043827424501">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> তে কানেক্ট হয়েছে"</item> + <item msgid="1043944043827424501">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g>-এ কানেক্ট হয়েছে"</item> <item msgid="7445993821842009653">"স্থগিত করা হয়েছে"</item> <item msgid="1175040558087735707">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> থেকে ডিসকানেক্ট হচ্ছে…"</item> <item msgid="699832486578171722">"ডিসকানেক্ট করা হয়েছে"</item> diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml index 8db0b7e9dd28..2644cb987e3a 100644 --- a/packages/SettingsLib/res/values-el/strings.xml +++ b/packages/SettingsLib/res/values-el/strings.xml @@ -194,7 +194,7 @@ <item msgid="581904787661470707">"Ταχύτατη"</item> </string-array> <string name="choose_profile" msgid="343803890897657450">"Επιλογή προφίλ"</string> - <string name="category_personal" msgid="6236798763159385225">"Προσωπικός"</string> + <string name="category_personal" msgid="6236798763159385225">"Προσωπικό"</string> <string name="category_work" msgid="4014193632325996115">"Εργασίας"</string> <string name="development_settings_title" msgid="140296922921597393">"Επιλογές για προγραμματιστές"</string> <string name="development_settings_enable" msgid="4285094651288242183">"Ενεργοποίηση επιλογών για προγραμματιστές"</string> diff --git a/packages/SettingsLib/res/values-in/arrays.xml b/packages/SettingsLib/res/values-in/arrays.xml index e73febcb1aa4..d20bf38024ab 100644 --- a/packages/SettingsLib/res/values-in/arrays.xml +++ b/packages/SettingsLib/res/values-in/arrays.xml @@ -40,7 +40,7 @@ <item msgid="8339720953594087771">"Menyambung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item> <item msgid="3028983857109369308">"Mengautentikasi dengan <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item> <item msgid="4287401332778341890">"Mendapatkan alamat IP dari <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item> - <item msgid="1043944043827424501">"Tersambung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>"</item> + <item msgid="1043944043827424501">"Terhubung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>"</item> <item msgid="7445993821842009653">"Ditangguhkan"</item> <item msgid="1175040558087735707">"Diputus dari <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item> <item msgid="699832486578171722">"Sambungan terputus"</item> diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index a6202956efa5..38eeda245616 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -19,10 +19,13 @@ package com.android.settingslib.applications; import android.app.Application; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.hardware.usb.IUsbManager; +import android.net.Uri; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; @@ -44,6 +47,15 @@ public class AppUtils { */ private static InstantAppDataProvider sInstantAppDataProvider = null; + private static final Intent sBrowserIntent; + + static { + sBrowserIntent = new Intent() + .setAction(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.parse("http:")); + } + public static CharSequence getLaunchByDefaultSummary(ApplicationsState.AppEntry appEntry, IUsbManager usbManager, PackageManager pm, Context context) { String packageName = appEntry.info.packageName; @@ -153,4 +165,22 @@ public class AppUtils { return com.android.settingslib.utils.applications.AppUtils.getAppContentDescription(context, packageName, userId); } + + /** + * Returns a boolean indicating whether a given package is a browser app. + * + * An app is a "browser" if it has an activity resolution that wound up + * marked with the 'handleAllWebDataURI' flag. + */ + public static boolean isBrowserApp(Context context, String packageName, int userId) { + sBrowserIntent.setPackage(packageName); + final List<ResolveInfo> list = context.getPackageManager().queryIntentActivitiesAsUser( + sBrowserIntent, PackageManager.MATCH_ALL, userId); + for (ResolveInfo info : list) { + if (info.activityInfo != null && info.handleAllWebDataURI) { + return true; + } + } + return false; + } } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index fa87b62bd73a..eb7ad72eb1ba 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -316,7 +316,6 @@ public class SettingsBackupTest { Settings.Global.KERNEL_CPU_THREAD_READER, Settings.Global.LANG_ID_UPDATE_CONTENT_URL, Settings.Global.LANG_ID_UPDATE_METADATA_URL, - Settings.Global.LAST_ACTIVE_USER_ID, Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS, Settings.Global.LOCATION_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS, Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST, diff --git a/packages/SystemUI/res/drawable/dismiss_circle_background.xml b/packages/SystemUI/res/drawable/dismiss_circle_background.xml index e311c520d3d6..7809c8398c2d 100644 --- a/packages/SystemUI/res/drawable/dismiss_circle_background.xml +++ b/packages/SystemUI/res/drawable/dismiss_circle_background.xml @@ -21,8 +21,8 @@ <stroke android:width="1dp" - android:color="#66FFFFFF" /> + android:color="#AAFFFFFF" /> - <solid android:color="#B3000000" /> + <solid android:color="#77000000" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml b/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml new file mode 100644 index 000000000000..8f7fb1011cf4 --- /dev/null +++ b/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <gradient + android:angle="270" + android:startColor="#00000000" + android:endColor="#77000000" + android:type="linear" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml b/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml new file mode 100644 index 000000000000..6a0695e817c7 --- /dev/null +++ b/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<transition xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@color/transparent" /> + <item android:drawable="@drawable/floating_dismiss_gradient" /> +</transition>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/dismiss_target_x.xml b/packages/SystemUI/res/drawable/ic_music_note.xml index 3672efffe139..30959a870a02 100644 --- a/packages/SystemUI/res/drawable/dismiss_target_x.xml +++ b/packages/SystemUI/res/drawable/ic_music_note.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2020 The Android Open Source Project ~ @@ -14,15 +13,12 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - -<!-- 'X' icon. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24.0dp" - android:height="24.0dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> <path - android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z" - android:fillColor="#FFFFFFFF" - android:strokeColor="#FF000000"/> -</vector>
\ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/> +</vector> diff --git a/packages/SystemUI/res/layout/quick_settings_footer.xml b/packages/SystemUI/res/layout/quick_settings_footer.xml index 846c5386dd06..15f398aa52e6 100644 --- a/packages/SystemUI/res/layout/quick_settings_footer.xml +++ b/packages/SystemUI/res/layout/quick_settings_footer.xml @@ -23,7 +23,7 @@ android:paddingStart="@dimen/qs_footer_padding_start" android:paddingEnd="@dimen/qs_footer_padding_end" android:gravity="center_vertical" - android:background="?android:attr/colorPrimary" > + android:background="@android:color/transparent"> <TextView android:id="@+id/footer_text" @@ -32,7 +32,7 @@ android:gravity="start" android:layout_weight="1" android:textAppearance="@style/TextAppearance.QS.TileLabel" - android:textColor="?android:attr/textColorSecondary"/> + style="@style/qs_security_footer"/> <ImageView android:id="@+id/footer_icon" @@ -40,6 +40,6 @@ android:layout_height="@dimen/qs_footer_icon_size" android:contentDescription="@null" android:src="@drawable/ic_info_outline" - android:tint="?android:attr/textColorSecondary"/> + style="@style/qs_security_footer"/> </LinearLayout> diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml index 4fdeb6fa4a92..50261e1b2139 100644 --- a/packages/SystemUI/res/values-night/styles.xml +++ b/packages/SystemUI/res/values-night/styles.xml @@ -29,4 +29,9 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> + <style name="qs_security_footer" parent="@style/qs_theme"> + <item name="android:textColor">#B3FFFFFF</item> <!-- 70% white --> + <item name="android:tint">#FFFFFFFF</item> + </style> + </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c46460853683..73d8e9a0d8a7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -974,7 +974,9 @@ <dimen name="recents_quick_scrub_onboarding_margin_start">8dp</dimen> <!-- The height of the gradient indicating the dismiss edge when moving a PIP. --> - <dimen name="floating_dismiss_gradient_height">176dp</dimen> + <dimen name="floating_dismiss_gradient_height">250dp</dimen> + + <dimen name="floating_dismiss_bottom_margin">50dp</dimen> <!-- The bottom margin of the PIP drag to dismiss info text shown when moving a PIP. --> <dimen name="pip_dismiss_text_bottom_margin">24dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 39237ac246eb..0314fc89d33a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2781,6 +2781,8 @@ <!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] --> <string name="controls_media_close_session">Close this media session</string> + <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] --> + <string name="controls_media_resume">Resume</string> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <string name="controls_error_timeout">Inactive, check app</string> @@ -2788,7 +2790,13 @@ a retry will be attempted [CHAR LIMIT=30] --> <string name="controls_error_retryable">Error, retrying\u2026</string> <!-- Error message indicating that the control is no longer available in the application [CHAR LIMIT=30] --> - <string name="controls_error_removed">Device removed</string> + <string name="controls_error_removed">Not found</string> + <!-- Title for dialog indicating that the control is no longer available in the application [CHAR LIMIT=30] --> + <string name="controls_error_removed_title">Control is unavailable</string> + <!-- Message body for dialog indicating that the control is no longer available in the application [CHAR LIMIT=NONE] --> + <string name="controls_error_removed_message">Couldn\u2019t access <xliff:g id="device" example="Backdoor lock">%1$s</xliff:g>. Check the <xliff:g id="application" example="Google Home">%2$s</xliff:g> app to make sure the control is still available and that the app settings haven\u2019t changed.</string> + <!-- Text for button to open the corresponding application [CHAR_LIMIT=20] --> + <string name="controls_open_app">Open app</string> <!-- Error message indicating that an unspecified error occurred while getting the status [CHAR LIMIT=30] --> <string name="controls_error_generic">Can\u2019t load status</string> <!-- Error message indicating that a control action failed [CHAR_LIMIT=30] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ed36bdbe1e7e..39f78bf46028 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -387,6 +387,11 @@ <item name="android:homeAsUpIndicator">@drawable/ic_arrow_back</item> </style> + <style name="qs_security_footer" parent="@style/qs_theme"> + <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:tint">?android:attr/textColorSecondary</item> + </style> + <style name="systemui_theme_remote_input" parent="@android:style/Theme.DeviceDefault.Light"> <item name="android:colorAccent">@color/remote_input_accent</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index dfa71baddb93..8a80c4d75e84 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -47,6 +47,7 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.graphics.drawable.TransitionDrawable; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -133,6 +134,9 @@ public class BubbleStackView extends FrameLayout /** Percent to darken the bubbles when they're in the dismiss target. */ private static final float DARKEN_PERCENT = 0.3f; + /** Duration of the dismiss scrim fading in/out. */ + private static final int DISMISS_TRANSITION_DURATION_MS = 200; + /** How long to wait, in milliseconds, before hiding the flyout. */ @VisibleForTesting static final int FLYOUT_HIDE_AFTER = 5000; @@ -752,7 +756,7 @@ public class BubbleStackView extends FrameLayout final View targetView = new DismissCircleView(context); final FrameLayout.LayoutParams newParams = new FrameLayout.LayoutParams(targetSize, targetSize); - newParams.gravity = Gravity.CENTER; + newParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; targetView.setLayoutParams(newParams); mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView); @@ -761,9 +765,16 @@ public class BubbleStackView extends FrameLayout MATCH_PARENT, getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), Gravity.BOTTOM)); + + final int bottomMargin = + getResources().getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin); + mDismissTargetContainer.setPadding(0, 0, 0, bottomMargin); + mDismissTargetContainer.setClipToPadding(false); mDismissTargetContainer.setClipChildren(false); mDismissTargetContainer.addView(targetView); mDismissTargetContainer.setVisibility(View.INVISIBLE); + mDismissTargetContainer.setBackgroundResource( + R.drawable.floating_dismiss_gradient_transition); addView(mDismissTargetContainer); // Start translated down so the target springs up. @@ -1884,6 +1895,9 @@ public class BubbleStackView extends FrameLayout mDismissTargetContainer.setZ(Short.MAX_VALUE - 1); mDismissTargetContainer.setVisibility(VISIBLE); + ((TransitionDrawable) mDismissTargetContainer.getBackground()).startTransition( + DISMISS_TRANSITION_DURATION_MS); + mDismissTargetAnimator.cancel(); mDismissTargetAnimator .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring) @@ -1901,6 +1915,9 @@ public class BubbleStackView extends FrameLayout mShowingDismiss = false; + ((TransitionDrawable) mDismissTargetContainer.getBackground()).reverseTransition( + DISMISS_TRANSITION_DURATION_MS); + mDismissTargetAnimator .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(), mDismissTargetSpring) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index f07f3168d246..994557cf696b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -118,6 +118,7 @@ class ControlViewHolder( var behavior: Behavior? = null var lastAction: ControlAction? = null var isLoading = false + var visibleDialog: Dialog? = null private var lastChallengeDialog: Dialog? = null private val onDialogCancel: () -> Unit = { lastChallengeDialog = null } @@ -197,18 +198,24 @@ class ControlViewHolder( fun dismiss() { lastChallengeDialog?.dismiss() lastChallengeDialog = null + visibleDialog?.dismiss() + visibleDialog = null } fun setTransientStatus(tempStatus: String) { val previousText = status.getText() cancelUpdate = uiExecutor.executeDelayed({ - setStatusText(previousText) - updateContentDescription() + animateStatusChange(/* animated */ true, { + setStatusText(previousText, /* immediately */ true) + updateContentDescription() + }) }, UPDATE_DELAY_IN_MILLIS) - setStatusText(tempStatus) - updateContentDescription() + animateStatusChange(/* animated */ true, { + setStatusText(tempStatus, /* immediately */ true) + updateContentDescription() + }) } private fun updateContentDescription() = diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt index bf3835dba4fd..6bf189744033 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt @@ -16,7 +16,13 @@ package com.android.systemui.controls.ui +import android.app.AlertDialog +import android.app.PendingIntent +import android.content.DialogInterface +import android.content.pm.PackageManager import android.service.controls.Control +import android.view.View +import android.view.WindowManager import com.android.systemui.R @@ -31,7 +37,17 @@ class StatusBehavior : Behavior { val status = cws.control?.status ?: Control.STATUS_UNKNOWN val msg = when (status) { Control.STATUS_ERROR -> R.string.controls_error_generic - Control.STATUS_NOT_FOUND -> R.string.controls_error_removed + Control.STATUS_DISABLED -> R.string.controls_error_timeout + Control.STATUS_NOT_FOUND -> { + cvh.layout.setOnClickListener(View.OnClickListener() { + showNotFoundDialog(cvh, cws) + }) + cvh.layout.setOnLongClickListener(View.OnLongClickListener() { + showNotFoundDialog(cvh, cws) + true + }) + R.string.controls_error_removed + } else -> { cvh.isLoading = true com.android.internal.R.string.loading @@ -40,4 +56,42 @@ class StatusBehavior : Behavior { cvh.setStatusText(cvh.context.getString(msg)) cvh.applyRenderInfo(false, colorOffset) } + + private fun showNotFoundDialog(cvh: ControlViewHolder, cws: ControlWithState) { + val pm = cvh.context.getPackageManager() + val ai = pm.getApplicationInfo(cws.componentName.packageName, PackageManager.GET_META_DATA) + val appLabel = pm.getApplicationLabel(ai) + val builder = AlertDialog.Builder( + cvh.context, + android.R.style.Theme_DeviceDefault_Dialog_Alert + ).apply { + val res = cvh.context.resources + setTitle(res.getString(R.string.controls_error_removed_title)) + setMessage(res.getString( + R.string.controls_error_removed_message, cvh.title.getText(), appLabel)) + setPositiveButton( + R.string.controls_open_app, + DialogInterface.OnClickListener { dialog, _ -> + try { + cws.control?.getAppIntent()?.send() + } catch (e: PendingIntent.CanceledException) { + cvh.setTransientStatus( + cvh.context.resources.getString(R.string.controls_error_failed)) + } + dialog.dismiss() + }) + setNegativeButton( + android.R.string.cancel, + DialogInterface.OnClickListener { dialog, _ -> + dialog.cancel() + } + ) + } + cvh.visibleDialog = builder.create().apply { + getWindow().apply { + setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY) + show() + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 5595201a670f..b8c1842a1780 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -17,12 +17,8 @@ package com.android.systemui.media; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -35,7 +31,6 @@ import android.graphics.drawable.RippleDrawable; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.service.media.MediaBrowserService; import android.util.Log; import android.view.View; import android.widget.ImageButton; @@ -55,7 +50,6 @@ import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -81,7 +75,6 @@ public class MediaControlPanel { private final SeekBarViewModel mSeekBarViewModel; private SeekBarObserver mSeekBarObserver; - private final Executor mForegroundExecutor; protected final Executor mBackgroundExecutor; private final ActivityStarter mActivityStarter; @@ -91,48 +84,18 @@ public class MediaControlPanel { private MediaSession.Token mToken; private MediaController mController; private int mBackgroundColor; - protected ComponentName mServiceComponent; - private boolean mIsRegistered = false; - private String mKey; private int mAlbumArtSize; private int mAlbumArtRadius; - private int mViewWidth; - - public static final String MEDIA_PREFERENCES = "media_control_prefs"; - public static final String MEDIA_PREFERENCE_KEY = "browser_components"; - private SharedPreferences mSharedPrefs; - private boolean mCheckedForResumption = false; - private QSMediaBrowser mQSMediaBrowser; - - private final MediaController.Callback mSessionCallback = new MediaController.Callback() { - @Override - public void onSessionDestroyed() { - Log.d(TAG, "session destroyed"); - mController.unregisterCallback(mSessionCallback); - clearControls(); - } - @Override - public void onPlaybackStateChanged(PlaybackState state) { - final int s = state != null ? state.getState() : PlaybackState.STATE_NONE; - if (s == PlaybackState.STATE_NONE) { - Log.d(TAG, "playback state change will trigger resumption, state=" + state); - clearControls(); - } - } - }; /** * Initialize a new control panel * @param context - * @param foregroundExecutor foreground executor * @param backgroundExecutor background executor, used for processing artwork * @param activityStarter activity starter */ - public MediaControlPanel(Context context, Executor foregroundExecutor, - DelayableExecutor backgroundExecutor, ActivityStarter activityStarter, - MediaHostStatesManager mediaHostStatesManager) { + public MediaControlPanel(Context context, DelayableExecutor backgroundExecutor, + ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager) { mContext = context; - mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); @@ -214,45 +177,18 @@ public class MediaControlPanel { MediaSession.Token token = data.getToken(); mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { - if (mQSMediaBrowser != null) { - Log.d(TAG, "Disconnecting old media browser"); - mQSMediaBrowser.disconnect(); - mQSMediaBrowser = null; - } mToken = token; - mServiceComponent = null; - mCheckedForResumption = false; } - mController = new MediaController(mContext, mToken); + if (mToken != null) { + mController = new MediaController(mContext, mToken); + } else { + mController = null; + } ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); - // Try to find a browser service component for this app - // TODO also check for a media button receiver intended for restarting (b/154127084) - // Only check if we haven't tried yet or the session token changed - final String pkgName = data.getPackageName(); - if (mServiceComponent == null && !mCheckedForResumption) { - Log.d(TAG, "Checking for service component"); - PackageManager pm = mContext.getPackageManager(); - Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); - List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0); - // TODO: look into this resumption - if (resumeInfo != null) { - for (ResolveInfo inf : resumeInfo) { - if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { - mBackgroundExecutor.execute(() -> - tryUpdateResumptionList(inf.getComponentInfo().getComponentName())); - break; - } - } - } - mCheckedForResumption = true; - } - - mController.registerCallback(mSessionCallback); - mViewHolder.getPlayer().setBackgroundTintList( ColorStateList.valueOf(mBackgroundColor)); @@ -267,12 +203,22 @@ public class MediaControlPanel { ImageView albumView = mViewHolder.getAlbumView(); // TODO: migrate this to a view with rounded corners instead of baking the rounding // into the bitmap - Drawable artwork = createRoundedBitmap(data.getArtwork()); - albumView.setImageDrawable(artwork); + boolean hasArtwork = data.getArtwork() != null; + if (hasArtwork) { + Drawable artwork = createRoundedBitmap(data.getArtwork()); + albumView.setImageDrawable(artwork); + } + setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork); + setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork); // App icon ImageView appIcon = mViewHolder.getAppIcon(); - appIcon.setImageDrawable(data.getAppIcon()); + if (data.getAppIcon() != null) { + appIcon.setImageDrawable(data.getAppIcon()); + } else { + Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note); + appIcon.setImageDrawable(iconDrawable); + } // Song name TextView titleText = mViewHolder.getTitleText(); @@ -294,7 +240,7 @@ public class MediaControlPanel { final Intent intent = new Intent() .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - mController.getPackageName()) + data.getPackageName()) .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); mActivityStarter.startActivity(intent, false, true /* dismissShade */, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -350,15 +296,11 @@ public class MediaControlPanel { MediaAction mediaAction = actionIcons.get(i); button.setImageDrawable(mediaAction.getDrawable()); button.setContentDescription(mediaAction.getContentDescription()); - PendingIntent actionIntent = mediaAction.getIntent(); + Runnable action = mediaAction.getAction(); button.setOnClickListener(v -> { - if (actionIntent != null) { - try { - actionIntent.send(); - } catch (PendingIntent.CanceledException e) { - e.printStackTrace(); - } + if (action != null) { + action.run(); } }); boolean visibleInCompat = actionsWhenCollapsed.contains(i); @@ -444,14 +386,6 @@ public class MediaControlPanel { } /** - * Return the original notification's key - * @return The notification key - */ - public String getKey() { - return mKey; - } - - /** * Check whether this player has an attached media session. * @return whether there is a controller with a current media session. */ @@ -485,150 +419,8 @@ public class MediaControlPanel { return (state.getState() == PlaybackState.STATE_PLAYING); } - /** - * Puts controls into a resumption state if possible, or calls removePlayer if no component was - * found that could resume playback - */ - public void clearControls() { - Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage()); - if (mServiceComponent == null) { - // If we don't have a way to resume, just remove the player altogether - Log.d(TAG, "Removing unresumable controls"); - removePlayer(); - return; - } - resetButtons(); - } - - /** - * Hide the media buttons and show only a restart button - */ - protected void resetButtons() { - if (mViewHolder == null) { - return; - } - // Hide all the old buttons - - ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); - ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); - for (int i = 1; i < ACTION_IDS.length; i++) { - setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); - setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); - } - - // Add a restart button - ImageButton btn = mViewHolder.getAction0(); - btn.setOnClickListener(v -> { - Log.d(TAG, "Attempting to restart session"); - if (mQSMediaBrowser != null) { - mQSMediaBrowser.disconnect(); - } - mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){ - @Override - public void onConnected() { - Log.d(TAG, "Successfully restarted"); - } - @Override - public void onError() { - Log.e(TAG, "Error restarting"); - mQSMediaBrowser.disconnect(); - mQSMediaBrowser = null; - } - }, mServiceComponent); - mQSMediaBrowser.restart(); - }); - btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); - setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */); - setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */); - - mSeekBarViewModel.clearController(); - // TODO: fix guts - // View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mViewHolder.getOptions(); - - mViewHolder.getPlayer().setOnLongClickListener(v -> { - // Replace player view with close/cancel view -// guts.setVisibility(View.GONE); - options.setVisibility(View.VISIBLE); - return true; // consumed click - }); - mMediaViewController.refreshState(); - } - private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE); set.setAlpha(actionId, visible ? 1.0f : 0.0f); } - - /** - * Verify that we can connect to the given component with a MediaBrowser, and if so, add that - * component to the list of resumption components - */ - private void tryUpdateResumptionList(ComponentName componentName) { - Log.d(TAG, "Testing if we can connect to " + componentName); - if (mQSMediaBrowser != null) { - mQSMediaBrowser.disconnect(); - } - mQSMediaBrowser = new QSMediaBrowser(mContext, - new QSMediaBrowser.Callback() { - @Override - public void onConnected() { - Log.d(TAG, "yes we can resume with " + componentName); - mServiceComponent = componentName; - updateResumptionList(componentName); - mQSMediaBrowser.disconnect(); - mQSMediaBrowser = null; - } - - @Override - public void onError() { - Log.d(TAG, "Cannot resume with " + componentName); - mServiceComponent = null; - if (!hasMediaSession()) { - // If it's not active and we can't resume, remove - removePlayer(); - } - mQSMediaBrowser.disconnect(); - mQSMediaBrowser = null; - } - }, - componentName); - mQSMediaBrowser.testConnection(); - } - - /** - * Add the component to the saved list of media browser services, checking for duplicates and - * removing older components that exceed the maximum limit - * @param componentName - */ - private synchronized void updateResumptionList(ComponentName componentName) { - // Add to front of saved list - if (mSharedPrefs == null) { - mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0); - } - String componentString = componentName.flattenToString(); - String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null); - if (listString == null) { - listString = componentString; - } else { - String[] components = listString.split(QSMediaBrowser.DELIMITER); - StringBuilder updated = new StringBuilder(componentString); - int nBrowsers = 1; - for (int i = 0; i < components.length - && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { - if (componentString.equals(components[i])) { - continue; - } - updated.append(QSMediaBrowser.DELIMITER).append(components[i]); - nBrowsers++; - } - listString = updated.toString(); - } - mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply(); - } - - /** - * Called when a player can't be resumed to give it an opportunity to hide or remove itself - */ - protected void removePlayer() { } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index a94f6a87d58a..5d28178a3b1b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -32,17 +32,19 @@ data class MediaData( val artwork: Icon?, val actions: List<MediaAction>, val actionsToShowInCompact: List<Int>, - val packageName: String?, + val packageName: String, val token: MediaSession.Token?, val clickIntent: PendingIntent?, val device: MediaDeviceData?, - val notificationKey: String = "INVALID" + var resumeAction: Runnable?, + val notificationKey: String = "INVALID", + var hasCheckedForResume: Boolean = false ) /** State of a media action. */ data class MediaAction( val drawable: Drawable?, - val intent: PendingIntent?, + val action: Runnable?, val contentDescription: CharSequence? ) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index 67cf21ae10b9..11cbc482459a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -32,9 +32,15 @@ class MediaDataCombineLatest @Inject constructor( init { dataSource.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, data: MediaData) { - entries[key] = data to entries[key]?.second - update(key) + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (oldKey != null && !oldKey.equals(key)) { + val s = entries[oldKey]?.second + entries[key] = data to entries[oldKey]?.second + entries.remove(oldKey) + } else { + entries[key] = data to entries[key]?.second + } + update(key, oldKey) } override fun onMediaDataRemoved(key: String) { remove(key) @@ -43,7 +49,7 @@ class MediaDataCombineLatest @Inject constructor( deviceSource.addListener(object : MediaDeviceManager.Listener { override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) { entries[key] = entries[key]?.first to data - update(key) + update(key, key) } override fun onKeyRemoved(key: String) { remove(key) @@ -61,13 +67,13 @@ class MediaDataCombineLatest @Inject constructor( */ fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) - private fun update(key: String) { + private fun update(key: String, oldKey: String?) { val (entry, device) = entries[key] ?: null to null if (entry != null && device != null) { val data = entry.copy(device = device) val listenersCopy = listeners.toSet() listenersCopy.forEach { - it.onMediaDataLoaded(key, data) + it.onMediaDataLoaded(key, oldKey, data) } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index d94985703083..094c5bef3c18 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -17,6 +17,7 @@ package com.android.systemui.media import android.app.Notification +import android.app.PendingIntent import android.content.ContentResolver import android.content.Context import android.graphics.Bitmap @@ -25,6 +26,7 @@ import android.graphics.Color import android.graphics.ImageDecoder import android.graphics.drawable.Drawable import android.graphics.drawable.Icon +import android.media.MediaDescription import android.media.MediaMetadata import android.media.session.MediaSession import android.net.Uri @@ -32,8 +34,10 @@ import android.service.notification.StatusBarNotification import android.text.TextUtils import android.util.Log import com.android.internal.graphics.ColorUtils +import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.MediaNotificationProcessor import com.android.systemui.statusbar.notification.NotificationEntryManager import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON @@ -58,7 +62,7 @@ private const val LUMINOSITY_THRESHOLD = 0.05f private const val SATURATION_MULTIPLIER = 0.8f private val LOADING = MediaData(false, 0, null, null, null, null, null, - emptyList(), emptyList(), null, null, null, null) + emptyList(), emptyList(), "INVALID", null, null, null, null) fun isMediaNotification(sbn: StatusBarNotification): Boolean { if (!sbn.notification.hasMediaSession()) { @@ -81,34 +85,92 @@ class MediaDataManager @Inject constructor( private val mediaControllerFactory: MediaControllerFactory, private val mediaTimeoutListener: MediaTimeoutListener, private val notificationEntryManager: NotificationEntryManager, + private val mediaResumeListener: MediaResumeListener, @Background private val backgroundExecutor: Executor, @Main private val foregroundExecutor: Executor ) { private val listeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + private val useMediaResumption: Boolean = Utils.useMediaResumption(context) init { mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> setTimedOut(token, timedOut) } addListener(mediaTimeoutListener) + + if (useMediaResumption) { + mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription, + resumeAction: Runnable, token: MediaSession.Token, appName: String, + appIntent: PendingIntent, packageName: String -> + addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName) + } + mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? -> + mediaEntries.get(key)?.resumeAction = action + mediaEntries.get(key)?.hasCheckedForResume = true + } + addListener(mediaResumeListener) + } } fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) { Assert.isMainThread() - if (!mediaEntries.containsKey(key)) { - mediaEntries.put(key, LOADING) + val oldKey = findExistingEntry(key, sbn.packageName) + if (oldKey == null) { + val temp = LOADING.copy(packageName = sbn.packageName) + mediaEntries.put(key, temp) + } else if (oldKey != key) { + // Move to new key + val oldData = mediaEntries.remove(oldKey)!! + mediaEntries.put(key, oldData) } - loadMediaData(key, sbn) + loadMediaData(key, sbn, oldKey) } else { onNotificationRemoved(key) } } - private fun loadMediaData(key: String, sbn: StatusBarNotification) { + private fun addResumptionControls( + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + // Resume controls don't have a notification key, so store by package name instead + if (!mediaEntries.containsKey(packageName)) { + val resumeData = LOADING.copy(packageName = packageName, resumeAction = action) + mediaEntries.put(packageName, resumeData) + } backgroundExecutor.execute { - loadMediaDataInBg(key, sbn) + loadMediaDataInBg(desc, action, token, appName, appIntent, packageName) + } + } + + /** + * Check if there is an existing entry that matches the key or package name. + * Returns the key that matches, or null if not found. + */ + private fun findExistingEntry(key: String, packageName: String): String? { + if (mediaEntries.containsKey(key)) { + return key + } + // Check if we already had a resume player + if (mediaEntries.containsKey(packageName)) { + return packageName + } + return null + } + + private fun loadMediaData( + key: String, + sbn: StatusBarNotification, + oldKey: String? + ) { + backgroundExecutor.execute { + loadMediaDataInBg(key, sbn, oldKey) } } @@ -132,7 +194,50 @@ class MediaDataManager @Inject constructor( } } - private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) { + private fun loadMediaDataInBg( + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + if (resumeAction == null) { + Log.e(TAG, "Resume action cannot be null") + return + } + + if (TextUtils.isEmpty(desc.title)) { + Log.e(TAG, "Description incomplete") + return + } + + Log.d(TAG, "adding track from browser: $desc") + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = loadBitmapFromUri(desc.iconUri!!) + } + val artworkIcon = if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val mediaAction = getResumeMediaAction(resumeAction) + foregroundExecutor.execute { + onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName, + null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), + packageName, token, appIntent, null, resumeAction, packageName)) + } + } + + private fun loadMediaDataInBg( + key: String, + sbn: StatusBarNotification, + oldKey: String? + ) { val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) as MediaSession.Token? val metadata = mediaControllerFactory.create(token).metadata @@ -234,16 +339,23 @@ class MediaDataManager @Inject constructor( } val mediaAction = MediaAction( action.getIcon().loadDrawable(packageContext), - action.actionIntent, + Runnable { + try { + action.actionIntent.send() + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + } + }, action.title) actionIcons.add(mediaAction) } } + val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction foregroundExecutor.execute { - onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song, - artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, - notif.contentIntent, null, key)) + onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist, + song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, + notif.contentIntent, null, resumeAction, key)) } } @@ -257,7 +369,7 @@ class MediaDataManager @Inject constructor( val albumArt = loadBitmapFromUri(Uri.parse(uriString)) if (albumArt != null) { Log.d(TAG, "loaded art from $uri") - break + return albumArt } } } @@ -283,27 +395,52 @@ class MediaDataManager @Inject constructor( val source = ImageDecoder.createSource(context.getContentResolver(), uri) return try { - ImageDecoder.decodeBitmap(source) + ImageDecoder.decodeBitmap(source) { + decoder, info, source -> decoder.isMutableRequired = true + } } catch (e: IOException) { e.printStackTrace() null } } - fun onMediaDataLoaded(key: String, data: MediaData) { + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + context.getDrawable(R.drawable.lb_ic_play), + action, + context.getString(R.string.controls_media_resume) + ) + } + + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { Assert.isMainThread() if (mediaEntries.containsKey(key)) { // Otherwise this was removed already mediaEntries.put(key, data) val listenersCopy = listeners.toSet() listenersCopy.forEach { - it.onMediaDataLoaded(key, data) + it.onMediaDataLoaded(key, oldKey, data) } } } fun onNotificationRemoved(key: String) { Assert.isMainThread() + if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) { + Log.d(TAG, "Not removing $key because resumable") + // Move to resume key aka package name + val data = mediaEntries.remove(key)!! + val resumeAction = getResumeMediaAction(data.resumeAction!!) + val updated = data.copy(token = null, actions = listOf(resumeAction), + actionsToShowInCompact = listOf(0)) + mediaEntries.put(data.packageName, updated) + // Notify listeners of "new" controls + val listenersCopy = listeners.toSet() + listenersCopy.forEach { + it.onMediaDataLoaded(data.packageName, key, updated) + } + return + } val removed = mediaEntries.remove(key) if (removed != null) { val listenersCopy = listeners.toSet() @@ -316,19 +453,32 @@ class MediaDataManager @Inject constructor( /** * Are there any media notifications active? */ - fun hasActiveMedia() = mediaEntries.isNotEmpty() + fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) }) - fun hasAnyMedia(): Boolean { - // TODO: implement this when we implemented resumption - return hasActiveMedia() + fun isActive(data: MediaData): Boolean { + if (data.token == null) { + return false + } + val controller = mediaControllerFactory.create(data.token) + val state = controller?.playbackState?.state + return state != null && NotificationMediaManager.isActiveState(state) } + /** + * Are there any media entries, including resume controls? + */ + fun hasAnyMedia() = mediaEntries.isNotEmpty() + interface Listener { /** - * Called whenever there's new MediaData Loaded for the consumption in views + * Called whenever there's new MediaData Loaded for the consumption in views. + * + * oldKey is provided to check whether the view has changed keys, which can happen when a + * player has gone from resume state (key is package name) to active state (key is + * notification key) or vice versa. */ - fun onMediaDataLoaded(key: String, data: MediaData) {} + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {} /** * Called whenever a previously existing Media notification was removed diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index 552fea63a278..2f521ea39242 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -16,11 +16,8 @@ package com.android.systemui.media -import android.app.Notification import android.content.Context -import android.service.notification.StatusBarNotification import android.media.MediaRouter2Manager -import android.media.session.MediaSession import android.media.session.MediaController import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice @@ -38,11 +35,16 @@ class MediaDeviceManager @Inject constructor( private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, private val featureFlag: MediaFeatureFlag, - @Main private val fgExecutor: Executor -) { + @Main private val fgExecutor: Executor, + private val mediaDataManager: MediaDataManager +) : MediaDataManager.Listener { private val listeners: MutableSet<Listener> = mutableSetOf() private val entries: MutableMap<String, Token> = mutableMapOf() + init { + mediaDataManager.addListener(this) + } + /** * Add a listener for changes to the media route (ie. device). */ @@ -53,23 +55,25 @@ class MediaDeviceManager @Inject constructor( */ fun removeListener(listener: Listener) = listeners.remove(listener) - fun onNotificationAdded(key: String, sbn: StatusBarNotification) { - if (featureFlag.enabled && isMediaNotification(sbn)) { + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (featureFlag.enabled) { + if (oldKey != null && oldKey != key) { + val oldToken = entries.remove(oldKey) + oldToken?.stop() + } var tok = entries[key] - if (tok == null) { - val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) - as MediaSession.Token? - val controller = MediaController(context, token) - tok = Token(key, controller, localMediaManagerFactory.create(sbn.packageName)) + if (tok == null && data.token != null) { + val controller = MediaController(context, data.token!!) + tok = Token(key, controller, localMediaManagerFactory.create(data.packageName)) entries[key] = tok tok.start() } } else { - onNotificationRemoved(key) + onMediaDataRemoved(key) } } - fun onNotificationRemoved(key: String) { + override fun onMediaDataRemoved(key: String) { val token = entries.remove(key) token?.stop() token?.let { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index e904e935b0e0..2bd8c0cbeab2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -50,7 +50,7 @@ class MediaHost @Inject constructor( } private val listener = object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, data: MediaData) { + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { updateViewVisibility() } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt new file mode 100644 index 000000000000..6bbe0d1651dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2020 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.systemui.media + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.media.MediaDescription +import android.media.session.MediaController +import android.media.session.MediaSession +import android.os.UserHandle +import android.service.media.MediaBrowserService +import android.util.Log +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.Utils +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "MediaResumeListener" + +private const val MEDIA_PREFERENCES = "media_control_prefs" +private const val MEDIA_PREFERENCE_KEY = "browser_components" + +@Singleton +class MediaResumeListener @Inject constructor( + private val context: Context, + private val broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundExecutor: Executor +) : MediaDataManager.Listener { + + private val useMediaResumption: Boolean = Utils.useMediaResumption(context) + private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue() + + lateinit var addTrackToResumeCallback: ( + MediaDescription, + Runnable, + MediaSession.Token, + String, + PendingIntent, + String + ) -> Unit + lateinit var resumeComponentFoundCallback: (String, Runnable?) -> Unit + + private var mediaBrowser: ResumeMediaBrowser? = null + + private val unlockReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Intent.ACTION_USER_UNLOCKED == intent.action) { + loadMediaResumptionControls() + } + } + } + + private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() { + override fun addTrack( + desc: MediaDescription, + component: ComponentName, + browser: ResumeMediaBrowser + ) { + val token = browser.token + val appIntent = browser.appIntent + val pm = context.getPackageManager() + var appName: CharSequence = component.packageName + val resumeAction = getResumeAction(component) + try { + appName = pm.getApplicationLabel( + pm.getApplicationInfo(component.packageName, 0)) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Error getting package information", e) + } + + Log.d(TAG, "Adding resume controls $desc") + addTrackToResumeCallback(desc, resumeAction, token, appName.toString(), appIntent, + component.packageName) + } + } + + init { + if (useMediaResumption) { + val unlockFilter = IntentFilter() + unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) + broadcastDispatcher.registerReceiver(unlockReceiver, unlockFilter, null, UserHandle.ALL) + loadSavedComponents() + } + } + + private fun loadSavedComponents() { + val userContext = context.createContextAsUser(context.getUser(), 0) + val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) + val listString = prefs.getString(MEDIA_PREFERENCE_KEY, null) + val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex()) + ?.dropLastWhile { it.isEmpty() } + components?.forEach { + val info = it.split("/") + val packageName = info[0] + val className = info[1] + val component = ComponentName(packageName, className) + resumeComponents.add(component) + } + Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") + } + + /** + * Load controls for resuming media, if available + */ + private fun loadMediaResumptionControls() { + if (!useMediaResumption) { + return + } + + resumeComponents.forEach { + val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) + browser.findRecentMedia() + } + broadcastDispatcher.unregisterReceiver(unlockReceiver) // only need to load once + } + + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (useMediaResumption) { + // If this had been started from a resume state, disconnect now that it's live + mediaBrowser?.disconnect() + // If we don't have a resume action, check if we haven't already + if (data.resumeAction == null && !data.hasCheckedForResume) { + // TODO also check for a media button receiver intended for restarting (b/154127084) + Log.d(TAG, "Checking for service component for " + data.packageName) + val pm = context.packageManager + val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) + val resumeInfo = pm.queryIntentServices(serviceIntent, 0) + + val inf = resumeInfo?.filter { + it.serviceInfo.packageName == data.packageName + } + if (inf != null && inf.size > 0) { + backgroundExecutor.execute { + tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) + } + } else { + // No service found + resumeComponentFoundCallback(key, null) + } + } + } + } + + /** + * Verify that we can connect to the given component with a MediaBrowser, and if so, add that + * component to the list of resumption components + */ + private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { + Log.d(TAG, "Testing if we can connect to $componentName") + mediaBrowser?.disconnect() + mediaBrowser = ResumeMediaBrowser(context, + object : ResumeMediaBrowser.Callback() { + override fun onConnected() { + Log.d(TAG, "yes we can resume with $componentName") + resumeComponentFoundCallback(key, getResumeAction(componentName)) + updateResumptionList(componentName) + mediaBrowser?.disconnect() + mediaBrowser = null + } + + override fun onError() { + Log.e(TAG, "Cannot resume with $componentName") + resumeComponentFoundCallback(key, null) + mediaBrowser?.disconnect() + mediaBrowser = null + } + }, + componentName) + mediaBrowser?.testConnection() + } + + /** + * Add the component to the saved list of media browser services, checking for duplicates and + * removing older components that exceed the maximum limit + * @param componentName + */ + private fun updateResumptionList(componentName: ComponentName) { + // Remove if exists + resumeComponents.remove(componentName) + // Insert at front of queue + resumeComponents.add(componentName) + // Remove old components if over the limit + if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + resumeComponents.remove() + } + + // Save changes + val sb = StringBuilder() + resumeComponents.forEach { + sb.append(it.flattenToString()) + sb.append(ResumeMediaBrowser.DELIMITER) + } + val userContext = context.createContextAsUser(context.getUser(), 0) + val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) + prefs.edit().putString(MEDIA_PREFERENCE_KEY, sb.toString()).apply() + } + + /** + * Get a runnable which will resume media playback + */ + private fun getResumeAction(componentName: ComponentName): Runnable { + return Runnable { + mediaBrowser?.disconnect() + mediaBrowser = ResumeMediaBrowser(context, + object : ResumeMediaBrowser.Callback() { + override fun onConnected() { + if (mediaBrowser?.token == null) { + Log.e(TAG, "Error after connect") + mediaBrowser?.disconnect() + mediaBrowser = null + return + } + Log.d(TAG, "Connected for restart $componentName") + val controller = MediaController(context, mediaBrowser!!.token) + val controls = controller.transportControls + controls.prepare() + controls.play() + } + + override fun onError() { + Log.e(TAG, "Resume failed for $componentName") + mediaBrowser?.disconnect() + mediaBrowser = null + } + }, + componentName) + mediaBrowser?.restart() + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 92a1ab1b1871..359c2f5e297c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -45,7 +45,7 @@ class MediaTimeoutListener @Inject constructor( lateinit var timeoutCallback: (String, Boolean) -> Unit - override fun onMediaDataLoaded(key: String, data: MediaData) { + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { if (mediaListeners.containsKey(key)) { return } @@ -67,15 +67,20 @@ class MediaTimeoutListener @Inject constructor( var timedOut = false - private val mediaController = mediaControllerFactory.create(data.token) + // Resume controls may have null token + private val mediaController = if (data.token != null) { + mediaControllerFactory.create(data.token) + } else { + null + } private var cancellation: Runnable? = null init { - mediaController.registerCallback(this) + mediaController?.registerCallback(this) } fun destroy() { - mediaController.unregisterCallback(this) + mediaController?.unregisterCallback(this) } override fun onPlaybackStateChanged(state: PlaybackState?) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index 8ab30c75c7eb..3557b04a57bc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -12,14 +12,12 @@ import android.widget.LinearLayout import androidx.core.view.GestureDetectorCompat import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.PageIndicator import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor -import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton @@ -32,7 +30,6 @@ private const val FLING_SLOP = 1000000 @Singleton class MediaViewManager @Inject constructor( private val context: Context, - @Main private val foregroundExecutor: Executor, @Background private val backgroundExecutor: DelayableExecutor, private val visualStabilityManager: VisualStabilityManager, private val activityStarter: ActivityStarter, @@ -147,8 +144,8 @@ class MediaViewManager @Inject constructor( visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, true /* persistent */) mediaManager.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, data: MediaData) { - updateView(key, data) + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + updateView(key, oldKey, data) updatePlayerVisibilities() mediaCarousel.requiresRemeasuring = true } @@ -259,11 +256,17 @@ class MediaViewManager @Inject constructor( } } - private fun updateView(key: String, data: MediaData) { + private fun updateView(key: String, oldKey: String?, data: MediaData) { + // If the key was changed, update entry + val oldData = mediaPlayers[oldKey] + if (oldData != null) { + val oldData = mediaPlayers.remove(oldKey) + mediaPlayers.put(key, oldData!!) + } var existingPlayer = mediaPlayers[key] if (existingPlayer == null) { - existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor, - activityStarter, mediaHostStatesManager) + existingPlayer = MediaControlPanel(context, backgroundExecutor, activityStarter, + mediaHostStatesManager) existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) mediaPlayers[key] = existingPlayer diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java index a5b73dcbd289..1e9a30364607 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs; +package com.android.systemui.media; import android.app.PendingIntent; import android.content.ComponentName; @@ -27,14 +27,17 @@ import android.media.session.MediaController; import android.media.session.MediaSession; import android.os.Bundle; import android.service.media.MediaBrowserService; +import android.text.TextUtils; import android.util.Log; +import com.android.systemui.util.Utils; + import java.util.List; /** - * Media browser for managing resumption in QS media controls + * Media browser for managing resumption in media controls */ -public class QSMediaBrowser { +public class ResumeMediaBrowser { /** Maximum number of controls to show on boot */ public static final int MAX_RESUMPTION_CONTROLS = 5; @@ -42,7 +45,8 @@ public class QSMediaBrowser { /** Delimiter for saved component names */ public static final String DELIMITER = ":"; - private static final String TAG = "QSMediaBrowser"; + private static final String TAG = "ResumeMediaBrowser"; + private boolean mIsEnabled = false; private final Context mContext; private final Callback mCallback; private MediaBrowser mMediaBrowser; @@ -54,21 +58,25 @@ public class QSMediaBrowser { * @param callback used to report media items found * @param componentName Component name of the MediaBrowserService this browser will connect to */ - public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) { + public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) { + mIsEnabled = Utils.useMediaResumption(context); mContext = context; mCallback = callback; mComponentName = componentName; } /** - * Connects to the MediaBrowserService and looks for valid media. If a media item is returned - * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription. - * QSMediaBrowser.Callback#onConnected and QSMediaBrowser.Callback#onError will also be called - * when the initial connection is successful, or an error occurs. Note that it is possible for - * the service to connect but for no playable tracks to be found later. - * QSMediaBrowser#disconnect will be called automatically with this function. + * Connects to the MediaBrowserService and looks for valid media. If a media item is returned, + * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription. + * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be + * called when the initial connection is successful, or an error occurs. + * Note that it is possible for the service to connect but for no playable tracks to be found. + * ResumeMediaBrowser#disconnect will be called automatically with this function. */ public void findRecentMedia() { + if (!mIsEnabled) { + return; + } Log.d(TAG, "Connecting to " + mComponentName); disconnect(); Bundle rootHints = new Bundle(); @@ -86,7 +94,7 @@ public class QSMediaBrowser { public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) { if (children.size() == 0) { - Log.e(TAG, "No children found for " + mComponentName); + Log.d(TAG, "No children found for " + mComponentName); return; } // We ask apps to return a playable item as the first child when sending @@ -94,23 +102,24 @@ public class QSMediaBrowser { MediaBrowser.MediaItem child = children.get(0); MediaDescription desc = child.getDescription(); if (child.isPlayable()) { - mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this); + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), + ResumeMediaBrowser.this); } else { - Log.e(TAG, "Child found but not playable for " + mComponentName); + Log.d(TAG, "Child found but not playable for " + mComponentName); } disconnect(); } @Override public void onError(String parentId) { - Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId); + Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId); mCallback.onError(); disconnect(); } @Override public void onError(String parentId, Bundle options) { - Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId + Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId + ", options: " + options); mCallback.onError(); disconnect(); @@ -149,7 +158,7 @@ public class QSMediaBrowser { */ @Override public void onConnectionFailed() { - Log.e(TAG, "Connection failed for " + mComponentName); + Log.d(TAG, "Connection failed for " + mComponentName); mCallback.onError(); disconnect(); } @@ -167,11 +176,15 @@ public class QSMediaBrowser { } /** - * Connects to the MediaBrowserService and starts playback. QSMediaBrowser.Callback#onError or - * QSMediaBrowser.Callback#onConnected will be called depending on whether it was successful. - * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed. + * Connects to the MediaBrowserService and starts playback. + * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called + * depending on whether it was successful. + * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void restart() { + if (!mIsEnabled) { + return; + } disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); @@ -224,18 +237,21 @@ public class QSMediaBrowser { /** * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser. - * QSMediaBrowser.Callback#onError or QSMediaBrowser.Callback#onConnected will be called + * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called * depending on whether it was successful. - * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed. + * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void testConnection() { + if (!mIsEnabled) { + return; + } disconnect(); final MediaBrowser.ConnectionCallback connectionCallback = new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { Log.d(TAG, "connected"); - if (mMediaBrowser.getRoot() == null) { + if (TextUtils.isEmpty(mMediaBrowser.getRoot())) { mCallback.onError(); } else { mCallback.onConnected(); @@ -264,7 +280,7 @@ public class QSMediaBrowser { } /** - * Interface to handle results from QSMediaBrowser + * Interface to handle results from ResumeMediaBrowser */ public static class Callback { /** @@ -286,7 +302,7 @@ public class QSMediaBrowser { * @param browser reference to the browser */ public void addTrack(MediaDescription track, ComponentName component, - QSMediaBrowser browser) { + ResumeMediaBrowser browser) { } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt index 06821cd615a5..75ad06962afe 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt @@ -91,8 +91,14 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) { playbackState = state if (shouldPollPlaybackPosition()) { checkPlaybackPosition() + } else if (PlaybackState.STATE_NONE.equals(playbackState)) { + clearController() } } + + override fun onSessionDestroyed() { + clearController() + } } /** Listening state (QS open or closed) is used to control polling of progress. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java index 0d614497190f..2c76d70fb3cc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java @@ -45,9 +45,6 @@ public class PageIndicator extends ViewGroup { } public void setNumPages(int numPages) { - if (numPages == getChildCount()) { - return; - } TypedArray array = getContext().obtainStyledAttributes( new int[]{android.R.attr.colorControlActivated}); int color = array.getColor(0, 0); @@ -55,12 +52,12 @@ public class PageIndicator extends ViewGroup { setNumPages(numPages, color); } - /** Oveload of setNumPages that allows the indicator color to be specified.*/ + /** Overload of setNumPages that allows the indicator color to be specified.*/ public void setNumPages(int numPages, int color) { + setVisibility(numPages > 1 ? View.VISIBLE : View.GONE); if (numPages == getChildCount()) { return; } - setVisibility(numPages > 1 ? View.VISIBLE : View.GONE); if (mAnimating) { Log.w(TAG, "setNumPages during animation"); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 191d4757258d..94b4cee92965 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -41,7 +41,6 @@ import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; -import com.android.systemui.util.Utils; import java.util.ArrayList; import java.util.Collection; @@ -82,8 +81,7 @@ public class QuickQSPanel extends QSPanel { MediaHost mediaHost, UiEventLogger uiEventLogger ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, - uiEventLogger); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger); if (mFooter != null) { removeView(mFooter.getView()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 217148df60e2..5628a24f40ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -52,7 +52,6 @@ import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaDeviceManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; @@ -102,6 +101,12 @@ public class NotificationMediaManager implements Dumpable { PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED); PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR); } + private static final HashSet<Integer> INACTIVE_MEDIA_STATES = new HashSet<>(); + static { + INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE); + INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); + INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR); + } private final NotificationEntryManager mEntryManager; private final MediaDataManager mMediaDataManager; @@ -190,8 +195,7 @@ public class NotificationMediaManager implements Dumpable { KeyguardBypassController keyguardBypassController, @Main DelayableExecutor mainExecutor, DeviceConfigProxy deviceConfig, - MediaDataManager mediaDataManager, - MediaDeviceManager mediaDeviceManager) { + MediaDataManager mediaDataManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; @@ -212,13 +216,11 @@ public class NotificationMediaManager implements Dumpable { @Override public void onPendingEntryAdded(NotificationEntry entry) { mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); - mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override public void onPreEntryUpdated(NotificationEntry entry) { mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); - mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override @@ -239,7 +241,6 @@ public class NotificationMediaManager implements Dumpable { int reason) { onNotificationRemoved(entry.getKey()); mediaDataManager.onNotificationRemoved(entry.getKey()); - mediaDeviceManager.onNotificationRemoved(entry.getKey()); } }); @@ -252,10 +253,24 @@ public class NotificationMediaManager implements Dumpable { mPropertiesChangedListener); } + /** + * Check if a state should be considered actively playing + * @param state a PlaybackState + * @return true if playing + */ public static boolean isPlayingState(int state) { return !PAUSED_MEDIA_STATES.contains(state); } + /** + * Check if a state should be considered active (playing or paused) + * @param state a PlaybackState + * @return true if active + */ + public static boolean isActiveState(int state) { + return !INACTIVE_MEDIA_STATES.contains(state); + } + public void setUpWithPresenter(NotificationPresenter presenter) { mPresenter = presenter; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index c988e1251d3f..84c8db3218e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -24,7 +24,6 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaDeviceManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.ActionClickLogger; import com.android.systemui.statusbar.CommandQueue; @@ -51,8 +50,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.concurrency.DelayableExecutor; -import java.util.concurrent.Executor; - import javax.inject.Singleton; import dagger.Lazy; @@ -105,8 +102,7 @@ public interface StatusBarDependenciesModule { KeyguardBypassController keyguardBypassController, @Main DelayableExecutor mainExecutor, DeviceConfigProxy deviceConfigProxy, - MediaDataManager mediaDataManager, - MediaDeviceManager mediaDeviceManager) { + MediaDataManager mediaDataManager) { return new NotificationMediaManager( context, statusBarLazy, @@ -116,8 +112,7 @@ public interface StatusBarDependenciesModule { keyguardBypassController, mainExecutor, deviceConfigProxy, - mediaDataManager, - mediaDeviceManager); + mediaDataManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 9925909c3e16..8a4fdc24dc1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -557,9 +557,9 @@ public class NotificationContentView extends FrameLayout { private void focusExpandButtonIfNecessary() { if (mFocusOnVisibilityChange) { - NotificationHeaderView header = getVisibleNotificationHeader(); - if (header != null) { - ImageView expandButton = header.getExpandButton(); + NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); + if (wrapper != null) { + View expandButton = wrapper.getExpandButton(); if (expandButton != null) { expandButton.requestAccessibilityFocus(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index 15499b87d56d..fe70c818216e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -50,6 +50,7 @@ class NotificationConversationTemplateViewWrapper constructor( private lateinit var conversationBadgeBg: View private lateinit var expandButton: View private lateinit var expandButtonContainer: View + private lateinit var expandButtonInnerContainer: View private lateinit var imageMessageContainer: ViewGroup private lateinit var messagingLinearLayout: MessagingLinearLayout private lateinit var conversationTitleView: View @@ -69,6 +70,8 @@ class NotificationConversationTemplateViewWrapper constructor( expandButton = requireViewById(com.android.internal.R.id.expand_button) expandButtonContainer = requireViewById(com.android.internal.R.id.expand_button_container) + expandButtonInnerContainer = + requireViewById(com.android.internal.R.id.expand_button_inner_container) importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring) appName = requireViewById(com.android.internal.R.id.app_name_text) conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) @@ -134,6 +137,8 @@ class NotificationConversationTemplateViewWrapper constructor( ) } + override fun getExpandButton() = expandButtonInnerContainer + override fun setShelfIconVisible(visible: Boolean) { if (conversationLayout.isImportantConversation) { if (conversationIconView.visibility != GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index f8b783113ccb..4c9cb209424a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -317,6 +317,11 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } @Override + public View getExpandButton() { + return mExpandButton; + } + + @Override public int getOriginalIconColor() { return mIcon.getOriginalIconColor(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index 02e537d2879f..30080e3d8cc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -240,6 +240,13 @@ public abstract class NotificationViewWrapper implements TransformableView { return null; } + /** + * @return the expand button if it exists + */ + public @Nullable View getExpandButton() { + return null; + } + public int getOriginalIconColor() { return Notification.COLOR_INVALID; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index b4de3cd5d43b..18a7adda3f7d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -640,8 +640,7 @@ public class MobileSignalController extends SignalController< + " dataState=" + state.getDataRegistrationState()); } mServiceState = state; - // onDisplayInfoChanged is invoked directly after onServiceStateChanged, so not calling - // updateTelephony() to prevent icon flickering in case of overrides. + updateTelephony(); } @Override @@ -651,12 +650,6 @@ public class MobileSignalController extends SignalController< + " type=" + networkType); } mDataState = state; - if (networkType != mTelephonyDisplayInfo.getNetworkType()) { - Log.d(mTag, "onDataConnectionStateChanged:" - + " network type change and reset displayInfo. type=" + networkType); - mTelephonyDisplayInfo = new TelephonyDisplayInfo(networkType, - TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); - } updateTelephony(); } diff --git a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java index 6c3538cb6142..a31ea7c3ab17 100644 --- a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java +++ b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java @@ -40,7 +40,7 @@ public class DismissCircleView extends FrameLayout { setBackground(res.getDrawable(R.drawable.dismiss_circle_background)); - mIconView.setImageDrawable(res.getDrawable(R.drawable.dismiss_target_x)); + mIconView.setImageDrawable(res.getDrawable(R.drawable.ic_close_white)); addView(mIconView); setViewSizes(); diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java index b1792d003290..5c9db54a0f00 100644 --- a/packages/SystemUI/src/com/android/systemui/util/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java @@ -133,4 +133,13 @@ public class Utils { Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1); return flag > 0; } + + /** + * Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled. + * Off by default, but can be enabled by setting to 1 + */ + public static boolean useMediaResumption(Context context) { + int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_resumption", 0); + return useQsMediaPlayer(context) && flag > 0; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index 9d2b6f4deb14..1ba36e19b404 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -67,7 +67,6 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var player: MediaControlPanel - private lateinit var fgExecutor: FakeExecutor private lateinit var bgExecutor: FakeExecutor @Mock private lateinit var activityStarter: ActivityStarter @@ -97,14 +96,12 @@ public class MediaControlPanelTest : SysuiTestCase() { @Before fun setUp() { - fgExecutor = FakeExecutor(FakeSystemClock()) bgExecutor = FakeExecutor(FakeSystemClock()) activityStarter = mock(ActivityStarter::class.java) mediaHostStatesManager = mock(MediaHostStatesManager::class.java) - player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter, - mediaHostStatesManager) + player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager) // Mock out a view holder for the player to attach to. holder = mock(PlayerViewHolder::class.java) @@ -171,7 +168,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindWhenUnattached() { val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, null, null, device) + emptyList(), PACKAGE, null, null, device, null) player.bind(state) assertThat(player.isPlaying()).isFalse() } @@ -180,7 +177,7 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindText() { player.attach(holder) val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, session.getSessionToken(), null, device) + emptyList(), PACKAGE, session.getSessionToken(), null, device, null) player.bind(state) assertThat(appName.getText()).isEqualTo(APP) assertThat(titleText.getText()).isEqualTo(TITLE) @@ -191,7 +188,7 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindBackgroundColor() { player.attach(holder) val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, session.getSessionToken(), null, device) + emptyList(), PACKAGE, session.getSessionToken(), null, device, null) player.bind(state) val list = ArgumentCaptor.forClass(ColorStateList::class.java) verify(view).setBackgroundTintList(list.capture()) @@ -202,7 +199,7 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindDevice() { player.attach(holder) val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, session.getSessionToken(), null, device) + emptyList(), PACKAGE, session.getSessionToken(), null, device, null) player.bind(state) assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) assertThat(seamless.isEnabled()).isTrue() @@ -212,7 +209,7 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindDisabledDevice() { player.attach(holder) val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice) + emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, null) player.bind(state) assertThat(seamless.isEnabled()).isFalse() assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString( @@ -223,7 +220,7 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindNullDevice() { player.attach(holder) val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), - emptyList(), PACKAGE, session.getSessionToken(), null, null) + emptyList(), PACKAGE, session.getSessionToken(), null, null, null) player.bind(state) assertThat(seamless.isEnabled()).isTrue() assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 48e3b0a9d993..bed5c9eb6df5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -79,16 +79,16 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, - new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, KEY); + new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, null, KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } @Test public void eventNotEmittedWithoutDevice() { // WHEN data source emits an event without device data - mDataListener.onMediaDataLoaded(KEY, mMediaData); + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); // THEN an event isn't emitted - verify(mListener, never()).onMediaDataLoaded(eq(KEY), any()); + verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any()); } @Test @@ -96,7 +96,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { // WHEN device source emits an event without media data mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData); // THEN an event isn't emitted - verify(mListener, never()).onMediaDataLoaded(eq(KEY), any()); + verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any()); } @Test @@ -104,22 +104,22 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { // GIVEN that a device event has already been received mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData); // WHEN media event is received - mDataListener.onMediaDataLoaded(KEY, mMediaData); + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void emitEventAfterMediaFirst() { // GIVEN that media event has already been received - mDataListener.onMediaDataLoaded(KEY, mMediaData); + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); // WHEN device event is received mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture()); assertThat(captor.getValue().getDevice()).isNotNull(); } @@ -133,7 +133,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { @Test public void mediaDataRemovedAfterMediaEvent() { - mDataListener.onMediaDataLoaded(KEY, mMediaData); + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); mDataListener.onMediaDataRemoved(KEY); verify(mListener).onMediaDataRemoved(eq(KEY)); } @@ -145,6 +145,18 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { verify(mListener).onMediaDataRemoved(eq(KEY)); } + @Test + public void mediaDataKeyUpdated() { + // GIVEN that device and media events have already been received + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); + mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData); + // WHEN the key is changed + mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData); + // THEN the listener gets a load event with the correct keys + ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); + verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture()); + } + private MediaDataManager.Listener captureDataListener() { ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass( MediaDataManager.Listener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index c0aef8adc4af..3a3140f2ff53 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -23,8 +23,6 @@ import android.media.MediaRouter2Manager import android.media.RoutingSessionInfo import android.media.session.MediaSession import android.media.session.PlaybackState -import android.os.Process -import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -67,6 +65,7 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var manager: MediaDeviceManager + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var lmmFactory: LocalMediaManagerFactory @Mock private lateinit var lmm: LocalMediaManager @Mock private lateinit var mr2: MediaRouter2Manager @@ -80,13 +79,14 @@ public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var playbackBuilder: PlaybackState.Builder private lateinit var notifBuilder: Notification.Builder - private lateinit var sbn: StatusBarNotification + private lateinit var mediaData: MediaData @JvmField @Rule val mockito = MockitoJUnit.rule() @Before fun setUp() { fakeExecutor = FakeExecutor(FakeSystemClock()) - manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor) + manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor, + mediaDataManager) manager.addListener(listener) // Configure mocks. @@ -117,8 +117,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setSmallIcon(android.R.drawable.ic_media_pause) setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken())) } - sbn = StatusBarNotification(PACKAGE, PACKAGE, 0, "TAG", Process.myUid(), 0, 0, - notifBuilder.build(), Process.myUserHandle(), 0) + mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null) } @After @@ -128,33 +128,33 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun removeUnknown() { - manager.onNotificationRemoved("unknown") + manager.onMediaDataRemoved("unknown") } @Test fun addNotification() { - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) verify(lmmFactory).create(PACKAGE) } @Test fun featureDisabled() { whenever(featureFlag.enabled).thenReturn(false) - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) verify(lmmFactory, never()).create(PACKAGE) } @Test fun addAndRemoveNotification() { - manager.onNotificationAdded(KEY, sbn) - manager.onNotificationRemoved(KEY) + manager.onMediaDataLoaded(KEY, null, mediaData) + manager.onMediaDataRemoved(KEY) verify(lmm).unregisterCallback(any()) } @Test fun deviceEventOnAddNotification() { // WHEN a notification is added - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) val deviceCallback = captureCallback() // THEN the update is dispatched to the listener val data = captureDeviceData(KEY) @@ -165,7 +165,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun deviceListUpdate() { - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) val deviceCallback = captureCallback() // WHEN the device list changes deviceCallback.onDeviceListUpdate(mutableListOf(device)) @@ -179,7 +179,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun selectedDeviceStateChanged() { - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) val deviceCallback = captureCallback() // WHEN the selected device changes state deviceCallback.onSelectedDeviceStateChanged(device, 1) @@ -193,9 +193,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun listenerReceivesKeyRemoved() { - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) // WHEN the notification is removed - manager.onNotificationRemoved(KEY) + manager.onMediaDataRemoved(KEY) // THEN the listener receives key removed event verify(listener).onKeyRemoved(eq(KEY)) } @@ -205,7 +205,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // GIVEN that MR2Manager returns null for routing session whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) // WHEN a notification is added - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) // THEN the device is disabled val data = captureDeviceData(KEY) assertThat(data.enabled).isFalse() @@ -216,7 +216,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() { // GIVEN a notif is added - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) reset(listener) // AND MR2Manager returns null for routing session whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) @@ -234,7 +234,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() { // GIVEN a notif is added - manager.onNotificationAdded(KEY, sbn) + manager.onMediaDataLoaded(KEY, null, mediaData) reset(listener) // GIVEN that MR2Manager returns null for routing session whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index c21343cb5423..643a3352c30c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -16,7 +16,9 @@ package com.android.systemui.media +import android.media.MediaMetadata import android.media.session.MediaController +import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest @@ -41,6 +43,10 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit private const val KEY = "KEY" +private const val PACKAGE = "PKG" +private const val SESSION_KEY = "SESSION_KEY" +private const val SESSION_ARTIST = "SESSION_ARTIST" +private const val SESSION_TITLE = "SESSION_TITLE" private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { @@ -54,12 +60,15 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var mediaControllerFactory: MediaControllerFactory @Mock private lateinit var mediaController: MediaController @Mock private lateinit var executor: DelayableExecutor - @Mock private lateinit var mediaData: MediaData @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var cancellationRunnable: Runnable @Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable> @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> @JvmField @Rule val mockito = MockitoJUnit.rule() + private lateinit var metadataBuilder: MediaMetadata.Builder + private lateinit var playbackBuilder: PlaybackState.Builder + private lateinit var session: MediaSession + private lateinit var mediaData: MediaData private lateinit var mediaTimeoutListener: MediaTimeoutListener @Before @@ -68,22 +77,39 @@ class MediaTimeoutListenerTest : SysuiTestCase() { `when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable) mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor) mediaTimeoutListener.timeoutCallback = timeoutCallback + + // Create a media session and notification for testing. + metadataBuilder = MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + playbackBuilder = PlaybackState.Builder().apply { + setState(PlaybackState.STATE_PAUSED, 6000L, 1f) + setActions(PlaybackState.ACTION_PLAY) + } + session = MediaSession(context, SESSION_KEY).apply { + setMetadata(metadataBuilder.build()) + setPlaybackState(playbackBuilder.build()) + } + session.setActive(true) + mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null) } @Test fun testOnMediaDataLoaded_registersPlaybackListener() { - mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData) + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) // Ignores is same key clearInvocations(mediaController) - mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData) + mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData) verify(mediaController, never()).registerCallback(anyObject()) } @Test fun testOnMediaDataRemoved_unregistersPlaybackListener() { - mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData) + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) mediaTimeoutListener.onMediaDataRemoved(KEY) verify(mediaController).unregisterCallback(anyObject()) @@ -124,7 +150,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testIsTimedOut() { - mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData) + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse() } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java index b018b59e4389..ed4f8b330e23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -30,11 +29,13 @@ import android.app.AppOpsManager; import android.util.ArraySet; import android.view.NotificationHeaderView; import android.view.View; +import android.view.ViewPropertyAnimator; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.widget.NotificationExpandButton; import com.android.systemui.SysuiTestCase; import org.junit.Before; @@ -98,4 +99,42 @@ public class NotificationContentViewTest extends SysuiTestCase { verify(mockExpanded, times(1)).setVisibility(View.VISIBLE); verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE); } + + @Test + @UiThreadTest + public void testExpandButtonFocusIsCalled() { + View mockContractedEB = mock(NotificationExpandButton.class); + View mockContracted = mock(NotificationHeaderView.class); + when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class)); + when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn( + mockContractedEB); + + View mockExpandedEB = mock(NotificationExpandButton.class); + View mockExpanded = mock(NotificationHeaderView.class); + when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class)); + when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn( + mockExpandedEB); + + View mockHeadsUpEB = mock(NotificationExpandButton.class); + View mockHeadsUp = mock(NotificationHeaderView.class); + when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class)); + when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn( + mockHeadsUpEB); + + // Set up all 3 child forms + mView.setContractedChild(mockContracted); + mView.setExpandedChild(mockExpanded); + mView.setHeadsUpChild(mockHeadsUp); + + // This is required to call requestAccessibilityFocus() + mView.setFocusOnVisibilityChange(); + + // The following will initialize the view and switch from not visible to expanded. + // (heads-up is actually an alternate form of contracted, hence this enters expanded state) + mView.setHeadsUp(true); + + verify(mockContractedEB, times(0)).requestAccessibilityFocus(); + verify(mockExpandedEB, times(1)).requestAccessibilityFocus(); + verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus(); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 9de95abafdda..b9669c74a6df 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -40,6 +40,7 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiHotplugEvent; import android.hardware.hdmi.HdmiPortInfo; +import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlService; import android.hardware.hdmi.IHdmiControlStatusChangeListener; @@ -63,6 +64,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; @@ -268,6 +270,11 @@ public class HdmiControlService extends SystemService { private final ArrayList<HdmiControlStatusChangeListenerRecord> mHdmiControlStatusChangeListenerRecords = new ArrayList<>(); + // List of records for HDMI control volume control status change listener for death monitoring. + @GuardedBy("mLock") + private final RemoteCallbackList<IHdmiCecVolumeControlFeatureListener> + mHdmiCecVolumeControlFeatureListenerRecords = new RemoteCallbackList<>(); + // List of records for hotplug event listener to handle the the caller killed in action. @GuardedBy("mLock") private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = @@ -1814,6 +1821,21 @@ public class HdmiControlService extends SystemService { } @Override + public void addHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + enforceAccessPermission(); + HdmiControlService.this.addHdmiCecVolumeControlFeatureListener(listener); + } + + @Override + public void removeHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + enforceAccessPermission(); + HdmiControlService.this.removeHdmiControlVolumeControlStatusChangeListener(listener); + } + + + @Override public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { enforceAccessPermission(); HdmiControlService.this.addHotplugEventListener(listener); @@ -2409,6 +2431,33 @@ public class HdmiControlService extends SystemService { } } + @VisibleForTesting + void addHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + mHdmiCecVolumeControlFeatureListenerRecords.register(listener); + + runOnServiceThread(new Runnable() { + @Override + public void run() { + // Return the current status of mHdmiCecVolumeControlEnabled; + synchronized (mLock) { + try { + listener.onHdmiCecVolumeControlFeature(mHdmiCecVolumeControlEnabled); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report HdmiControlVolumeControlStatusChange: " + + mHdmiCecVolumeControlEnabled, e); + } + } + } + }); + } + + @VisibleForTesting + void removeHdmiControlVolumeControlStatusChangeListener( + final IHdmiCecVolumeControlFeatureListener listener) { + mHdmiCecVolumeControlFeatureListenerRecords.unregister(listener); + } + private void addHotplugEventListener(final IHdmiHotplugEventListener listener) { final HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); try { @@ -2682,6 +2731,19 @@ public class HdmiControlService extends SystemService { } } + private void announceHdmiCecVolumeControlFeatureChange(boolean isEnabled) { + assertRunOnServiceThread(); + mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> { + try { + listener.onHdmiCecVolumeControlFeature(isEnabled); + } catch (RemoteException e) { + Slog.e(TAG, + "Failed to report HdmiControlVolumeControlStatusChange: " + + isEnabled); + } + }); + } + public HdmiCecLocalDeviceTv tv() { return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_TV); } @@ -3026,6 +3088,7 @@ public class HdmiControlService extends SystemService { isHdmiCecVolumeControlEnabled); } } + announceHdmiCecVolumeControlFeatureChange(isHdmiCecVolumeControlEnabled); } boolean isHdmiCecVolumeControlEnabled() { diff --git a/services/core/java/com/android/server/notification/BadgeExtractor.java b/services/core/java/com/android/server/notification/BadgeExtractor.java index af8baa50d501..d323d8095525 100644 --- a/services/core/java/com/android/server/notification/BadgeExtractor.java +++ b/services/core/java/com/android/server/notification/BadgeExtractor.java @@ -19,6 +19,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; import android.content.Context; import android.util.Slog; +import android.app.Notification; /** * Determines whether a badge should be shown for this notification @@ -61,6 +62,10 @@ public class BadgeExtractor implements NotificationSignalExtractor { record.setShowBadge(false); } + Notification.BubbleMetadata metadata = record.getNotification().getBubbleMetadata(); + if (metadata != null && metadata.isNotificationSuppressed()) { + record.setShowBadge(false); + } return null; } diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 59735ebb24d2..d6b1b27360ca 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -65,7 +65,7 @@ final class IdmapManager { * modified. */ boolean createIdmap(@NonNull final PackageInfo targetPackage, - @NonNull final PackageInfo overlayPackage, int additionalPolicies, int userId) { + @NonNull final PackageInfo overlayPackage, int userId) { if (DEBUG) { Slog.d(TAG, "create idmap for " + targetPackage.packageName + " and " + overlayPackage.packageName); @@ -73,14 +73,13 @@ final class IdmapManager { final String targetPath = targetPackage.applicationInfo.getBaseCodePath(); final String overlayPath = overlayPackage.applicationInfo.getBaseCodePath(); try { + int policies = calculateFulfilledPolicies(targetPackage, overlayPackage, userId); boolean enforce = enforceOverlayable(overlayPackage); - int policies = calculateFulfilledPolicies(targetPackage, overlayPackage, userId) - | additionalPolicies; if (mIdmapDaemon.verifyIdmap(targetPath, overlayPath, policies, enforce, userId)) { return false; } - return mIdmapDaemon.createIdmap(targetPath, overlayPath, policies, enforce, userId) - != null; + return mIdmapDaemon.createIdmap(targetPath, overlayPath, policies, + enforce, userId) != null; } catch (Exception e) { Slog.w(TAG, "failed to generate idmap for " + targetPath + " and " + overlayPath + ": " + e.getMessage()); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index 3c5e47625fa2..396815399874 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -45,7 +45,6 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; import android.content.res.ApkAssets; -import android.content.res.Resources; import android.net.Uri; import android.os.Binder; import android.os.Environment; @@ -63,7 +62,6 @@ import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; -import com.android.internal.R; import com.android.internal.content.om.OverlayConfig; import com.android.server.FgThread; import com.android.server.IoThread; @@ -252,8 +250,7 @@ public final class OverlayManagerService extends SystemService { mSettings = new OverlayManagerSettings(); mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings, OverlayConfig.getSystemInstance(), getDefaultOverlayPackages(), - new OverlayChangeListener(), getOverlayableConfigurator(), - getOverlayableConfiguratorTargets()); + new OverlayChangeListener()); mActorEnforcer = new OverlayActorEnforcer(mPackageManager); final IntentFilter packageFilter = new IntentFilter(); @@ -336,28 +333,6 @@ public final class OverlayManagerService extends SystemService { return defaultPackages.toArray(new String[defaultPackages.size()]); } - - /** - * Retrieves the package name that is recognized as an actor for the packages specified by - * {@link #getOverlayableConfiguratorTargets()}. - */ - @Nullable - private String getOverlayableConfigurator() { - return TextUtils.nullIfEmpty(Resources.getSystem() - .getString(R.string.config_overlayableConfigurator)); - } - - /** - * Retrieves the target packages that recognize the {@link #getOverlayableConfigurator} as an - * actor for itself. Overlays targeting one of the specified targets that are signed with the - * same signature as the overlayable configurator will be granted the "actor" policy. - */ - @Nullable - private String[] getOverlayableConfiguratorTargets() { - return Resources.getSystem().getStringArray( - R.array.config_overlayableConfiguratorTargets); - } - private final class PackageReceiver extends BroadcastReceiver { @Override public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 879ad4fdf011..05a4a38feef1 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -31,7 +31,6 @@ import android.annotation.Nullable; import android.content.om.OverlayInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; -import android.os.OverlayablePolicy; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -74,9 +73,6 @@ final class OverlayManagerServiceImpl { private final String[] mDefaultOverlays; private final OverlayChangeListener mListener; - private final String mOverlayableConfigurator; - private final String[] mOverlayableConfiguratorTargets; - /** * Helper method to merge the overlay manager's (as read from overlays.xml) * and package manager's (as parsed from AndroidManifest.xml files) views @@ -119,17 +115,13 @@ final class OverlayManagerServiceImpl { @NonNull final OverlayManagerSettings settings, @NonNull final OverlayConfig overlayConfig, @NonNull final String[] defaultOverlays, - @NonNull final OverlayChangeListener listener, - @Nullable final String overlayableConfigurator, - @Nullable final String[] overlayableConfiguratorTargets) { + @NonNull final OverlayChangeListener listener) { mPackageManager = packageManager; mIdmapManager = idmapManager; mSettings = settings; mOverlayConfig = overlayConfig; mDefaultOverlays = defaultOverlays; mListener = listener; - mOverlayableConfigurator = overlayableConfigurator; - mOverlayableConfiguratorTargets = overlayableConfiguratorTargets; } /** @@ -714,25 +706,7 @@ final class OverlayManagerServiceImpl { if (targetPackage != null && overlayPackage != null && !("android".equals(targetPackageName) && !isPackageConfiguredMutable(overlayPackageName))) { - - int additionalPolicies = 0; - if (TextUtils.nullIfEmpty(mOverlayableConfigurator) != null - && ArrayUtils.contains(mOverlayableConfiguratorTargets, targetPackageName) - && isPackageConfiguredMutable(overlayPackageName) - && mPackageManager.signaturesMatching(mOverlayableConfigurator, - overlayPackageName, userId)) { - // The overlay targets a package that has the overlayable configurator configured as - // its actor. The overlay and this actor are signed with the same signature, so - // the overlay fulfills the actor policy. - modified |= mSettings.setHasConfiguratorActorPolicy(overlayPackageName, userId, - true); - additionalPolicies |= OverlayablePolicy.ACTOR_SIGNATURE; - } else if (mSettings.hasConfiguratorActorPolicy(overlayPackageName, userId)) { - additionalPolicies |= OverlayablePolicy.ACTOR_SIGNATURE; - } - - modified |= mIdmapManager.createIdmap(targetPackage, overlayPackage, additionalPolicies, - userId); + modified |= mIdmapManager.createIdmap(targetPackage, overlayPackage, userId); } if (overlayPackage != null) { diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index f8226faf1336..3d520bf59068 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -73,7 +73,7 @@ final class OverlayManagerSettings { remove(packageName, userId); insert(new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName, baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled, isMutable, priority, - overlayCategory, false /* hasConfiguratorActorPolicy */)); + overlayCategory)); } /** @@ -160,26 +160,6 @@ final class OverlayManagerSettings { return mItems.get(idx).setState(state); } - boolean hasConfiguratorActorPolicy(@NonNull final String packageName, final int userId) { - final int idx = select(packageName, userId); - if (idx < 0) { - throw new BadKeyException(packageName, userId); - } - return mItems.get(idx).hasConfiguratorActorPolicy(); - } - - /** - * Returns true if the settings were modified, false if they remain the same. - */ - boolean setHasConfiguratorActorPolicy(@NonNull final String packageName, final int userId, - boolean hasPolicy) { - final int idx = select(packageName, userId); - if (idx < 0) { - throw new BadKeyException(packageName, userId); - } - return mItems.get(idx).setHasConfiguratorActorPolicy(hasPolicy); - } - List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName, final int userId) { // Immutable RROs targeting "android" are loaded from AssetManager, and so they should be @@ -343,17 +323,16 @@ final class OverlayManagerSettings { pw.println(item.mPackageName + ":" + item.getUserId() + " {"); pw.increaseIndent(); - pw.println("mPackageName................: " + item.mPackageName); - pw.println("mUserId.....................: " + item.getUserId()); - pw.println("mTargetPackageName..........: " + item.getTargetPackageName()); - pw.println("mTargetOverlayableName......: " + item.getTargetOverlayableName()); - pw.println("mBaseCodePath...............: " + item.getBaseCodePath()); - pw.println("mState......................: " + OverlayInfo.stateToString(item.getState())); - pw.println("mIsEnabled..................: " + item.isEnabled()); - pw.println("mIsMutable..................: " + item.isMutable()); - pw.println("mPriority...................: " + item.mPriority); - pw.println("mCategory...................: " + item.mCategory); - pw.println("mHasConfiguratorActorPolicy.: " + item.hasConfiguratorActorPolicy()); + pw.println("mPackageName...........: " + item.mPackageName); + pw.println("mUserId................: " + item.getUserId()); + pw.println("mTargetPackageName.....: " + item.getTargetPackageName()); + pw.println("mTargetOverlayableName.: " + item.getTargetOverlayableName()); + pw.println("mBaseCodePath..........: " + item.getBaseCodePath()); + pw.println("mState.................: " + OverlayInfo.stateToString(item.getState())); + pw.println("mIsEnabled.............: " + item.isEnabled()); + pw.println("mIsMutable.............: " + item.isMutable()); + pw.println("mPriority..............: " + item.mPriority); + pw.println("mCategory..............: " + item.mCategory); pw.decreaseIndent(); pw.println("}"); @@ -392,9 +371,6 @@ final class OverlayManagerSettings { case "category": pw.println(item.mCategory); break; - case "hasconfiguratoractorpolicy": - pw.println(item.mHasConfiguratorActorPolicy); - break; } } @@ -422,8 +398,6 @@ final class OverlayManagerSettings { private static final String ATTR_CATEGORY = "category"; private static final String ATTR_USER_ID = "userId"; private static final String ATTR_VERSION = "version"; - private static final String ATTR_HAS_CONFIGURATOR_ACTOR_POLICY = - "hasConfiguratorActorPolicy"; @VisibleForTesting static final int CURRENT_VERSION = 4; @@ -461,6 +435,10 @@ final class OverlayManagerSettings { // Throw an exception which will cause the overlay file to be ignored // and overwritten. throw new XmlPullParserException("old version " + oldVersion + "; ignoring"); + case 3: + // Upgrading from version 3 to 4 is not a breaking change so do not ignore the + // overlay file. + return; default: throw new XmlPullParserException("unrecognized version " + oldVersion); } @@ -480,12 +458,9 @@ final class OverlayManagerSettings { final boolean isStatic = XmlUtils.readBooleanAttribute(parser, ATTR_IS_STATIC); final int priority = XmlUtils.readIntAttribute(parser, ATTR_PRIORITY); final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY); - final boolean hasConfiguratorActorPolicy = XmlUtils.readBooleanAttribute(parser, - ATTR_HAS_CONFIGURATOR_ACTOR_POLICY); return new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName, - baseCodePath, state, isEnabled, !isStatic, priority, category, - hasConfiguratorActorPolicy); + baseCodePath, state, isEnabled, !isStatic, priority, category); } public static void persist(@NonNull final ArrayList<SettingsItem> table, @@ -520,8 +495,6 @@ final class OverlayManagerSettings { XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, !item.mIsMutable); XmlUtils.writeIntAttribute(xml, ATTR_PRIORITY, item.mPriority); XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory); - XmlUtils.writeBooleanAttribute(xml, ATTR_HAS_CONFIGURATOR_ACTOR_POLICY, - item.mHasConfiguratorActorPolicy); xml.endTag(null, TAG_ITEM); } } @@ -538,14 +511,12 @@ final class OverlayManagerSettings { private boolean mIsMutable; private int mPriority; private String mCategory; - private boolean mHasConfiguratorActorPolicy; SettingsItem(@NonNull final String packageName, final int userId, @NonNull final String targetPackageName, @Nullable final String targetOverlayableName, @NonNull final String baseCodePath, final @OverlayInfo.State int state, final boolean isEnabled, - final boolean isMutable, final int priority, @Nullable String category, - final boolean hasConfiguratorActorPolicy) { + final boolean isMutable, final int priority, @Nullable String category) { mPackageName = packageName; mUserId = userId; mTargetPackageName = targetPackageName; @@ -557,7 +528,6 @@ final class OverlayManagerSettings { mCache = null; mIsMutable = isMutable; mPriority = priority; - mHasConfiguratorActorPolicy = hasConfiguratorActorPolicy; } private String getTargetPackageName() { @@ -648,18 +618,6 @@ final class OverlayManagerSettings { private int getPriority() { return mPriority; } - - private boolean hasConfiguratorActorPolicy() { - return mHasConfiguratorActorPolicy; - } - - private boolean setHasConfiguratorActorPolicy(boolean hasPolicy) { - if (mHasConfiguratorActorPolicy != hasPolicy) { - mHasConfiguratorActorPolicy = hasPolicy; - return true; - } - return false; - } } private int select(@NonNull final String packageName, final int userId) { diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index 49c781905898..2f963b7e6b35 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -20,11 +20,16 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; import android.hardware.soundtrigger.V2_2.ISoundTriggerHw; +import android.media.audio.common.AudioConfig; +import android.media.audio.common.AudioOffloadInfo; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; import android.media.soundtrigger_middleware.PhraseSoundModel; import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; import android.media.soundtrigger_middleware.SoundModel; import android.media.soundtrigger_middleware.SoundModelType; import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; @@ -540,20 +545,20 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { switch (mModelType) { case SoundModelType.GENERIC: { android.media.soundtrigger_middleware.RecognitionEvent event = - new android.media.soundtrigger_middleware.RecognitionEvent(); + newEmptyRecognitionEvent(); event.status = android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + event.type = SoundModelType.GENERIC; mCallback.onRecognition(mHandle, event); } break; case SoundModelType.KEYPHRASE: { android.media.soundtrigger_middleware.PhraseRecognitionEvent event = - new android.media.soundtrigger_middleware.PhraseRecognitionEvent(); - event.common = - new android.media.soundtrigger_middleware.RecognitionEvent(); + newEmptyPhraseRecognitionEvent(); event.common.status = android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + event.common.type = SoundModelType.KEYPHRASE; mCallback.onPhraseRecognition(mHandle, event); } break; @@ -614,4 +619,35 @@ class SoundTriggerModule implements IHwBinder.DeathRecipient { } } } + + /** + * Creates a default-initialized recognition event. + * + * Object fields are default constructed. + * Array fields are initialized to 0 length. + * + * @return The event. + */ + private static RecognitionEvent newEmptyRecognitionEvent() { + RecognitionEvent result = new RecognitionEvent(); + result.audioConfig = new AudioConfig(); + result.audioConfig.offloadInfo = new AudioOffloadInfo(); + result.data = new byte[0]; + return result; + } + + /** + * Creates a default-initialized phrase recognition event. + * + * Object fields are default constructed. + * Array fields are initialized to 0 length. + * + * @return The event. + */ + private static PhraseRecognitionEvent newEmptyPhraseRecognitionEvent() { + PhraseRecognitionEvent result = new PhraseRecognitionEvent(); + result.common = newEmptyRecognitionEvent(); + result.phraseExtras = new PhraseRecognitionExtra[0]; + return result; + } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 31897057e076..48609e17ba40 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3164,6 +3164,11 @@ class Task extends WindowContainer<WindowContainer> { } @Override + public SurfaceControl.Builder makeAnimationLeash() { + return super.makeAnimationLeash().setMetadata(METADATA_TASK_ID, mTaskId); + } + + @Override public SurfaceControl getAnimationLeashParent() { if (WindowManagerService.sHierarchicalAnimations) { return super.getAnimationLeashParent(); diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 42c21930bdf7..c0252363a159 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -418,25 +418,25 @@ class WindowStateAnimator { if (!mDestroyPreservedSurfaceUponRedraw) { return; } - if (mSurfaceController != null) { - if (mPendingDestroySurface != null) { - // If we are preserving a surface but we aren't relaunching that means - // we are just doing an in-place switch. In that case any SurfaceFlinger side - // child layers need to be reparented to the new surface to make this - // transparent to the app. - if (mWin.mActivityRecord == null || mWin.mActivityRecord.isRelaunching() == false) { - mPostDrawTransaction.reparentChildren( - mPendingDestroySurface.getClientViewRootSurface(), - mSurfaceController.mSurfaceControl).apply(); - } - } + + // If we are preserving a surface but we aren't relaunching that means + // we are just doing an in-place switch. In that case any SurfaceFlinger side + // child layers need to be reparented to the new surface to make this + // transparent to the app. + // If the children are detached, we don't want to reparent them to the new surface. + // Instead let the children get removed when the old surface is deleted. + if (mSurfaceController != null && mPendingDestroySurface != null && !mChildrenDetached + && (mWin.mActivityRecord == null || !mWin.mActivityRecord.isRelaunching())) { + mPostDrawTransaction.reparentChildren( + mPendingDestroySurface.getClientViewRootSurface(), + mSurfaceController.mSurfaceControl).apply(); } destroyDeferredSurfaceLocked(); mDestroyPreservedSurfaceUponRedraw = false; } - void markPreservedSurfaceForDestroy() { + private void markPreservedSurfaceForDestroy() { if (mDestroyPreservedSurfaceUponRedraw && !mService.mDestroyPreservedSurface.contains(mWin)) { mService.mDestroyPreservedSurface.add(mWin); @@ -1363,9 +1363,13 @@ class WindowStateAnimator { if (mPendingDestroySurface != null && mDestroyPreservedSurfaceUponRedraw) { final SurfaceControl pendingSurfaceControl = mPendingDestroySurface.mSurfaceControl; mPostDrawTransaction.reparent(pendingSurfaceControl, null); - mPostDrawTransaction.reparentChildren( - mPendingDestroySurface.getClientViewRootSurface(), - mSurfaceController.mSurfaceControl); + // If the children are detached, we don't want to reparent them to the new surface. + // Instead let the children get removed when the old surface is deleted. + if (!mChildrenDetached) { + mPostDrawTransaction.reparentChildren( + mPendingDestroySurface.getClientViewRootSurface(), + mSurfaceController.mSurfaceControl); + } } SurfaceControl.mergeToGlobalTransaction(mPostDrawTransaction); @@ -1593,6 +1597,12 @@ class WindowStateAnimator { mSurfaceController.detachChildren(); } mChildrenDetached = true; + // If the children are detached, it means the app is exiting. We don't want to tear the + // content down too early, otherwise we could end up with a flicker. By preserving the + // current surface, we ensure the content remains on screen until the window is completely + // removed. It also ensures that the old surface is cleaned up when started again since it + // forces mSurfaceController to be set to null. + preserveSurfaceLocked(); } void setOffsetPositionForStackResize(boolean offsetPositionForStackResize) { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 7af7a23b1ef6..c34b8e19a41d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiPortInfo; +import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.os.IPowerManager; import android.os.IThermalService; import android.os.Looper; @@ -261,4 +262,89 @@ public class HdmiControlServiceTest { mHdmiControlService.setHdmiCecVolumeControlEnabled(true); assertThat(mHdmiControlService.isHdmiCecVolumeControlEnabled()).isTrue(); } + + @Test + public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_enabled() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isTrue(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_disabled() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isFalse(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isTrue(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_honorsUnregistration() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + mHdmiControlService.removeHdmiControlVolumeControlStatusChangeListener(callback); + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isFalse(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate_multiple() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback1 = new VolumeControlFeatureCallback(); + VolumeControlFeatureCallback callback2 = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback1); + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback2); + + + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback1.mCallbackReceived).isTrue(); + assertThat(callback2.mCallbackReceived).isTrue(); + assertThat(callback1.mVolumeControlEnabled).isTrue(); + assertThat(callback2.mVolumeControlEnabled).isTrue(); + } + + private static class VolumeControlFeatureCallback extends + IHdmiCecVolumeControlFeatureListener.Stub { + boolean mCallbackReceived = false; + boolean mVolumeControlEnabled = false; + + @Override + public void onHdmiCecVolumeControlFeature(boolean enabled) throws RemoteException { + this.mCallbackReceived = true; + this.mVolumeControlEnabled = enabled; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java index 8774ab020202..f4c5506c7001 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java +++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java @@ -26,7 +26,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.om.OverlayInfo; -import android.os.OverlayablePolicy; import androidx.test.runner.AndroidJUnit4; @@ -205,138 +204,4 @@ public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTes impl.setEnabled(OVERLAY, true, USER); assertEquals(0, listener.count); } - - @Test - public void testConfigurator() { - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{TARGET}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"), - USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources") - .setCertificate("one"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(OverlayablePolicy.ACTOR_SIGNATURE, - idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } - - @Test - public void testConfiguratorWithoutOverlayable() { - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{TARGET}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).setCertificate("two"), USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("one"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(OverlayablePolicy.ACTOR_SIGNATURE, - idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } - - @Test - public void testConfiguratorDifferentTargets() { - // The target package is not listed in the configurator target list, so the actor policy - // should not be granted. - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{"somethingElse"}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).setCertificate("two"), USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("one"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } - - @Test - public void testConfiguratorDifferentSignatures() { - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{TARGET}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"), - USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources") - .setCertificate("two"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } - - @Test - public void testConfiguratorWithoutOverlayableDifferentSignatures() { - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{TARGET}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).setCertificate("two"), USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("two"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } - - @Test - public void testConfiguratorChanges() { - mOverlayableConfigurator = "actor"; - mOverlayableConfiguratorTargets = new String[]{TARGET}; - reinitializeImpl(); - - installNewPackage(target("actor").setCertificate("one"), USER); - installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"), - USER); - - DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources") - .setCertificate("one"); - installNewPackage(overlay, USER); - - DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(OverlayablePolicy.ACTOR_SIGNATURE, - idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - - // Change the configurator to a different package. The overlay should still be granted the - // actor policy. - mOverlayableConfigurator = "differentActor"; - OverlayManagerServiceImpl impl = reinitializeImpl(); - impl.updateOverlaysForUser(USER); - - idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(OverlayablePolicy.ACTOR_SIGNATURE, - idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - - // Reset the setting persisting that the overlay once fulfilled the actor policy implicitly - // through the configurator. The overlay should lose the actor policy. - impl = reinitializeImpl(); - getSettings().setHasConfiguratorActorPolicy(OVERLAY, USER, false); - impl.updateOverlaysForUser(USER); - - idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath); - assertNotNull(idmap); - assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE); - } } diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java index 52a58907ea5a..733310b2508a 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java +++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java @@ -52,9 +52,6 @@ class OverlayManagerServiceImplTestsBase { private DummyPackageManagerHelper mPackageManager; private DummyIdmapDaemon mIdmapDaemon; private OverlayConfig mOverlayConfig; - private OverlayManagerSettings mSettings; - String mOverlayableConfigurator; - String[] mOverlayableConfiguratorTargets; @Before public void setUp() { @@ -62,26 +59,20 @@ class OverlayManagerServiceImplTestsBase { mListener = new DummyListener(); mPackageManager = new DummyPackageManagerHelper(mState); mIdmapDaemon = new DummyIdmapDaemon(mState); - mSettings = new OverlayManagerSettings(); mOverlayConfig = mock(OverlayConfig.class); when(mOverlayConfig.getPriority(any())).thenReturn(OverlayConfig.DEFAULT_PRIORITY); when(mOverlayConfig.isEnabled(any())).thenReturn(false); when(mOverlayConfig.isMutable(any())).thenReturn(true); - mOverlayableConfigurator = null; - mOverlayableConfiguratorTargets = null; reinitializeImpl(); } - OverlayManagerServiceImpl reinitializeImpl() { + void reinitializeImpl() { mImpl = new OverlayManagerServiceImpl(mPackageManager, new IdmapManager(mIdmapDaemon, mPackageManager), - mSettings, + new OverlayManagerSettings(), mOverlayConfig, new String[0], - mListener, - mOverlayableConfigurator, - mOverlayableConfiguratorTargets); - return mImpl; + mListener); } OverlayManagerServiceImpl getImpl() { @@ -92,14 +83,6 @@ class OverlayManagerServiceImplTestsBase { return mListener; } - DummyIdmapDaemon getIdmapDaemon() { - return mIdmapDaemon; - } - - OverlayManagerSettings getSettings() { - return mSettings; - } - void assertState(@State int expected, final String overlayPackageName, int userId) { final OverlayInfo info = mImpl.getOverlayInfo(overlayPackageName, userId); if (info == null) { diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java index e2cedb5e1a62..146f60aff724 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java +++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java @@ -367,8 +367,7 @@ public class OverlayManagerSettingsTests { + " isEnabled='false'\n" + " category='dummy-category'\n" + " isStatic='false'\n" - + " priority='0'" - + " hasConfiguratorActorPolicy='true' />\n" + + " priority='0' />\n" + "</overlays>\n"; ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8")); @@ -381,7 +380,6 @@ public class OverlayManagerSettingsTests { assertEquals(1234, oi.userId); assertEquals(STATE_DISABLED, oi.state); assertFalse(mSettings.getEnabled("com.dummy.overlay", 1234)); - assertTrue(mSettings.hasConfiguratorActorPolicy("com.dummy.overlay", 1234)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt index 5412bb5106ff..74b4d122cbc0 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt @@ -18,8 +18,8 @@ package com.android.server.pm.parsing import android.content.pm.PackageManager import android.platform.test.annotations.Presubmit +import androidx.test.filters.LargeTest import com.google.common.truth.Expect -import com.google.common.truth.Truth.assertWithMessage import org.junit.Rule import org.junit.Test @@ -52,6 +52,7 @@ class AndroidPackageParsingEquivalenceTest : AndroidPackageParsingTestBase() { } } + @LargeTest @Test fun packageInfoEquality() { val flags = PackageManager.GET_ACTIVITIES or @@ -65,7 +66,9 @@ class AndroidPackageParsingEquivalenceTest : AndroidPackageParsingTestBase() { PackageManager.GET_SERVICES or PackageManager.GET_SHARED_LIBRARY_FILES or PackageManager.GET_SIGNATURES or - PackageManager.GET_SIGNING_CERTIFICATES + PackageManager.GET_SIGNING_CERTIFICATES or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_DIRECT_BOOT_AWARE val oldPackageInfo = oldPackages.asSequence().map { oldPackageInfo(it, flags) } val newPackageInfo = newPackages.asSequence().map { newPackageInfo(it, flags) } @@ -77,11 +80,79 @@ class AndroidPackageParsingEquivalenceTest : AndroidPackageParsingTestBase() { } else { "$firstName | $secondName" } - expect.withMessage("${it.first?.applicationInfo?.sourceDir} $packageName") - .that(it.first?.dumpToString()) - .isEqualTo(it.second?.dumpToString()) + + // Main components are asserted independently to separate the failures. Otherwise the + // comparison would include every component in one massive string. + + val prefix = "${it.first?.applicationInfo?.sourceDir} $packageName" + + expect.withMessage("$prefix PackageInfo") + .that(it.second?.dumpToString()) + .isEqualTo(it.first?.dumpToString()) + + expect.withMessage("$prefix ApplicationInfo") + .that(it.second?.applicationInfo?.dumpToString()) + .isEqualTo(it.first?.applicationInfo?.dumpToString()) + + val firstActivityNames = it.first?.activities?.map { it.name } ?: emptyList() + val secondActivityNames = it.second?.activities?.map { it.name } ?: emptyList() + expect.withMessage("$prefix activities") + .that(secondActivityNames) + .containsExactlyElementsIn(firstActivityNames) + .inOrder() + + if (!it.first?.activities.isNullOrEmpty() && !it.second?.activities.isNullOrEmpty()) { + it.first?.activities?.zip(it.second?.activities!!)?.forEach { + expect.withMessage("$prefix ${it.first.name}") + .that(it.second.dumpToString()) + .isEqualTo(it.first.dumpToString()) + } + } + + val firstReceiverNames = it.first?.receivers?.map { it.name } ?: emptyList() + val secondReceiverNames = it.second?.receivers?.map { it.name } ?: emptyList() + expect.withMessage("$prefix receivers") + .that(secondReceiverNames) + .containsExactlyElementsIn(firstReceiverNames) + .inOrder() + + if (!it.first?.receivers.isNullOrEmpty() && !it.second?.receivers.isNullOrEmpty()) { + it.first?.receivers?.zip(it.second?.receivers!!)?.forEach { + expect.withMessage("$prefix ${it.first.name}") + .that(it.second.dumpToString()) + .isEqualTo(it.first.dumpToString()) + } + } + + val firstProviderNames = it.first?.providers?.map { it.name } ?: emptyList() + val secondProviderNames = it.second?.providers?.map { it.name } ?: emptyList() + expect.withMessage("$prefix providers") + .that(secondProviderNames) + .containsExactlyElementsIn(firstProviderNames) + .inOrder() + + if (!it.first?.providers.isNullOrEmpty() && !it.second?.providers.isNullOrEmpty()) { + it.first?.providers?.zip(it.second?.providers!!)?.forEach { + expect.withMessage("$prefix ${it.first.name}") + .that(it.second.dumpToString()) + .isEqualTo(it.first.dumpToString()) + } + } + + val firstServiceNames = it.first?.services?.map { it.name } ?: emptyList() + val secondServiceNames = it.second?.services?.map { it.name } ?: emptyList() + expect.withMessage("$prefix services") + .that(secondServiceNames) + .containsExactlyElementsIn(firstServiceNames) + .inOrder() + + if (!it.first?.services.isNullOrEmpty() && !it.second?.services.isNullOrEmpty()) { + it.first?.services?.zip(it.second?.services!!)?.forEach { + expect.withMessage("$prefix ${it.first.name}") + .that(it.second.dumpToString()) + .isEqualTo(it.first.dumpToString()) + } + } } } } - - diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt index 0f028f05d514..420ff19aab74 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt @@ -19,6 +19,7 @@ package com.android.server.pm.parsing import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo +import android.content.pm.ComponentInfo import android.content.pm.ConfigurationInfo import android.content.pm.FeatureInfo import android.content.pm.InstrumentationInfo @@ -27,6 +28,8 @@ import android.content.pm.PackageParser import android.content.pm.PackageUserState import android.content.pm.PermissionInfo import android.content.pm.ProviderInfo +import android.content.pm.ServiceInfo +import android.os.Bundle import android.os.Debug import android.os.Environment import android.util.SparseArray @@ -38,8 +41,10 @@ import com.android.server.pm.pkg.PackageStateUnserialized import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.whenever import org.junit.BeforeClass -import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString import org.mockito.Mockito.mock import java.io.File @@ -47,7 +52,7 @@ open class AndroidPackageParsingTestBase { companion object { - private const val VERIFY_ALL_APKS = false + private const val VERIFY_ALL_APKS = true /** For auditing memory usage differences */ private const val DUMP_HPROF_TO_EXTERNAL = false @@ -81,10 +86,14 @@ open class AndroidPackageParsingTestBase { .filter { file -> file.name.endsWith(".apk") } .toList() } + .distinct() private val dummyUserState = mock(PackageUserState::class.java).apply { installed = true - Mockito.`when`(isAvailable(anyInt())).thenReturn(true) + whenever(isAvailable(anyInt())) { true } + whenever(isMatch(any<ComponentInfo>(), anyInt())) { true } + whenever(isMatch(anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), + anyString(), anyInt())) { true } } lateinit var oldPackages: List<PackageParser.Package> @@ -145,6 +154,7 @@ open class AndroidPackageParsingTestBase { private fun mockPkgSetting(aPkg: AndroidPackage) = mockThrowOnUnmocked<PackageSetting> { this.pkg = aPkg whenever(pkgState) { PackageStateUnserialized() } + whenever(readUserState(anyInt())) { dummyUserState } } } @@ -156,19 +166,10 @@ open class AndroidPackageParsingTestBase { // The following methods prepend "this." because @hide APIs can cause an IDE to auto-import // the R.attr constant instead of referencing the field in an attempt to fix the error. - /** - * Known exclusions: - * - [ApplicationInfo.credentialProtectedDataDir] - * - [ApplicationInfo.dataDir] - * - [ApplicationInfo.deviceProtectedDataDir] - * - [ApplicationInfo.processName] - * - [ApplicationInfo.publicSourceDir] - * - [ApplicationInfo.scanPublicSourceDir] - * - [ApplicationInfo.scanSourceDir] - * - [ApplicationInfo.sourceDir] - * These attributes used to be assigned post-package-parsing as part of another component, - * but are now adjusted directly inside [PackageImpl]. - */ + // It's difficult to comment out a line in a triple quoted string, so this is used instead + // to ignore specific fields. A comment is required to explain why a field was ignored. + private fun Any?.ignored(comment: String): String = "IGNORED" + protected fun ApplicationInfo.dumpToString() = """ appComponentFactory=${this.appComponentFactory} backupAgentName=${this.backupAgentName} @@ -179,22 +180,31 @@ open class AndroidPackageParsingTestBase { compatibleWidthLimitDp=${this.compatibleWidthLimitDp} compileSdkVersion=${this.compileSdkVersion} compileSdkVersionCodename=${this.compileSdkVersionCodename} + credentialProtectedDataDir=${this.credentialProtectedDataDir + .ignored("Deferred pre-R, but assigned immediately in R")} + crossProfile=${this.crossProfile.ignored("Added in R")} + dataDir=${this.dataDir.ignored("Deferred pre-R, but assigned immediately in R")} descriptionRes=${this.descriptionRes} + deviceProtectedDataDir=${this.deviceProtectedDataDir + .ignored("Deferred pre-R, but assigned immediately in R")} enabled=${this.enabled} enabledSetting=${this.enabledSetting} flags=${Integer.toBinaryString(this.flags)} fullBackupContent=${this.fullBackupContent} + gwpAsanMode=${this.gwpAsanMode.ignored("Added in R")} hiddenUntilInstalled=${this.hiddenUntilInstalled} icon=${this.icon} iconRes=${this.iconRes} installLocation=${this.installLocation} + labelRes=${this.labelRes} largestWidthLimitDp=${this.largestWidthLimitDp} logo=${this.logo} longVersionCode=${this.longVersionCode} + ${"".ignored("mHiddenApiPolicy is a private field")} manageSpaceActivityName=${this.manageSpaceActivityName} - maxAspectRatio.compareTo(that.maxAspectRatio)=${this.maxAspectRatio} - metaData=${this.metaData} - minAspectRatio.compareTo(that.minAspectRatio)=${this.minAspectRatio} + maxAspectRatio=${this.maxAspectRatio} + metaData=${this.metaData.dumpToString()} + minAspectRatio=${this.minAspectRatio} minSdkVersion=${this.minSdkVersion} name=${this.name} nativeLibraryDir=${this.nativeLibraryDir} @@ -206,18 +216,27 @@ open class AndroidPackageParsingTestBase { permission=${this.permission} primaryCpuAbi=${this.primaryCpuAbi} privateFlags=${Integer.toBinaryString(this.privateFlags)} + processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")} + publicSourceDir=${this.publicSourceDir + .ignored("Deferred pre-R, but assigned immediately in R")} requiresSmallestWidthDp=${this.requiresSmallestWidthDp} resourceDirs=${this.resourceDirs?.contentToString()} roundIconRes=${this.roundIconRes} - secondaryCpuAbi=${this.secondaryCpuAbi} - secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir} + scanPublicSourceDir=${this.scanPublicSourceDir + .ignored("Deferred pre-R, but assigned immediately in R")} + scanSourceDir=${this.scanSourceDir + .ignored("Deferred pre-R, but assigned immediately in R")} seInfo=${this.seInfo} seInfoUser=${this.seInfoUser} + secondaryCpuAbi=${this.secondaryCpuAbi} + secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir} sharedLibraryFiles=${this.sharedLibraryFiles?.contentToString()} sharedLibraryInfos=${this.sharedLibraryInfos} showUserIcon=${this.showUserIcon} + sourceDir=${this.sourceDir + .ignored("Deferred pre-R, but assigned immediately in R")} splitClassLoaderNames=${this.splitClassLoaderNames?.contentToString()} - splitDependencies=${this.splitDependencies} + splitDependencies=${this.splitDependencies.dumpToString()} splitNames=${this.splitNames?.contentToString()} splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()} splitSourceDirs=${this.splitSourceDirs?.contentToString()} @@ -226,8 +245,8 @@ open class AndroidPackageParsingTestBase { targetSdkVersion=${this.targetSdkVersion} taskAffinity=${this.taskAffinity} theme=${this.theme} - uid=${this.uid} uiOptions=${this.uiOptions} + uid=${this.uid} versionCode=${this.versionCode} volumeUuid=${this.volumeUuid} zygotePreloadName=${this.zygotePreloadName} @@ -241,19 +260,27 @@ open class AndroidPackageParsingTestBase { """.trimIndent() protected fun InstrumentationInfo.dumpToString() = """ + banner=${this.banner} credentialProtectedDataDir=${this.credentialProtectedDataDir} dataDir=${this.dataDir} deviceProtectedDataDir=${this.deviceProtectedDataDir} functionalTest=${this.functionalTest} handleProfiling=${this.handleProfiling} + icon=${this.icon} + labelRes=${this.labelRes} + logo=${this.logo} + metaData=${this.metaData} + name=${this.name} nativeLibraryDir=${this.nativeLibraryDir} + nonLocalizedLabel=${this.nonLocalizedLabel} + packageName=${this.packageName} primaryCpuAbi=${this.primaryCpuAbi} publicSourceDir=${this.publicSourceDir} secondaryCpuAbi=${this.secondaryCpuAbi} secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir} + showUserIcon=${this.showUserIcon} sourceDir=${this.sourceDir} - splitDependencies=${this.splitDependencies.sequence() - .map { it.first to it.second?.contentToString() }.joinToString()} + splitDependencies=${this.splitDependencies.dumpToString()} splitNames=${this.splitNames?.contentToString()} splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()} splitSourceDirs=${this.splitSourceDirs?.contentToString()} @@ -262,25 +289,40 @@ open class AndroidPackageParsingTestBase { """.trimIndent() protected fun ActivityInfo.dumpToString() = """ + banner=${this.banner} colorMode=${this.colorMode} configChanges=${this.configChanges} + descriptionRes=${this.descriptionRes} + directBootAware=${this.directBootAware} documentLaunchMode=${this.documentLaunchMode} + enabled=${this.enabled} + exported=${this.exported} flags=${Integer.toBinaryString(this.flags)} + icon=${this.icon} + labelRes=${this.labelRes} launchMode=${this.launchMode} launchToken=${this.launchToken} lockTaskLaunchMode=${this.lockTaskLaunchMode} + logo=${this.logo} maxAspectRatio=${this.maxAspectRatio} maxRecents=${this.maxRecents} + metaData=${this.metaData.dumpToString()} minAspectRatio=${this.minAspectRatio} + name=${this.name} + nonLocalizedLabel=${this.nonLocalizedLabel} + packageName=${this.packageName} parentActivityName=${this.parentActivityName} permission=${this.permission} - persistableMode=${this.persistableMode} - privateFlags=${Integer.toBinaryString(this.privateFlags)} + persistableMode=${this.persistableMode.ignored("Could be dropped pre-R, fixed in R")} + privateFlags=${this.privateFlags} + processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")} requestedVrComponent=${this.requestedVrComponent} resizeMode=${this.resizeMode} rotationAnimation=${this.rotationAnimation} screenOrientation=${this.screenOrientation} + showUserIcon=${this.showUserIcon} softInputMode=${this.softInputMode} + splitName=${this.splitName} targetActivity=${this.targetActivity} taskAffinity=${this.taskAffinity} theme=${this.theme} @@ -300,30 +342,77 @@ open class AndroidPackageParsingTestBase { protected fun PermissionInfo.dumpToString() = """ backgroundPermission=${this.backgroundPermission} + banner=${this.banner} descriptionRes=${this.descriptionRes} flags=${Integer.toBinaryString(this.flags)} group=${this.group} + icon=${this.icon} + labelRes=${this.labelRes} + logo=${this.logo} + metaData=${this.metaData.dumpToString()} + name=${this.name} nonLocalizedDescription=${this.nonLocalizedDescription} + nonLocalizedLabel=${this.nonLocalizedLabel} + packageName=${this.packageName} protectionLevel=${this.protectionLevel} requestRes=${this.requestRes} + showUserIcon=${this.showUserIcon} """.trimIndent() protected fun ProviderInfo.dumpToString() = """ + applicationInfo=${this.applicationInfo.ignored("Already checked")} authority=${this.authority} + banner=${this.banner} + descriptionRes=${this.descriptionRes} + directBootAware=${this.directBootAware} + enabled=${this.enabled} + exported=${this.exported} flags=${Integer.toBinaryString(this.flags)} forceUriPermissions=${this.forceUriPermissions} grantUriPermissions=${this.grantUriPermissions} + icon=${this.icon} initOrder=${this.initOrder} isSyncable=${this.isSyncable} + labelRes=${this.labelRes} + logo=${this.logo} + metaData=${this.metaData.dumpToString()} multiprocess=${this.multiprocess} + name=${this.name} + nonLocalizedLabel=${this.nonLocalizedLabel} + packageName=${this.packageName} pathPermissions=${this.pathPermissions?.joinToString { "readPermission=${it.readPermission}\nwritePermission=${it.writePermission}" }} + processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")} readPermission=${this.readPermission} + showUserIcon=${this.showUserIcon} + splitName=${this.splitName} uriPermissionPatterns=${this.uriPermissionPatterns?.contentToString()} writePermission=${this.writePermission} """.trimIndent() + protected fun ServiceInfo.dumpToString() = """ + applicationInfo=${this.applicationInfo.ignored("Already checked")} + banner=${this.banner} + descriptionRes=${this.descriptionRes} + directBootAware=${this.directBootAware} + enabled=${this.enabled} + exported=${this.exported} + flags=${Integer.toBinaryString(this.flags)} + icon=${this.icon} + labelRes=${this.labelRes} + logo=${this.logo} + mForegroundServiceType"${this.mForegroundServiceType} + metaData=${this.metaData.dumpToString()} + name=${this.name} + nonLocalizedLabel=${this.nonLocalizedLabel} + packageName=${this.packageName} + permission=${this.permission} + processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")} + showUserIcon=${this.showUserIcon} + splitName=${this.splitName} + """.trimIndent() + protected fun ConfigurationInfo.dumpToString() = """ reqGlEsVersion=${this.reqGlEsVersion} reqInputFeatures=${this.reqInputFeatures} @@ -333,8 +422,10 @@ open class AndroidPackageParsingTestBase { """.trimIndent() protected fun PackageInfo.dumpToString() = """ - activities=${this.activities?.joinToString { it.dumpToString() }} - applicationInfo=${this.applicationInfo.dumpToString()} + activities=${this.activities?.joinToString { it.dumpToString() } + .ignored("Checked separately in test")} + applicationInfo=${this.applicationInfo.dumpToString() + .ignored("Checked separately in test")} baseRevisionCode=${this.baseRevisionCode} compileSdkVersion=${this.compileSdkVersion} compileSdkVersionCodename=${this.compileSdkVersionCodename} @@ -356,15 +447,18 @@ open class AndroidPackageParsingTestBase { overlayTarget=${this.overlayTarget} packageName=${this.packageName} permissions=${this.permissions?.joinToString { it.dumpToString() }} - providers=${this.providers?.joinToString { it.dumpToString() }} - receivers=${this.receivers?.joinToString { it.dumpToString() }} + providers=${this.providers?.joinToString { it.dumpToString() } + .ignored("Checked separately in test")} + receivers=${this.receivers?.joinToString { it.dumpToString() } + .ignored("Checked separately in test")} reqFeatures=${this.reqFeatures?.joinToString { it.dumpToString() }} requestedPermissions=${this.requestedPermissions?.contentToString()} requestedPermissionsFlags=${this.requestedPermissionsFlags?.contentToString()} requiredAccountType=${this.requiredAccountType} requiredForAllUsers=${this.requiredForAllUsers} restrictedAccountType=${this.restrictedAccountType} - services=${this.services?.contentToString()} + services=${this.services?.joinToString { it.dumpToString() } + .ignored("Checked separately in test")} sharedUserId=${this.sharedUserId} sharedUserLabel=${this.sharedUserLabel} signatures=${this.signatures?.joinToString { it.toCharsString() }} @@ -378,11 +472,17 @@ open class AndroidPackageParsingTestBase { versionName=${this.versionName} """.trimIndent() - @Suppress("unused") - private fun <T> SparseArray<T>.sequence(): Sequence<Pair<Int, T>> { - var index = 0 - return generateSequence { - index++.takeIf { it < size() }?.let { keyAt(it) to valueAt(index) } + private fun Bundle?.dumpToString() = this?.keySet()?.associateWith { get(it) }?.toString() + + private fun <T> SparseArray<T>?.dumpToString(): String { + if (this == null) { + return "EMPTY" + } + + val list = mutableListOf<Pair<Int, T>>() + for (index in (0 until size())) { + list += keyAt(index) to valueAt(index) } + return list.toString() } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java index e1f39137e618..c9c31bfcde08 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java @@ -29,6 +29,10 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.Notification.Builder; import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; + import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.test.suitebuilder.annotation.SmallTest; @@ -79,6 +83,37 @@ public class BadgeExtractorTest extends UiServiceTestCase { return r; } + private NotificationRecord getNotificationRecordWithBubble(boolean suppressNotif) { + NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_UNSPECIFIED); + channel.setShowBadge(/* showBadge */ true); + when(mConfig.getNotificationChannel(mPkg, mUid, "a", false)).thenReturn(channel); + + Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( + PendingIntent.getActivity(mContext, 0, new Intent(), 0), + Icon.createWithResource("", 0)).build(); + + int flags = metadata.getFlags(); + if (suppressNotif) { + flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + } else { + flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + } + metadata.setFlags(flags); + + final Builder builder = new Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .setDefaults(Notification.DEFAULT_SOUND) + .setBubbleMetadata(metadata); + + Notification n = builder.build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, mId, mTag, mUid, + mPid, n, mUser, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(getContext(), sbn, channel); + return r; + } + // // Tests // @@ -154,6 +189,20 @@ public class BadgeExtractorTest extends UiServiceTestCase { } @Test + public void testHideNotifOverridesYes() throws Exception { + BadgeExtractor extractor = new BadgeExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.badgingEnabled(mUser)).thenReturn(true); + when(mConfig.canShowBadge(mPkg, mUid)).thenReturn(true); + NotificationRecord r = getNotificationRecordWithBubble(/* suppressNotif */ true); + + extractor.process(r); + + assertFalse(r.canShowBadge()); + } + + @Test public void testDndOverridesYes() { BadgeExtractor extractor = new BadgeExtractor(); extractor.setConfig(mConfig); |