diff options
170 files changed, 5230 insertions, 2414 deletions
diff --git a/api/current.txt b/api/current.txt index 467aa32a7d67..41a74cfae20a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -58326,9 +58326,9 @@ package android.webkit { method public abstract void setAllowFileAccess(boolean); method @Deprecated public abstract void setAllowFileAccessFromFileURLs(boolean); method @Deprecated public abstract void setAllowUniversalAccessFromFileURLs(boolean); - method public abstract void setAppCacheEnabled(boolean); + method @Deprecated public abstract void setAppCacheEnabled(boolean); method @Deprecated public abstract void setAppCacheMaxSize(long); - method public abstract void setAppCachePath(String); + method @Deprecated public abstract void setAppCachePath(String); method public abstract void setBlockNetworkImage(boolean); method public abstract void setBlockNetworkLoads(boolean); method public abstract void setBuiltInZoomControls(boolean); diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index a1278f358380..ecb95bd11c2f 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -1306,7 +1306,7 @@ status_t BootAnimation::TimeCheckThread::readyToRun() { if (mSystemWd < 0) { close(mInotifyFd); mInotifyFd = -1; - SLOGE("Could not add watch for %s", SYSTEM_DATA_DIR_PATH); + SLOGE("Could not add watch for %s: %s", SYSTEM_DATA_DIR_PATH, strerror(errno)); return NO_INIT; } diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 71dd4dd49d8e..1d9f20e8d3c3 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -439,6 +439,7 @@ message Atom { app_permission_groups_fragment_auto_revoke_action = 273 [(module) = "permissioncontroller"]; EvsUsageStatsReported evs_usage_stats_reported = 274 [(module) = "evs"]; + AudioPowerUsageDataReported audio_power_usage_data_reported = 275; SdkExtensionStatus sdk_extension_status = 354; // StatsdStats tracks platform atoms with ids upto 500. @@ -9689,3 +9690,59 @@ message EvsUsageStatsReported { // The duration of the service optional int64 duration_millis = 10; } + +/** + * Logs audio power usage stats. + * + * Pushed from: + * frameworks/av/services/mediametrics/AudioPowerUsage.cpp + */ +message AudioPowerUsageDataReported { + /** + * Device used for input/output + * + * All audio devices please refer to below file: + * system/media/audio/include/system/audio-base.h + * + * Define our own enum values because we don't report all audio devices. + * Currently, we only report built-in audio devices such as handset, speaker, + * built-in mics, common audio devices such as wired headset, usb headset + * and bluetooth devices. + */ + enum AudioDevice { + OUTPUT_EARPIECE = 0x1; // handset + OUTPUT_SPEAKER = 0x2; // dual speaker + OUTPUT_WIRED_HEADSET = 0x4; // 3.5mm headset + OUTPUT_USB_HEADSET = 0x8; // usb headset + OUTPUT_BLUETOOTH_SCO = 0x10; // bluetooth sco + OUTPUT_BLUETOOTH_A2DP = 0x20; // a2dp + OUTPUT_SPEAKER_SAFE = 0x40; // bottom speaker + + INPUT_DEVICE_BIT = 0x40000000; // non-negative positive int32. + INPUT_BUILTIN_MIC = 0x40000001; // buildin mic + INPUT_BUILTIN_BACK_MIC = 0x40000002; // buildin back mic + INPUT_WIRED_HEADSET_MIC = 0x40000004; // 3.5mm headset mic + INPUT_USB_HEADSET_MIC = 0x40000008; // usb headset mic + INPUT_BLUETOOTH_SCO = 0x40000010; // bluetooth sco mic + } + optional AudioDevice audio_device = 1; + + // Duration of the audio in seconds + optional int32 duration_secs = 2; + + // Average volume (0 ... 1.0) + optional float average_volume = 3; + + enum AudioType { + UNKNOWN_TYPE = 0; + VOICE_CALL_TYPE = 1; // voice call + VOIP_CALL_TYPE = 2; // voip call, including uplink and downlink + MEDIA_TYPE = 3; // music and system sound + RINGTONE_NOTIFICATION_TYPE = 4; // ringtone and notification + ALARM_TYPE = 5; // alarm type + // record type + CAMCORDER_TYPE = 6; // camcorder + RECORD_TYPE = 7; // other recording + } + optional AudioType type = 4; +} diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp index 8ec0173ce461..f56fa6221bc9 100644 --- a/cmds/statsd/src/logd/LogEvent.cpp +++ b/cmds/statsd/src/logd/LogEvent.cpp @@ -66,15 +66,6 @@ using std::vector; #define ATTRIBUTION_CHAIN_TYPE 0x09 #define ERROR_TYPE 0x0F -LogEvent::LogEvent(const LogEvent& event) { - mTagId = event.mTagId; - mLogUid = event.mLogUid; - mLogPid = event.mLogPid; - mElapsedTimestampNs = event.mElapsedTimestampNs; - mLogdTimestampNs = event.mLogdTimestampNs; - mValues = event.mValues; -} - LogEvent::LogEvent(int32_t uid, int32_t pid) : mLogdTimestampNs(time(nullptr)), mLogUid(uid), mLogPid(pid) { } diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h index 53fb5d93e3ac..a5f24603585a 100644 --- a/cmds/statsd/src/logd/LogEvent.h +++ b/cmds/statsd/src/logd/LogEvent.h @@ -216,7 +216,7 @@ private: /** * Only use this if copy is absolutely needed. */ - LogEvent(const LogEvent&); + LogEvent(const LogEvent&) = default; void parseInt32(int32_t* pos, int32_t depth, bool* last, uint8_t numAnnotations); void parseInt64(int32_t* pos, int32_t depth, bool* last, uint8_t numAnnotations); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index bfae632593fb..af5fafbc93d4 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -17,6 +17,8 @@ package android.app; import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.inMultiWindowMode; import static android.os.Process.myUid; import static java.lang.Character.MIN_VALUE; @@ -947,9 +949,8 @@ public class Activity extends ContextThemeWrapper /** @hide */ boolean mEnterAnimationComplete; - /** Track last dispatched multi-window and PiP mode to client, internal debug purpose **/ - private Boolean mLastDispatchedIsInMultiWindowMode; - private Boolean mLastDispatchedIsInPictureInPictureMode; + private boolean mIsInMultiWindowMode; + private boolean mIsInPictureInPictureMode; private final WindowControllerCallback mWindowControllerCallback = new WindowControllerCallback() { @@ -2748,7 +2749,7 @@ public class Activity extends ContextThemeWrapper * @return True if the activity is in multi-window mode. */ public boolean isInMultiWindowMode() { - return mLastDispatchedIsInMultiWindowMode == Boolean.TRUE; + return mIsInMultiWindowMode; } /** @@ -2791,7 +2792,7 @@ public class Activity extends ContextThemeWrapper * @return True if the activity is in picture-in-picture mode. */ public boolean isInPictureInPictureMode() { - return mLastDispatchedIsInPictureInPictureMode == Boolean.TRUE; + return mIsInPictureInPictureMode; } /** @@ -7142,14 +7143,19 @@ public class Activity extends ContextThemeWrapper writer.print(mResumed); writer.print(" mStopped="); writer.print(mStopped); writer.print(" mFinished="); writer.println(mFinished); - writer.print(innerPrefix); writer.print("mLastDispatchedIsInMultiWindowMode="); - writer.print(mLastDispatchedIsInMultiWindowMode); - writer.print(" mLastDispatchedIsInPictureInPictureMode="); - writer.println(mLastDispatchedIsInPictureInPictureMode); + writer.print(innerPrefix); writer.print("mIsInMultiWindowMode="); + writer.print(mIsInMultiWindowMode); + writer.print(" mIsInPictureInPictureMode="); + writer.println(mIsInPictureInPictureMode); writer.print(innerPrefix); writer.print("mChangingConfigurations="); writer.println(mChangingConfigurations); writer.print(innerPrefix); writer.print("mCurrentConfig="); writer.println(mCurrentConfig); + if (getResources().hasOverrideDisplayAdjustments()) { + writer.print(innerPrefix); + writer.print("FixedRotationAdjustments="); + writer.println(getResources().getDisplayAdjustments().getFixedRotationAdjustments()); + } mFragments.dumpLoaders(innerPrefix, fd, writer, args); mFragments.getFragmentManager().dump(innerPrefix, fd, writer, args); @@ -7977,6 +7983,11 @@ public class Activity extends ContextThemeWrapper final void performCreate(Bundle icicle, PersistableBundle persistentState) { dispatchActivityPreCreated(icicle); mCanEnterPictureInPicture = true; + // initialize mIsInMultiWindowMode and mIsInPictureInPictureMode before onCreate + final int windowingMode = getResources().getConfiguration().windowConfiguration + .getWindowingMode(); + mIsInMultiWindowMode = inMultiWindowMode(windowingMode); + mIsInPictureInPictureMode = windowingMode == WINDOWING_MODE_PINNED; restoreHasCurrentPermissionRequest(icicle); if (persistentState != null) { onCreate(icicle, persistentState); @@ -8245,7 +8256,7 @@ public class Activity extends ContextThemeWrapper if (mWindow != null) { mWindow.onMultiWindowModeChanged(); } - mLastDispatchedIsInMultiWindowMode = isInMultiWindowMode; + mIsInMultiWindowMode = isInMultiWindowMode; onMultiWindowModeChanged(isInMultiWindowMode, newConfig); } @@ -8258,7 +8269,7 @@ public class Activity extends ContextThemeWrapper if (mWindow != null) { mWindow.onPictureInPictureModeChanged(isInPictureInPictureMode); } - mLastDispatchedIsInPictureInPictureMode = isInPictureInPictureMode; + mIsInPictureInPictureMode = isInPictureInPictureMode; onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index b45705924910..eea1d69b6326 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -156,6 +156,8 @@ import android.util.proto.ProtoOutputStream; import android.view.Choreographer; import android.view.ContextThemeWrapper; import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.DisplayAdjustments.FixedRotationAdjustments; import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewDebug; @@ -215,6 +217,7 @@ import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; final class RemoteServiceException extends AndroidRuntimeException { public RemoteServiceException(String msg) { @@ -405,6 +408,9 @@ public final class ActivityThread extends ClientTransactionHandler { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final ResourcesManager mResourcesManager; + /** The active adjustments that override the {@link DisplayAdjustments} in resources. */ + private ArrayList<Pair<IBinder, Consumer<DisplayAdjustments>>> mActiveRotationAdjustments; + // Registry of remote cancellation transports pending a reply with reply handles. @GuardedBy("this") private @Nullable Map<SafeCancellationTransport, CancellationSignal> mRemoteCancellations; @@ -541,6 +547,12 @@ public final class ActivityThread extends ClientTransactionHandler { @UnsupportedAppUsage boolean mPreserveWindow; + /** + * If non-null, the activity is launching with a specified rotation, the adjustments should + * be consumed before activity creation. + */ + FixedRotationAdjustments mPendingFixedRotationAdjustments; + @LifecycleState private int mLifecycleState = PRE_ON_CREATE; @@ -557,7 +569,7 @@ public final class ActivityThread extends ClientTransactionHandler { PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, boolean isForward, ProfilerInfo profilerInfo, ClientTransactionHandler client, - IBinder assistToken) { + IBinder assistToken, FixedRotationAdjustments fixedRotationAdjustments) { this.token = token; this.assistToken = assistToken; this.ident = ident; @@ -575,6 +587,7 @@ public final class ActivityThread extends ClientTransactionHandler { this.overrideConfig = overrideConfig; this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo, compatInfo); + mPendingFixedRotationAdjustments = fixedRotationAdjustments; init(); } @@ -3233,6 +3246,44 @@ public final class ActivityThread extends ClientTransactionHandler { sendMessage(H.CLEAN_UP_CONTEXT, cci); } + @Override + public void handleFixedRotationAdjustments(@NonNull IBinder token, + @Nullable FixedRotationAdjustments fixedRotationAdjustments) { + final Consumer<DisplayAdjustments> override = fixedRotationAdjustments != null + ? displayAdjustments -> displayAdjustments.setFixedRotationAdjustments( + fixedRotationAdjustments) + : 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; + } + + // Apply the last override to application resources for compatibility. Because the Resources + // of Display can be from application, e.g. + // applicationContext.getSystemService(DisplayManager.class).getDisplay(displayId) + // and the deprecated usage: + // applicationContext.getSystemService(WindowManager.class).getDefaultDisplay(); + final Consumer<DisplayAdjustments> appOverride; + if (mActiveRotationAdjustments == null) { + mActiveRotationAdjustments = new ArrayList<>(2); + } + if (override != null) { + mActiveRotationAdjustments.add(Pair.create(token, override)); + appOverride = override; + } else { + mActiveRotationAdjustments.removeIf(adjustmentsPair -> adjustmentsPair.first == token); + appOverride = mActiveRotationAdjustments.isEmpty() + ? null + : mActiveRotationAdjustments.get(mActiveRotationAdjustments.size() - 1).second; + } + mInitialApplication.getResources().overrideDisplayAdjustments(appOverride); + } + /** Core implementation of activity launch. */ private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; @@ -3446,6 +3497,13 @@ public final class ActivityThread extends ClientTransactionHandler { ContextImpl appContext = ContextImpl.createActivityContext( this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig); + // 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); + r.mPendingFixedRotationAdjustments = null; + } + final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); // For debugging purposes, if the activity's package name contains the value of // the "debug.use-second-display" system property as a substring, then show @@ -7455,7 +7513,15 @@ public final class ActivityThread extends ClientTransactionHandler { try { super.rename(oldPath, newPath); } catch (ErrnoException e) { - if (e.errno == OsConstants.EXDEV && oldPath.startsWith("/storage/")) { + // On emulated volumes, we have bind mounts for /Android/data and + // /Android/obb, which prevents move from working across those directories + // and other directories on the filesystem. To work around that, try to + // recover by doing a copy instead. + // Note that we only do this for "/storage/emulated", because public volumes + // don't have these bind mounts, neither do private volumes that are not + // the primary storage. + if (e.errno == OsConstants.EXDEV && oldPath.startsWith("/storage/emulated") + && newPath.startsWith("/storage/emulated")) { Log.v(TAG, "Recovering failed rename " + oldPath + " to " + newPath); try { Files.move(new File(oldPath).toPath(), new File(newPath).toPath(), diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 83465b0f8d36..2df756e80fde 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -25,6 +25,7 @@ import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.IBinder; import android.util.MergedConfiguration; +import android.view.DisplayAdjustments.FixedRotationAdjustments; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.ReferrerIntent; @@ -167,6 +168,10 @@ public abstract class ClientTransactionHandler { /** Deliver app configuration change notification. */ public abstract void handleConfigurationChanged(Configuration config); + /** Apply addition adjustments to override display information. */ + public abstract void handleFixedRotationAdjustments(IBinder token, + FixedRotationAdjustments fixedRotationAdjustments); + /** * Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the * provided token. diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 106f8ac92d05..1aae04db32d0 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -59,6 +59,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.WeakHashMap; +import java.util.function.Consumer; import java.util.function.Predicate; /** @hide */ @@ -1296,6 +1297,35 @@ public class ResourcesManager { } } + /** + * Overrides the display adjustments of all resources which are associated with the given token. + * + * @param token The token that owns the resources. + * @param override The operation to override the existing display adjustments. If it is null, + * the override adjustments will be cleared. + * @return {@code true} if the override takes effect. + */ + public boolean overrideTokenDisplayAdjustments(IBinder token, + @Nullable Consumer<DisplayAdjustments> override) { + boolean handled = false; + synchronized (this) { + final ActivityResources tokenResources = mActivityResourceReferences.get(token); + if (tokenResources == null) { + return false; + } + final ArrayList<WeakReference<Resources>> resourcesRefs = + tokenResources.activityResources; + for (int i = resourcesRefs.size() - 1; i >= 0; i--) { + final Resources res = resourcesRefs.get(i).get(); + if (res != null) { + res.overrideDisplayAdjustments(override); + handled = true; + } + } + } + return handled; + } + private class UpdateHandler implements Resources.UpdateCallbacks { /** diff --git a/core/java/android/app/servertransaction/FixedRotationAdjustmentsItem.java b/core/java/android/app/servertransaction/FixedRotationAdjustmentsItem.java new file mode 100644 index 000000000000..6183d5f28fdb --- /dev/null +++ b/core/java/android/app/servertransaction/FixedRotationAdjustmentsItem.java @@ -0,0 +1,112 @@ +/* + * Copyright 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.view.DisplayAdjustments.FixedRotationAdjustments; + +import java.util.Objects; + +/** + * The request to update display adjustments for a rotated activity or window token. + * @hide + */ +public class FixedRotationAdjustmentsItem extends ClientTransactionItem { + + /** The token who may have {@link android.content.res.Resources}. */ + private IBinder mToken; + + /** + * The adjustments for the display adjustments of resources. If it is null, the existing + * rotation adjustments will be dropped to restore natural state. + */ + private FixedRotationAdjustments mFixedRotationAdjustments; + + private FixedRotationAdjustmentsItem() {} + + /** Obtain an instance initialized with provided params. */ + public static FixedRotationAdjustmentsItem obtain(IBinder token, + FixedRotationAdjustments fixedRotationAdjustments) { + FixedRotationAdjustmentsItem instance = + ObjectPool.obtain(FixedRotationAdjustmentsItem.class); + if (instance == null) { + instance = new FixedRotationAdjustmentsItem(); + } + instance.mToken = token; + instance.mFixedRotationAdjustments = fixedRotationAdjustments; + + return instance; + } + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handleFixedRotationAdjustments(mToken, mFixedRotationAdjustments); + } + + @Override + public void recycle() { + mToken = null; + mFixedRotationAdjustments = null; + ObjectPool.recycle(this); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mToken); + dest.writeTypedObject(mFixedRotationAdjustments, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final FixedRotationAdjustmentsItem other = (FixedRotationAdjustmentsItem) o; + return Objects.equals(mToken, other.mToken) + && Objects.equals(mFixedRotationAdjustments, other.mFixedRotationAdjustments); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Objects.hashCode(mToken); + result = 31 * result + Objects.hashCode(mFixedRotationAdjustments); + return result; + } + + private FixedRotationAdjustmentsItem(Parcel in) { + mToken = in.readStrongBinder(); + mFixedRotationAdjustments = in.readTypedObject(FixedRotationAdjustments.CREATOR); + } + + public static final Creator<FixedRotationAdjustmentsItem> CREATOR = + new Creator<FixedRotationAdjustmentsItem>() { + public FixedRotationAdjustmentsItem createFromParcel(Parcel in) { + return new FixedRotationAdjustmentsItem(in); + } + + public FixedRotationAdjustmentsItem[] newArray(int size) { + return new FixedRotationAdjustmentsItem[size]; + } + }; +} diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java index 9ab6e7fc9f58..2e7b6262c785 100644 --- a/core/java/android/app/servertransaction/LaunchActivityItem.java +++ b/core/java/android/app/servertransaction/LaunchActivityItem.java @@ -33,6 +33,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.PersistableBundle; import android.os.Trace; +import android.view.DisplayAdjustments.FixedRotationAdjustments; import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; @@ -64,6 +65,7 @@ public class LaunchActivityItem extends ClientTransactionItem { private boolean mIsForward; private ProfilerInfo mProfilerInfo; private IBinder mAssistToken; + private FixedRotationAdjustments mFixedRotationAdjustments; @Override public void preExecute(ClientTransactionHandler client, IBinder token) { @@ -79,7 +81,7 @@ public class LaunchActivityItem extends ClientTransactionItem { ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo, mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState, mPendingResults, mPendingNewIntents, mIsForward, - mProfilerInfo, client, mAssistToken); + mProfilerInfo, client, mAssistToken, mFixedRotationAdjustments); client.handleLaunchActivity(r, pendingActions, null /* customIntent */); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -101,14 +103,14 @@ public class LaunchActivityItem extends ClientTransactionItem { String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, boolean isForward, ProfilerInfo profilerInfo, - IBinder assistToken) { + IBinder assistToken, FixedRotationAdjustments fixedRotationAdjustments) { LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class); if (instance == null) { instance = new LaunchActivityItem(); } setValues(instance, intent, ident, info, curConfig, overrideConfig, compatInfo, referrer, voiceInteractor, procState, state, persistentState, pendingResults, - pendingNewIntents, isForward, profilerInfo, assistToken); + pendingNewIntents, isForward, profilerInfo, assistToken, fixedRotationAdjustments); return instance; } @@ -116,7 +118,7 @@ public class LaunchActivityItem extends ClientTransactionItem { @Override public void recycle() { setValues(this, null, 0, null, null, null, null, null, null, 0, null, null, null, null, - false, null, null); + false, null, null, null); ObjectPool.recycle(this); } @@ -142,6 +144,7 @@ public class LaunchActivityItem extends ClientTransactionItem { dest.writeBoolean(mIsForward); dest.writeTypedObject(mProfilerInfo, flags); dest.writeStrongBinder(mAssistToken); + dest.writeTypedObject(mFixedRotationAdjustments, flags); } /** Read from Parcel. */ @@ -156,7 +159,8 @@ public class LaunchActivityItem extends ClientTransactionItem { in.createTypedArrayList(ResultInfo.CREATOR), in.createTypedArrayList(ReferrerIntent.CREATOR), in.readBoolean(), in.readTypedObject(ProfilerInfo.CREATOR), - in.readStrongBinder()); + in.readStrongBinder(), + in.readTypedObject(FixedRotationAdjustments.CREATOR)); } public static final @android.annotation.NonNull Creator<LaunchActivityItem> CREATOR = @@ -192,7 +196,8 @@ public class LaunchActivityItem extends ClientTransactionItem { && Objects.equals(mPendingNewIntents, other.mPendingNewIntents) && mIsForward == other.mIsForward && Objects.equals(mProfilerInfo, other.mProfilerInfo) - && Objects.equals(mAssistToken, other.mAssistToken); + && Objects.equals(mAssistToken, other.mAssistToken) + && Objects.equals(mFixedRotationAdjustments, other.mFixedRotationAdjustments); } @Override @@ -212,6 +217,7 @@ public class LaunchActivityItem extends ClientTransactionItem { result = 31 * result + (mIsForward ? 1 : 0); result = 31 * result + Objects.hashCode(mProfilerInfo); result = 31 * result + Objects.hashCode(mAssistToken); + result = 31 * result + Objects.hashCode(mFixedRotationAdjustments); return result; } @@ -247,7 +253,7 @@ public class LaunchActivityItem extends ClientTransactionItem { + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults + ",pendingNewIntents=" + mPendingNewIntents + ",profilerInfo=" + mProfilerInfo - + " assistToken=" + mAssistToken + + ",assistToken=" + mAssistToken + ",rotationAdj=" + mFixedRotationAdjustments + "}"; } @@ -257,7 +263,8 @@ public class LaunchActivityItem extends ClientTransactionItem { CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, - boolean isForward, ProfilerInfo profilerInfo, IBinder assistToken) { + boolean isForward, ProfilerInfo profilerInfo, IBinder assistToken, + FixedRotationAdjustments fixedRotationAdjustments) { instance.mIntent = intent; instance.mIdent = ident; instance.mInfo = info; @@ -274,5 +281,6 @@ public class LaunchActivityItem extends ClientTransactionItem { instance.mIsForward = isForward; instance.mProfilerInfo = profilerInfo; instance.mAssistToken = assistToken; + instance.mFixedRotationAdjustments = fixedRotationAdjustments; } } diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 37baae35734b..010589617e09 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -51,6 +51,9 @@ interface IPackageInstaller { void uninstall(in VersionedPackage versionedPackage, String callerPackageName, int flags, in IntentSender statusReceiver, int userId); + void uninstallExistingPackage(in VersionedPackage versionedPackage, String callerPackageName, + in IntentSender statusReceiver, int userId); + void installExistingPackage(String packageName, int installFlags, int installReason, in IntentSender statusReceiver, int userId, in List<String> whiteListedPermissions); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 8bebafff37f0..f257326904fd 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -235,6 +235,16 @@ interface IPackageManager { void deletePackageVersioned(in VersionedPackage versionedPackage, IPackageDeleteObserver2 observer, int userId, int flags); + /** + * Delete a package for a specific user. + * + * @param versionedPackage The package to delete. + * @param observer a callback to use to notify when the package deletion in finished. + * @param userId the id of the user for whom to delete the package + */ + void deleteExistingPackageAsUser(in VersionedPackage versionedPackage, + IPackageDeleteObserver2 observer, int userId); + @UnsupportedAppUsage String getInstallerPackageName(in String packageName); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 85a3986a65f9..ed75504529b9 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -720,6 +720,27 @@ public class PackageInstaller { } } + /** + * Uninstall the given package for the user for which this installer was created if the package + * will still exist for other users on the device. + * + * @param packageName The package to install. + * @param statusReceiver Where to deliver the result. + * + * {@hide} + */ + @RequiresPermission(Manifest.permission.DELETE_PACKAGES) + public void uninstallExistingPackage(@NonNull String packageName, + @Nullable IntentSender statusReceiver) { + Objects.requireNonNull(packageName, "packageName cannot be null"); + try { + mInstaller.uninstallExistingPackage( + new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), + mInstallerPackageName, statusReceiver, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** {@hide} */ @SystemApi diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index c399bc72e438..0f1c876a1133 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -81,6 +81,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Class for accessing an application's resources. This sits on top of the @@ -140,6 +141,9 @@ public class Resources { @UnsupportedAppUsage private DrawableInflater mDrawableInflater; + /** Used to override the returned adjustments of {@link #getDisplayAdjustments}. */ + private DisplayAdjustments mOverrideDisplayAdjustments; + /** Lock object used to protect access to {@link #mTmpValue}. */ private final Object mTmpValueLock = new Object(); @@ -2055,10 +2059,41 @@ public class Resources { /** @hide */ @UnsupportedAppUsage public DisplayAdjustments getDisplayAdjustments() { + final DisplayAdjustments overrideDisplayAdjustments = mOverrideDisplayAdjustments; + if (overrideDisplayAdjustments != null) { + return overrideDisplayAdjustments; + } return mResourcesImpl.getDisplayAdjustments(); } /** + * Customize the display adjustments based on the current one in {@link #mResourcesImpl}, in + * order to isolate the effect with other instances of {@link Resource} that may share the same + * instance of {@link ResourcesImpl}. + * + * @param override The operation to override the existing display adjustments. If it is null, + * the override adjustments will be cleared. + * @hide + */ + public void overrideDisplayAdjustments(@Nullable Consumer<DisplayAdjustments> override) { + if (override != null) { + mOverrideDisplayAdjustments = new DisplayAdjustments( + mResourcesImpl.getDisplayAdjustments()); + override.accept(mOverrideDisplayAdjustments); + } else { + mOverrideDisplayAdjustments = null; + } + } + + /** + * Return {@code true} if the override display adjustments have been set. + * @hide + */ + public boolean hasOverrideDisplayAdjustments() { + return mOverrideDisplayAdjustments != null; + } + + /** * Return the current configuration that is in effect for this resource * object. The returned object should be treated as read-only. * diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 6905f83104cd..d071037409a7 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2182,11 +2182,13 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>By using this control, the application gains a simpler way to control zoom, which can * be a combination of optical and digital zoom. For example, a multi-camera system may * contain more than one lens with different focal lengths, and the user can use optical - * zoom by switching between lenses. Using zoomRatio has benefits in the scenarios below: - * <em> Zooming in from a wide-angle lens to a telephoto lens: A floating-point ratio provides - * better precision compared to an integer value of {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}. - * </em> Zooming out from a wide lens to an ultrawide lens: zoomRatio supports zoom-out whereas - * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} doesn't.</p> + * zoom by switching between lenses. Using zoomRatio has benefits in the scenarios below:</p> + * <ul> + * <li>Zooming in from a wide-angle lens to a telephoto lens: A floating-point ratio provides + * better precision compared to an integer value of {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}.</li> + * <li>Zooming out from a wide lens to an ultrawide lens: zoomRatio supports zoom-out whereas + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} doesn't.</li> + * </ul> * <p>To illustrate, here are several scenarios of different zoom ratios, crop regions, * and output streams, for a hypothetical camera device with an active array of size * <code>(2000,1500)</code>.</p> diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index be03502eb943..ae04693b4ccf 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -2412,11 +2412,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>By using this control, the application gains a simpler way to control zoom, which can * be a combination of optical and digital zoom. For example, a multi-camera system may * contain more than one lens with different focal lengths, and the user can use optical - * zoom by switching between lenses. Using zoomRatio has benefits in the scenarios below: - * <em> Zooming in from a wide-angle lens to a telephoto lens: A floating-point ratio provides - * better precision compared to an integer value of {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}. - * </em> Zooming out from a wide lens to an ultrawide lens: zoomRatio supports zoom-out whereas - * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} doesn't.</p> + * zoom by switching between lenses. Using zoomRatio has benefits in the scenarios below:</p> + * <ul> + * <li>Zooming in from a wide-angle lens to a telephoto lens: A floating-point ratio provides + * better precision compared to an integer value of {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}.</li> + * <li>Zooming out from a wide lens to an ultrawide lens: zoomRatio supports zoom-out whereas + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} doesn't.</li> + * </ul> * <p>To illustrate, here are several scenarios of different zoom ratios, crop regions, * and output streams, for a hypothetical camera device with an active array of size * <code>(2000,1500)</code>.</p> diff --git a/core/java/android/hardware/display/OWNERS b/core/java/android/hardware/display/OWNERS new file mode 100644 index 000000000000..9ca391013aa3 --- /dev/null +++ b/core/java/android/hardware/display/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +santoscordon@google.com diff --git a/core/java/android/hardware/input/OWNERS b/core/java/android/hardware/input/OWNERS new file mode 100644 index 000000000000..0313a40f7270 --- /dev/null +++ b/core/java/android/hardware/input/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +svv@google.com diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS index e371df001151..0ec4fb832801 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -1,3 +1,20 @@ +# Haptics +per-file ExternalVibration.aidl = michaelwr@google.com +per-file ExternalVibration.java = michaelwr@google.com +per-file IExternalVibrationController.aidl = michaelwr@google.com +per-file IExternalVibratorService.aidl = michaelwr@google.com +per-file IVibratorService.aidl = michaelwr@google.com +per-file NullVibrator.java = michaelwr@google.com +per-file SystemVibrator.java = michaelwr@google.com +per-file VibrationEffect.aidl = michaelwr@google.com +per-file VibrationEffect.java = michaelwr@google.com +per-file Vibrator.java = michaelwr@google.com + +# PowerManager +per-file IPowerManager.aidl = michaelwr@google.com, santoscordon@google.com +per-file PowerManager.java = michaelwr@google.com, santoscordon@google.com +per-file PowerManagerInternal.java = michaelwr@google.com, santoscordon@google.com + # Zygote per-file ZygoteProcess.java = chriswailes@google.com, ngeoffray@google.com, sehr@google.com, narayan@google.com, maco@google.com diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 236ea0088062..25bf43043422 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2582,8 +2582,8 @@ public class UserManager { } /** - * Creates a user with the specified name and options. For non-admin users, default user - * restrictions are going to be applied. + * Creates a user with the specified name and options. + * Default user restrictions will be applied. * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. * * @param name the user's name @@ -2602,8 +2602,8 @@ public class UserManager { } /** - * Creates a user with the specified name and options. For non-admin users, default user - * restrictions will be applied. + * Creates a user with the specified name and options. + * Default user restrictions will be applied. * * <p>Requires {@link android.Manifest.permission#MANAGE_USERS}. * {@link android.Manifest.permission#CREATE_USERS} suffices if flags are in @@ -2637,8 +2637,7 @@ public class UserManager { } /** - * Pre-creates a user of the specified type. For non-admin users, default user - * restrictions will be applied. + * Pre-creates a user of the specified type. Default user restrictions will be applied. * * <p>This method can be used by OEMs to "warm" up the user creation by pre-creating some users * at the first boot, so they when the "real" user is created (for example, diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index d94160c2b40c..62becc507404 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -86,6 +86,16 @@ public final class FillRequest implements Parcelable { */ public static final @RequestFlags int FLAG_PASSWORD_INPUT_TYPE = 0x4; + /** + * Indicates the view was not focused. + * + * <p><b>Note:</b> Defines the flag value to 0x10, because the flag value 0x08 has been defined + * in {@link AutofillManager}.</p> + * + * @hide + */ + public static final @RequestFlags int FLAG_VIEW_NOT_FOCUSED = 0x10; + /** @hide */ public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE; @@ -165,7 +175,8 @@ public final class FillRequest implements Parcelable { @IntDef(flag = true, prefix = "FLAG_", value = { FLAG_MANUAL_REQUEST, FLAG_COMPATIBILITY_MODE_REQUEST, - FLAG_PASSWORD_INPUT_TYPE + FLAG_PASSWORD_INPUT_TYPE, + FLAG_VIEW_NOT_FOCUSED }) @Retention(RetentionPolicy.SOURCE) @DataClass.Generated.Member @@ -187,6 +198,8 @@ public final class FillRequest implements Parcelable { return "FLAG_COMPATIBILITY_MODE_REQUEST"; case FLAG_PASSWORD_INPUT_TYPE: return "FLAG_PASSWORD_INPUT_TYPE"; + case FLAG_VIEW_NOT_FOCUSED: + return "FLAG_VIEW_NOT_FOCUSED"; default: return Integer.toHexString(value); } } @@ -248,7 +261,8 @@ public final class FillRequest implements Parcelable { mFlags, FLAG_MANUAL_REQUEST | FLAG_COMPATIBILITY_MODE_REQUEST - | FLAG_PASSWORD_INPUT_TYPE); + | FLAG_PASSWORD_INPUT_TYPE + | FLAG_VIEW_NOT_FOCUSED); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; onConstructed(); @@ -384,7 +398,8 @@ public final class FillRequest implements Parcelable { mFlags, FLAG_MANUAL_REQUEST | FLAG_COMPATIBILITY_MODE_REQUEST - | FLAG_PASSWORD_INPUT_TYPE); + | FLAG_PASSWORD_INPUT_TYPE + | FLAG_VIEW_NOT_FOCUSED); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; onConstructed(); @@ -405,10 +420,10 @@ public final class FillRequest implements Parcelable { }; @DataClass.Generated( - time = 1588119440090L, + time = 1589280816805L, codegenVersion = "1.0.15", sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java", - inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 4469fdbb12ec..8db1703a627f 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -104,6 +104,14 @@ public final class Display { private int mCachedAppHeightCompat; /** + * Indicates that the application is started in a different rotation than the real display, so + * the display information may be adjusted. That ensures the methods {@link #getRotation}, + * {@link #getRealSize}, {@link #getRealMetrics}, and {@link #getCutout} are consistent with how + * the application window is laid out. + */ + private boolean mMayAdjustByFixedRotation; + + /** * The default Display id, which is the id of the primary display assuming there is one. */ public static final int DEFAULT_DISPLAY = 0; @@ -804,7 +812,9 @@ public final class Display { public int getRotation() { synchronized (this) { updateDisplayInfoLocked(); - return mDisplayInfo.rotation; + return mMayAdjustByFixedRotation + ? getDisplayAdjustments().getRotation(mDisplayInfo.rotation) + : mDisplayInfo.rotation; } } @@ -828,7 +838,9 @@ public final class Display { public DisplayCutout getCutout() { synchronized (this) { updateDisplayInfoLocked(); - return mDisplayInfo.displayCutout; + return mMayAdjustByFixedRotation + ? getDisplayAdjustments().getDisplayCutout(mDisplayInfo.displayCutout) + : mDisplayInfo.displayCutout; } } @@ -1140,6 +1152,9 @@ public final class Display { updateDisplayInfoLocked(); outSize.x = mDisplayInfo.logicalWidth; outSize.y = mDisplayInfo.logicalHeight; + if (mMayAdjustByFixedRotation) { + getDisplayAdjustments().adjustSize(outSize, mDisplayInfo.rotation); + } } } @@ -1159,6 +1174,9 @@ public final class Display { updateDisplayInfoLocked(); mDisplayInfo.getLogicalMetrics(outMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + if (mMayAdjustByFixedRotation) { + getDisplayAdjustments().adjustMetrics(outMetrics, mDisplayInfo.rotation); + } } } @@ -1225,6 +1243,9 @@ public final class Display { } } } + + mMayAdjustByFixedRotation = mResources != null + && mResources.hasOverrideDisplayAdjustments(); } private void updateCachedAppSizeIfNeededLocked() { @@ -1243,9 +1264,12 @@ public final class Display { public String toString() { synchronized (this) { updateDisplayInfoLocked(); - mDisplayInfo.getAppMetrics(mTempMetrics, getDisplayAdjustments()); + final DisplayAdjustments adjustments = getDisplayAdjustments(); + mDisplayInfo.getAppMetrics(mTempMetrics, adjustments); return "Display id " + mDisplayId + ": " + mDisplayInfo - + ", " + mTempMetrics + ", isValid=" + mIsValid; + + (mMayAdjustByFixedRotation + ? (", " + adjustments.getFixedRotationAdjustments() + ", ") : ", ") + + mTempMetrics + ", isValid=" + mIsValid; } } diff --git a/core/java/android/view/DisplayAdjustments.java b/core/java/android/view/DisplayAdjustments.java index 27c2d5c5cdc3..c726bee9f402 100644 --- a/core/java/android/view/DisplayAdjustments.java +++ b/core/java/android/view/DisplayAdjustments.java @@ -21,6 +21,10 @@ import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.graphics.Point; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.DisplayMetrics; import java.util.Objects; @@ -30,6 +34,7 @@ public class DisplayAdjustments { private volatile CompatibilityInfo mCompatInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; private final Configuration mConfiguration = new Configuration(Configuration.EMPTY); + private FixedRotationAdjustments mFixedRotationAdjustments; @UnsupportedAppUsage public DisplayAdjustments() { @@ -44,6 +49,7 @@ public class DisplayAdjustments { public DisplayAdjustments(@NonNull DisplayAdjustments daj) { setCompatibilityInfo(daj.mCompatInfo); mConfiguration.setTo(daj.getConfiguration()); + mFixedRotationAdjustments = daj.mFixedRotationAdjustments; } @UnsupportedAppUsage @@ -84,11 +90,78 @@ public class DisplayAdjustments { return mConfiguration; } + public void setFixedRotationAdjustments(FixedRotationAdjustments fixedRotationAdjustments) { + mFixedRotationAdjustments = fixedRotationAdjustments; + } + + public FixedRotationAdjustments getFixedRotationAdjustments() { + return mFixedRotationAdjustments; + } + + /** Returns {@code false} if the width and height of display should swap. */ + private boolean noFlip(@Surface.Rotation int realRotation) { + final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments; + if (rotationAdjustments == null) { + return true; + } + // Check if the delta is rotated by 90 degrees. + return (realRotation - rotationAdjustments.mRotation + 4) % 2 == 0; + } + + /** Adjusts the given size if possible. */ + public void adjustSize(@NonNull Point size, @Surface.Rotation int realRotation) { + if (noFlip(realRotation)) { + return; + } + final int w = size.x; + size.x = size.y; + size.y = w; + } + + /** Adjusts the given metrics if possible. */ + public void adjustMetrics(@NonNull DisplayMetrics metrics, @Surface.Rotation int realRotation) { + if (noFlip(realRotation)) { + return; + } + int w = metrics.widthPixels; + metrics.widthPixels = metrics.heightPixels; + metrics.heightPixels = w; + + w = metrics.noncompatWidthPixels; + metrics.noncompatWidthPixels = metrics.noncompatHeightPixels; + metrics.noncompatHeightPixels = w; + + float x = metrics.xdpi; + metrics.xdpi = metrics.ydpi; + metrics.ydpi = x; + + x = metrics.noncompatXdpi; + metrics.noncompatXdpi = metrics.noncompatYdpi; + metrics.noncompatYdpi = x; + } + + /** Returns the adjusted cutout if available. Otherwise the original cutout is returned. */ + @Nullable + public DisplayCutout getDisplayCutout(@Nullable DisplayCutout realCutout) { + final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments; + return rotationAdjustments != null && rotationAdjustments.mRotatedDisplayCutout != null + ? rotationAdjustments.mRotatedDisplayCutout + : realCutout; + } + + /** Returns the adjusted rotation if available. Otherwise the original rotation is returned. */ + @Surface.Rotation + public int getRotation(@Surface.Rotation int realRotation) { + final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments; + return rotationAdjustments != null ? rotationAdjustments.mRotation : realRotation; + } + @Override public int hashCode() { int hash = 17; hash = hash * 31 + Objects.hashCode(mCompatInfo); hash = hash * 31 + Objects.hashCode(mConfiguration); + hash = hash * 31 + Objects.hashCode(mFixedRotationAdjustments); return hash; } @@ -98,7 +171,82 @@ public class DisplayAdjustments { return false; } DisplayAdjustments daj = (DisplayAdjustments)o; - return Objects.equals(daj.mCompatInfo, mCompatInfo) && - Objects.equals(daj.mConfiguration, mConfiguration); + return Objects.equals(daj.mCompatInfo, mCompatInfo) + && Objects.equals(daj.mConfiguration, mConfiguration) + && Objects.equals(daj.mFixedRotationAdjustments, mFixedRotationAdjustments); + } + + /** + * An application can be launched in different rotation than the real display. This class + * provides the information to adjust the values returned by {@link #Display}. + * @hide + */ + public static class FixedRotationAdjustments implements Parcelable { + /** The application-based rotation. */ + @Surface.Rotation + final int mRotation; + + /** Non-null if the device has cutout. */ + @Nullable + final DisplayCutout mRotatedDisplayCutout; + + public FixedRotationAdjustments(@Surface.Rotation int rotation, DisplayCutout cutout) { + mRotation = rotation; + mRotatedDisplayCutout = cutout; + } + + @Override + public int hashCode() { + int hash = 17; + hash = hash * 31 + mRotation; + hash = hash * 31 + Objects.hashCode(mRotatedDisplayCutout); + return hash; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FixedRotationAdjustments)) { + return false; + } + final FixedRotationAdjustments other = (FixedRotationAdjustments) o; + return mRotation == other.mRotation + && Objects.equals(mRotatedDisplayCutout, other.mRotatedDisplayCutout); + } + + @Override + public String toString() { + return "FixedRotationAdjustments{rotation=" + Surface.rotationToString(mRotation) + + " cutout=" + mRotatedDisplayCutout + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRotation); + dest.writeTypedObject( + new DisplayCutout.ParcelableWrapper(mRotatedDisplayCutout), flags); + } + + private FixedRotationAdjustments(Parcel in) { + mRotation = in.readInt(); + final DisplayCutout.ParcelableWrapper cutoutWrapper = + in.readTypedObject(DisplayCutout.ParcelableWrapper.CREATOR); + mRotatedDisplayCutout = cutoutWrapper != null ? cutoutWrapper.get() : null; + } + + public static final Creator<FixedRotationAdjustments> CREATOR = + new Creator<FixedRotationAdjustments>() { + public FixedRotationAdjustments createFromParcel(Parcel in) { + return new FixedRotationAdjustments(in); + } + + public FixedRotationAdjustments[] newArray(int size) { + return new FixedRotationAdjustments[size]; + } + }; } } diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS new file mode 100644 index 000000000000..7b60f2e1a6bd --- /dev/null +++ b/core/java/android/view/OWNERS @@ -0,0 +1,15 @@ +# Display +per-file Display.java = michaelwr@google.com, santoscordon@google.com +per-file DisplayInfo.java = michaelwr@google.com, santoscordon@google.com + +# Haptics +per-file HapticFeedbackConstants.java = michaelwr@google.com, santoscordon@google.com + +# Input +per-file IInputMonitorHost.aidl = michaelwr@google.com, svv@google.com +per-file Input*.java = michaelwr@google.com, svv@google.com +per-file Input*.aidl = michaelwr@google.com, svv@google.com +per-file KeyEvent.java = michaelwr@google.com, svv@google.com +per-file MotionEvent.java = michaelwr@google.com, svv@google.com +per-file PointerIcon.java = michaelwr@google.com, svv@google.com +per-file SimulatedDpad.java = michaelwr@google.com, svv@google.com diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 76fe6b5f666d..553e3c8c2d1b 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -18,6 +18,7 @@ package android.view.autofill; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; +import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; import static android.view.autofill.Helper.sDebug; import static android.view.autofill.Helper.sVerbose; import static android.view.autofill.Helper.toList; @@ -879,7 +880,11 @@ public final class AutofillManager { * @param view view requesting the new autofill context. */ public void requestAutofill(@NonNull View view) { - notifyViewEntered(view, FLAG_MANUAL_REQUEST); + int flags = FLAG_MANUAL_REQUEST; + if (!view.isFocused()) { + flags |= FLAG_VIEW_NOT_FOCUSED; + } + notifyViewEntered(view, flags); } /** @@ -926,7 +931,11 @@ public final class AutofillManager { * @param absBounds absolute boundaries of the virtual view in the screen. */ public void requestAutofill(@NonNull View view, int virtualId, @NonNull Rect absBounds) { - notifyViewEntered(view, virtualId, absBounds, FLAG_MANUAL_REQUEST); + int flags = FLAG_MANUAL_REQUEST; + if (!view.isFocused()) { + flags |= FLAG_VIEW_NOT_FOCUSED; + } + notifyViewEntered(view, virtualId, absBounds, flags); } /** diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 35dd5760d5ab..e224e84a56fe 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1118,6 +1118,9 @@ public abstract class WebSettings { * {@link #setAppCachePath}. * * @param flag {@code true} if the WebView should enable Application Caches + * @deprecated The Application Cache API is deprecated and this method will + * become a no-op on all Android versions once support is + * removed in Chromium. Consider using Service Workers instead. */ public abstract void setAppCacheEnabled(boolean flag); @@ -1130,6 +1133,9 @@ public abstract class WebSettings { * @param appCachePath a String path to the directory containing * Application Caches files. * @see #setAppCacheEnabled + * @deprecated The Application Cache API is deprecated and this method will + * become a no-op on all Android versions once support is + * removed in Chromium. Consider using Service Workers instead. */ public abstract void setAppCachePath(String appCachePath); @@ -1142,7 +1148,7 @@ public abstract class WebSettings { * It is recommended to leave the maximum size set to the default value. * * @param appCacheMaxSize the maximum size in bytes - * @deprecated In future quota will be managed automatically. + * @deprecated Quota is managed automatically; this method is a no-op. */ @Deprecated public abstract void setAppCacheMaxSize(long appCacheMaxSize); diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 891d53527f96..a3c29a8d4a7b 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -19,6 +19,7 @@ package com.android.internal.accessibility; import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; +import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; import static com.android.internal.util.ArrayUtils.convertToLongArray; import android.accessibilityservice.AccessibilityServiceInfo; @@ -52,6 +53,7 @@ import android.view.accessibility.AccessibilityManager; import android.widget.Toast; import com.android.internal.R; +import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.util.function.pooled.PooledLambda; import java.lang.annotation.Retention; @@ -267,16 +269,21 @@ public class AccessibilityShortcutController { } private AlertDialog createShortcutWarningDialog(int userId) { - final String warningMessage = mContext.getString( - R.string.accessibility_shortcut_toogle_warning); + List<AccessibilityTarget> targets = getTargets(mContext, ACCESSIBILITY_SHORTCUT_KEY); + if (targets.size() == 0) { + return null; + } + + // Avoid non-a11y users accidentally turning shortcut on without reading this carefully. + // Put "don't turn on" as the primary action. final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder( // Use SystemUI context so we pick up any theme set in a vendor overlay mFrameworkObjectProvider.getSystemUiContext()) - .setTitle(R.string.accessibility_shortcut_warning_dialog_title) - .setMessage(warningMessage) + .setTitle(getShortcutWarningTitle(targets)) + .setMessage(getShortcutWarningMessage(targets)) .setCancelable(false) - .setPositiveButton(R.string.leave_accessibility_shortcut_on, null) - .setNegativeButton(R.string.disable_accessibility_shortcut, + .setNegativeButton(R.string.accessibility_shortcut_on, null) + .setPositiveButton(R.string.accessibility_shortcut_off, (DialogInterface d, int which) -> { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", @@ -297,6 +304,32 @@ public class AccessibilityShortcutController { return alertDialog; } + private String getShortcutWarningTitle(List<AccessibilityTarget> targets) { + if (targets.size() == 1) { + return mContext.getString( + R.string.accessibility_shortcut_single_service_warning_title, + targets.get(0).getLabel()); + } + return mContext.getString( + R.string.accessibility_shortcut_multiple_service_warning_title); + } + + private String getShortcutWarningMessage(List<AccessibilityTarget> targets) { + if (targets.size() == 1) { + return mContext.getString( + R.string.accessibility_shortcut_single_service_warning, + targets.get(0).getLabel()); + } + + final StringBuilder sb = new StringBuilder(); + for (AccessibilityTarget target : targets) { + sb.append(mContext.getString(R.string.accessibility_shortcut_multiple_service_list, + target.getLabel())); + } + return mContext.getString(R.string.accessibility_shortcut_multiple_service_warning, + sb.toString()); + } + private AccessibilityServiceInfo getInfoForTargetService() { final ComponentName targetComponentName = getShortcutTargetComponentName(); if (targetComponentName == null) { diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java index 37871d0b5a10..d75659372a07 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java @@ -38,7 +38,7 @@ import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; * Abstract base class for creating various target related to accessibility service, * accessibility activity, and white listing feature. */ -abstract class AccessibilityTarget implements TargetOperations, OnTargetSelectedListener, +public abstract class AccessibilityTarget implements TargetOperations, OnTargetSelectedListener, OnTargetCheckedChangeListener { private Context mContext; @ShortcutType diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index f63cbe0dcd9e..60a102adcf7a 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -53,19 +53,61 @@ import java.util.Locale; /** * Collection of utilities for accessibility target. */ -final class AccessibilityTargetHelper { +public final class AccessibilityTargetHelper { private AccessibilityTargetHelper() {} - static List<AccessibilityTarget> getTargets(Context context, + /** + * Returns list of {@link AccessibilityTarget} of assigned accessibility shortcuts from + * {@link AccessibilityManager#getAccessibilityShortcutTargets} including accessibility + * feature's package name, component id, etc. + * + * @param context The context of the application. + * @param shortcutType The shortcut type. + * @return The list of {@link AccessibilityTarget}. + * @hide + */ + public static List<AccessibilityTarget> getTargets(Context context, @ShortcutType int shortcutType) { - final List<AccessibilityTarget> targets = getInstalledTargets(context, shortcutType); - final AccessibilityManager ams = context.getSystemService(AccessibilityManager.class); - final List<String> requiredTargets = ams.getAccessibilityShortcutTargets(shortcutType); - targets.removeIf(target -> !requiredTargets.contains(target.getId())); - - return targets; + // List all accessibility target + final List<AccessibilityTarget> installedTargets = getInstalledTargets(context, + shortcutType); + + // List accessibility shortcut target + final AccessibilityManager am = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); + final List<String> assignedTargets = am.getAccessibilityShortcutTargets(shortcutType); + + // Get the list of accessibility shortcut target in all accessibility target + final List<AccessibilityTarget> results = new ArrayList<>(); + for (String assignedTarget : assignedTargets) { + for (AccessibilityTarget installedTarget : installedTargets) { + if (!MAGNIFICATION_CONTROLLER_NAME.contentEquals(assignedTarget)) { + final ComponentName assignedTargetComponentName = + ComponentName.unflattenFromString(assignedTarget); + final ComponentName targetComponentName = ComponentName.unflattenFromString( + installedTarget.getId()); + if (assignedTargetComponentName.equals(targetComponentName)) { + results.add(installedTarget); + continue; + } + } + if (assignedTarget.contentEquals(installedTarget.getId())) { + results.add(installedTarget); + } + } + } + return results; } + /** + * Returns list of {@link AccessibilityTarget} of the installed accessibility service, + * accessibility activity, and white listing feature including accessibility feature's package + * name, component id, etc. + * + * @param context The context of the application. + * @param shortcutType The shortcut type. + * @return The list of {@link AccessibilityTarget}. + */ static List<AccessibilityTarget> getInstalledTargets(Context context, @ShortcutType int shortcutType) { final List<AccessibilityTarget> targets = new ArrayList<>(); @@ -110,9 +152,10 @@ final class AccessibilityTargetHelper { private static List<AccessibilityTarget> getAccessibilityServiceTargets(Context context, @ShortcutType int shortcutType) { - final AccessibilityManager ams = context.getSystemService(AccessibilityManager.class); + final AccessibilityManager am = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); final List<AccessibilityServiceInfo> installedServices = - ams.getInstalledAccessibilityServiceList(); + am.getInstalledAccessibilityServiceList(); if (installedServices == null) { return Collections.emptyList(); } @@ -136,9 +179,10 @@ final class AccessibilityTargetHelper { private static List<AccessibilityTarget> getAccessibilityActivityTargets(Context context, @ShortcutType int shortcutType) { - final AccessibilityManager ams = context.getSystemService(AccessibilityManager.class); + final AccessibilityManager am = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); final List<AccessibilityShortcutInfo> installedServices = - ams.getInstalledAccessibilityShortcutListAsUser(context, + am.getInstalledAccessibilityShortcutListAsUser(context, ActivityManager.getCurrentUser()); if (installedServices == null) { return Collections.emptyList(); diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java index e50b010d691a..9ee0b0ea1891 100644 --- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java @@ -142,7 +142,8 @@ public final class AccessibilityUtils { */ public static boolean isAccessibilityServiceEnabled(Context context, @NonNull String componentId) { - final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + final AccessibilityManager am = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); final List<AccessibilityServiceInfo> enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java index 100422f5660d..31ccb6c32bab 100644 --- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java +++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java @@ -137,7 +137,8 @@ public final class ShortcutUtils { */ public static boolean isShortcutContained(Context context, @ShortcutType int shortcutType, @NonNull String componentId) { - final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + final AccessibilityManager am = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); final List<String> requiredTargets = am.getAccessibilityShortcutTargets(shortcutType); return requiredTargets.contains(componentId); } diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index 2f048c95ae4e..a50a52219c74 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -68,6 +68,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; import java.util.regex.Pattern; /** @@ -381,17 +382,51 @@ public abstract class FileSystemProvider extends DocumentsProvider { return result; } + /** + * This method is similar to + * {@link DocumentsProvider#queryChildDocuments(String, String[], String)}. This method returns + * all children documents including hidden directories/files. + * + * <p> + * In a scoped storage world, access to "Android/data" style directories are hidden for privacy + * reasons. This method may show privacy sensitive data, so its usage should only be in + * restricted modes. + * + * @param parentDocumentId the directory to return children for. + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + * @param sortOrder how to order the rows, formatted as an SQL + * {@code ORDER BY} clause (excluding the ORDER BY itself). + * Passing {@code null} will use the default sort order, which + * may be unordered. This ordering is a hint that can be used to + * prioritize how data is fetched from the network, but UI may + * always enforce a specific ordering + * @throws FileNotFoundException when parent document doesn't exist or query fails + */ + protected Cursor queryChildDocumentsShowAll( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException { + return queryChildDocuments(parentDocumentId, projection, sortOrder, File -> true); + } + @Override public Cursor queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + // Access to some directories is hidden for privacy reasons. + return queryChildDocuments(parentDocumentId, projection, sortOrder, this::shouldShow); + } + private Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder, + @NonNull Predicate<File> filter) throws FileNotFoundException { final File parent = getFileForDocId(parentDocumentId); final MatrixCursor result = new DirectoryCursor( resolveProjection(projection), parentDocumentId, parent); if (parent.isDirectory()) { for (File file : FileUtils.listFilesOrEmpty(parent)) { - if (!shouldHide(file)) { + if (filter.test(file)) { includeFile(result, null, file); } } @@ -617,6 +652,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { return (PATTERN_HIDDEN_PATH.matcher(file.getAbsolutePath()).matches()); } + private boolean shouldShow(@NonNull File file) { + return !shouldHide(file); + } + protected boolean shouldBlockFromTree(@NonNull String docId) { return false; } diff --git a/core/java/com/android/internal/widget/OWNERS b/core/java/com/android/internal/widget/OWNERS new file mode 100644 index 000000000000..cca39ea3287d --- /dev/null +++ b/core/java/com/android/internal/widget/OWNERS @@ -0,0 +1 @@ +per-file PointerLocationView.java = michaelwr@google.com, svv@google.com diff --git a/core/jni/OWNERS b/core/jni/OWNERS index 7ff15f2e182d..d7d8621a3640 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -5,5 +5,16 @@ per-file *Camera*,*camera* = shuzhenwang@google.com, yinchiayeh@google.com, zhij # Connectivity per-file android_net_* = codewiz@google.com, jchalard@google.com, lorenzo@google.com, reminv@google.com, satk@google.com +# Display +per-file android_hardware_display_* = michaelwr@google.com, santoscordon@google.com + +# Input +per-file android_hardware_input* = michaelwr@google.com, svv@google.com +per-file android_view_Input* = michaelwr@google.com, svv@google.com +per-file android_view_KeyCharacterMap.* = michaelwr@google.com, svv@google.com +per-file android_view_*KeyEvent.* = michaelwr@google.com, svv@google.com +per-file android_view_*MotionEvent.* = michaelwr@google.com, svv@google.com +per-file android_view_PointerIcon.* = michaelwr@google.com, svv@google.com + # Zygote per-file com_android_internal_os_Zygote.*,fd_utils.* = chriswailes@google.com, ngeoffray@google.com, sehr@google.com, narayan@google.com, maco@google.com diff --git a/core/res/res/values-mcc334-mnc020/config.xml b/core/res/res/values-mcc334-mnc020/config.xml index c64acc7c29db..82b3ee6448e3 100644 --- a/core/res/res/values-mcc334-mnc020/config.xml +++ b/core/res/res/values-mcc334-mnc020/config.xml @@ -19,6 +19,6 @@ <resources> <bool name="config_use_sim_language_file">false</bool> - <bool name="config_pdp_rejeect_enable_retry">true</bool> + <bool name="config_pdp_reject_enable_retry">true</bool> <integer name="config_pdp_reject_retry_delay_ms">45000</integer> -</resources>
\ No newline at end of file +</resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 35a7857b839f..f3f3d47df4a7 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4390,12 +4390,6 @@ <!-- Used in multiple service warning to list current features. [CHAR LIMIT=none] --> <string name="accessibility_shortcut_multiple_service_list">\t• <xliff:g id="service" example="TalkBack">%1$s</xliff:g>\n</string> - <!-- Dialog title for dialog shown when the TalkBack shortcut is activated, and we want to confirm that the user understands what's going to happen. [CHAR LIMIT=none] --> - <string name="accessibility_shortcut_talkback_warning_title">Turn on TalkBack?</string> - - <!-- Message shown in dialog when user is in the process of enabling the TalkBack via the volume buttons shortcut for the first time. [CHAR LIMIT=none] --> - <string name="accessibility_shortcut_talkback_warning">Holding down both volume keys for a few seconds turns on TalkBack, a screen reader that is helpful for people who are blind or have low vision. TalkBack completely changes how your device works.\n\nYou can change this shortcut to another feature in Settings > Accessibility.</string> - <!-- Dialog title for dialog shown when this accessibility shortcut is activated, and we want to confirm that the user understands what's going to happen. [CHAR LIMIT=none] --> <string name="accessibility_shortcut_single_service_warning_title">Turn on <xliff:g id="service" example="TalkBack">%1$s</xliff:g>?</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9f3ace54c264..369a3e51df26 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3221,12 +3221,15 @@ <java-symbol type="integer" name="config_debugSystemServerPssThresholdBytes" /> <!-- Accessibility Shortcut --> - <java-symbol type="string" name="accessibility_shortcut_warning_dialog_title" /> - <java-symbol type="string" name="accessibility_shortcut_toogle_warning" /> + <java-symbol type="string" name="accessibility_shortcut_single_service_warning_title" /> + <java-symbol type="string" name="accessibility_shortcut_single_service_warning" /> + <java-symbol type="string" name="accessibility_shortcut_multiple_service_warning_title" /> + <java-symbol type="string" name="accessibility_shortcut_multiple_service_warning" /> + <java-symbol type="string" name="accessibility_shortcut_multiple_service_list" /> + <java-symbol type="string" name="accessibility_shortcut_on" /> + <java-symbol type="string" name="accessibility_shortcut_off" /> <java-symbol type="string" name="accessibility_shortcut_enabling_service" /> <java-symbol type="string" name="accessibility_shortcut_disabling_service" /> - <java-symbol type="string" name="disable_accessibility_shortcut" /> - <java-symbol type="string" name="leave_accessibility_shortcut_on" /> <java-symbol type="string" name="color_inversion_feature_name" /> <java-symbol type="string" name="color_correction_feature_name" /> <java-symbol type="string" name="config_defaultAccessibilityService" /> diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java index 107fe3f3ced5..6c23125aaf13 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java @@ -144,11 +144,12 @@ public class ObjectPoolTests { IBinder assistToken = new Binder(); LaunchActivityItem emptyItem = LaunchActivityItem.obtain(null, 0, null, null, null, null, - null, null, 0, null, null, null, null, false, null, null); + null, null, 0, null, null, null, null, false, null, null, null); LaunchActivityItem item = LaunchActivityItem.obtain(intent, ident, activityInfo, config(), overrideConfig, compat, referrer, null /* voiceInteractor */, procState, bundle, persistableBundle, resultInfoList(), referrerIntentList(), - true /* isForward */, null /* profilerInfo */, assistToken); + true /* isForward */, null /* profilerInfo */, assistToken, + null /* fixedRotationAdjustments */); assertNotSame(item, emptyItem); assertFalse(item.equals(emptyItem)); @@ -158,7 +159,8 @@ public class ObjectPoolTests { LaunchActivityItem item2 = LaunchActivityItem.obtain(intent, ident, activityInfo, config(), overrideConfig, compat, referrer, null /* voiceInteractor */, procState, bundle, persistableBundle, resultInfoList(), referrerIntentList(), - true /* isForward */, null /* profilerInfo */, assistToken); + true /* isForward */, null /* profilerInfo */, assistToken, + null /* fixedRotationAdjustments */); assertSame(item, item2); assertFalse(item2.equals(emptyItem)); } diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java index 09ea1b1865c0..3c32c71cf961 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java @@ -267,7 +267,7 @@ public class TransactionExecutorTests { null /* voiceInteractor */, 0 /* procState */, null /* state */, null /* persistentState */, null /* pendingResults */, null /* pendingNewIntents */, false /* isForward */, null /* profilerInfo */, - null /* assistToken*/)); + null /* assistToken */, null /* fixedRotationAdjustments */)); launchTransaction.addCallback(launchItem); mExecutor.execute(launchTransaction); diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index 47f9323a95f9..3f8d9ef964db 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -52,6 +52,9 @@ import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.view.DisplayAdjustments.FixedRotationAdjustments; +import android.view.DisplayCutout; +import android.view.Surface; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -187,11 +190,14 @@ public class TransactionParcelTests { bundle.putParcelable("data", new ParcelableData(1)); PersistableBundle persistableBundle = new PersistableBundle(); persistableBundle.putInt("k", 4); + FixedRotationAdjustments fixedRotationAdjustments = new FixedRotationAdjustments( + Surface.ROTATION_90, DisplayCutout.NO_CUTOUT); LaunchActivityItem item = LaunchActivityItem.obtain(intent, ident, activityInfo, config(), overrideConfig, compat, referrer, null /* voiceInteractor */, procState, bundle, persistableBundle, resultInfoList(), referrerIntentList(), - true /* isForward */, null /* profilerInfo */, new Binder()); + true /* isForward */, null /* profilerInfo */, new Binder(), + fixedRotationAdjustments); writeAndPrepareForReading(item); // Read from parcel and assert @@ -340,6 +346,22 @@ public class TransactionParcelTests { assertTrue(transaction.equals(result)); } + @Test + public void testFixedRotationAdjustments() { + ClientTransaction transaction = ClientTransaction.obtain(new StubAppThread(), + null /* activityToken */); + transaction.addCallback(FixedRotationAdjustmentsItem.obtain(new Binder(), + new FixedRotationAdjustments(Surface.ROTATION_270, DisplayCutout.NO_CUTOUT))); + + writeAndPrepareForReading(transaction); + + // Read from parcel and assert + ClientTransaction result = ClientTransaction.CREATOR.createFromParcel(mParcel); + + assertEquals(transaction.hashCode(), result.hashCode()); + assertTrue(transaction.equals(result)); + } + /** Write to {@link #mParcel} and reset its position to prepare for reading from the start. */ private void writeAndPrepareForReading(Parcelable parcelable) { parcelable.writeToParcel(mParcel, 0 /* flags */); diff --git a/core/tests/coretests/src/android/app/usage/UsageStatsPersistenceTest.java b/core/tests/coretests/src/android/app/usage/UsageStatsPersistenceTest.java new file mode 100644 index 000000000000..4d04a7af4693 --- /dev/null +++ b/core/tests/coretests/src/android/app/usage/UsageStatsPersistenceTest.java @@ -0,0 +1,106 @@ +/* + * 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.app.usage; + +import static junit.framework.Assert.fail; + +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; + +/** + * These tests verify that all fields defined in {@link UsageStats} and {@link UsageEvents.Event} + * are all known fields. This ensures that newly added fields or refactorings are accounted for in + * the usagestatsservice.proto and usagestatsservice_v2.proto files. + * + * Note: verification for {@link com.android.server.usage.IntervalStats} fields is located in + * {@link com.android.server.usage.IntervalStatsTests}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UsageStatsPersistenceTest { + + // All fields in this list are defined in UsageStats and persisted - please ensure they're + // defined correctly in both usagestatsservice.proto and usagestatsservice_v2.proto + private static final String[] USAGESTATS_PERSISTED_FIELDS = {"mBeginTimeStamp", "mEndTimeStamp", + "mPackageName", "mPackageToken", "mLastEvent", "mAppLaunchCount", "mChooserCounts", + "mLastTimeUsed", "mTotalTimeInForeground", "mLastTimeForegroundServiceUsed", + "mTotalTimeForegroundServiceUsed", "mLastTimeVisible", "mTotalTimeVisible"}; + // All fields in this list are defined in UsageStats but not persisted + private static final String[] USAGESTATS_IGNORED_FIELDS = {"CREATOR", "mActivities", + "mForegroundServices", "mLaunchCount", "mChooserCountsObfuscated"}; + + @Test + public void testUsageStatsFields() { + final UsageStats stats = new UsageStats(); + final Field[] fields = stats.getClass().getDeclaredFields(); + for (Field field : fields) { + if (!(ArrayUtils.contains(USAGESTATS_PERSISTED_FIELDS, field.getName()) + || ArrayUtils.contains(USAGESTATS_IGNORED_FIELDS, field.getName()))) { + fail("Found an unknown field: " + field.getName() + ". Please correctly update " + + "either USAGESTATS_PERSISTED_FIELDS or USAGESTATS_IGNORED_FIELDS."); + } + } + } + + // All fields in this list are defined in UsageEvents.Event and persisted - please ensure + // they're defined correctly in both usagestatsservice.proto and usagestatsservice_v2.proto + private static final String[] USAGEEVENTS_PERSISTED_FIELDS = {"mPackage", "mPackageToken", + "mClass", "mClassToken", "mTimeStamp", "mFlags", "mEventType", "mConfiguration", + "mShortcutId", "mShortcutIdToken", "mBucketAndReason", "mInstanceId", + "mNotificationChannelId", "mNotificationChannelIdToken", "mTaskRootPackage", + "mTaskRootPackageToken", "mTaskRootClass", "mTaskRootClassToken", "mLocusId", + "mLocusIdToken"}; + // All fields in this list are defined in UsageEvents.Event but not persisted + private static final String[] USAGEEVENTS_IGNORED_FIELDS = {"mAction", "mContentAnnotations", + "mContentType", "DEVICE_EVENT_PACKAGE_NAME", "FLAG_IS_PACKAGE_INSTANT_APP", + "VALID_FLAG_BITS", "UNASSIGNED_TOKEN", "MAX_EVENT_TYPE"}; + // All fields in this list are final constants defining event types and not persisted + private static final String[] EVENT_TYPES = {"NONE", "ACTIVITY_DESTROYED", "ACTIVITY_PAUSED", + "ACTIVITY_RESUMED", "ACTIVITY_STOPPED", "CHOOSER_ACTION", "CONFIGURATION_CHANGE", + "CONTINUE_PREVIOUS_DAY", "CONTINUING_FOREGROUND_SERVICE", "DEVICE_SHUTDOWN", + "DEVICE_STARTUP", "END_OF_DAY", "FLUSH_TO_DISK", "FOREGROUND_SERVICE_START", + "FOREGROUND_SERVICE_STOP", "KEYGUARD_HIDDEN", "KEYGUARD_SHOWN", "LOCUS_ID_SET", + "MOVE_TO_BACKGROUND", "MOVE_TO_FOREGROUND", "NOTIFICATION_INTERRUPTION", + "NOTIFICATION_SEEN", "ROLLOVER_FOREGROUND_SERVICE", "SCREEN_INTERACTIVE", + "SCREEN_NON_INTERACTIVE", "SHORTCUT_INVOCATION", "SLICE_PINNED", "SLICE_PINNED_PRIV", + "STANDBY_BUCKET_CHANGED", "SYSTEM_INTERACTION", "USER_INTERACTION", "USER_STOPPED", + "USER_UNLOCKED"}; + + @Test + public void testUsageEventsFields() { + final UsageEvents.Event event = new UsageEvents.Event(); + final Field[] fields = event.getClass().getDeclaredFields(); + for (Field field : fields) { + final String name = field.getName(); + if (!(ArrayUtils.contains(USAGEEVENTS_PERSISTED_FIELDS, name) + || ArrayUtils.contains(USAGEEVENTS_IGNORED_FIELDS, name) + || ArrayUtils.contains(EVENT_TYPES, name))) { + fail("Found an unknown field: " + name + ". Please correctly update either " + + "USAGEEVENTS_PERSISTED_FIELDS or USAGEEVENTS_IGNORED_FIELDS. If this " + + "field is a new event type, please update EVENT_TYPES instead."); + } + } + } +} diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java index 4114b28a7252..efcd458e19cc 100644 --- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java +++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java @@ -259,4 +259,35 @@ public class ResourcesManagerTest extends TestCase { expectedConfig2.orientation = Configuration.ORIENTATION_LANDSCAPE; assertEquals(expectedConfig2, resources2.getConfiguration()); } + + @SmallTest + public void testOverrideDisplayAdjustments() { + final int originalOverrideDensity = 200; + final int overrideDisplayDensity = 400; + final Binder token = new Binder(); + final Configuration overrideConfig = new Configuration(); + overrideConfig.densityDpi = originalOverrideDensity; + final Resources resources = mResourcesManager.createBaseTokenResources( + token, APP_ONE_RES_DIR, null /* splitResDirs */, null /* overlayDirs */, + null /* libDirs */, Display.DEFAULT_DISPLAY, overrideConfig, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null /* classLoader */, + null /* loaders */); + + // Update the override. + boolean handled = mResourcesManager.overrideTokenDisplayAdjustments(token, + adjustments -> adjustments.getConfiguration().densityDpi = overrideDisplayDensity); + + assertTrue(handled); + assertTrue(resources.hasOverrideDisplayAdjustments()); + assertEquals(overrideDisplayDensity, + resources.getDisplayAdjustments().getConfiguration().densityDpi); + + // Clear the override. + handled = mResourcesManager.overrideTokenDisplayAdjustments(token, null /* override */); + + assertTrue(handled); + assertFalse(resources.hasOverrideDisplayAdjustments()); + assertEquals(originalOverrideDensity, + resources.getDisplayAdjustments().getConfiguration().densityDpi); + } } diff --git a/core/tests/coretests/src/android/hardware/display/OWNERS b/core/tests/coretests/src/android/hardware/display/OWNERS new file mode 100644 index 000000000000..9ca391013aa3 --- /dev/null +++ b/core/tests/coretests/src/android/hardware/display/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +santoscordon@google.com diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS new file mode 100644 index 000000000000..1a28b73de8cd --- /dev/null +++ b/core/tests/coretests/src/android/os/OWNERS @@ -0,0 +1,9 @@ +# Display +per-file BrightnessLimit.java = michaelwr@google.com, santoscordon@google.com + +# Haptics +per-file ExternalVibrationTest.java = michaelwr@google.com +per-file VibrationEffectTest.java = michaelwr@google.com + +# Power +per-file PowerManager*.java = michaelwr@google.com, santoscordon@google.com diff --git a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java index afbf8db3cd2d..2fc42e91a8cc 100644 --- a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java +++ b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java @@ -19,6 +19,9 @@ package android.view; import static org.junit.Assert.assertEquals; import android.content.res.Configuration; +import android.graphics.Point; +import android.util.DisplayMetrics; +import android.view.DisplayAdjustments.FixedRotationAdjustments; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -67,4 +70,38 @@ public class DisplayAdjustmentsTests { assertEquals(configuration, newAdjustments.getConfiguration()); } + + @Test + public void testFixedRotationAdjustments() { + final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments(); + final int realRotation = Surface.ROTATION_0; + final int fixedRotation = Surface.ROTATION_90; + + mDisplayAdjustments.setFixedRotationAdjustments( + new FixedRotationAdjustments(fixedRotation, null /* cutout */)); + + final int w = 1000; + final int h = 2000; + final Point size = new Point(w, h); + mDisplayAdjustments.adjustSize(size, realRotation); + + assertEquals(fixedRotation, mDisplayAdjustments.getRotation(realRotation)); + assertEquals(new Point(h, w), size); + + final DisplayMetrics metrics = new DisplayMetrics(); + metrics.xdpi = metrics.noncompatXdpi = w; + metrics.widthPixels = metrics.noncompatWidthPixels = w; + metrics.ydpi = metrics.noncompatYdpi = h; + metrics.heightPixels = metrics.noncompatHeightPixels = h; + + final DisplayMetrics flippedMetrics = new DisplayMetrics(); + flippedMetrics.xdpi = flippedMetrics.noncompatXdpi = h; + flippedMetrics.widthPixels = flippedMetrics.noncompatWidthPixels = h; + flippedMetrics.ydpi = flippedMetrics.noncompatYdpi = w; + flippedMetrics.heightPixels = flippedMetrics.noncompatHeightPixels = w; + + mDisplayAdjustments.adjustMetrics(metrics, realRotation); + + assertEquals(flippedMetrics, metrics); + } } diff --git a/core/tests/coretests/src/android/view/OWNERS b/core/tests/coretests/src/android/view/OWNERS new file mode 100644 index 000000000000..a3a3e7cfc4af --- /dev/null +++ b/core/tests/coretests/src/android/view/OWNERS @@ -0,0 +1,4 @@ +# Input +per-file *MotionEventTest.* = michaelwr@google.com, svv@google.com +per-file *KeyEventTest.* = michaelwr@google.com, svv@google.com +per-file VelocityTest.java = michaelwr@google.com, svv@google.com diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java index b21504c73772..c17c36eba2dc 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java @@ -93,6 +93,7 @@ import java.util.Set; @RunWith(AndroidJUnit4.class) public class AccessibilityShortcutControllerTest { private static final String SERVICE_NAME_STRING = "fake.package/fake.service.name"; + private static final CharSequence PACKAGE_NAME_STRING = "Service name"; private static final String SERVICE_NAME_SUMMARY = "Summary"; private static final long VIBRATOR_PATTERN_1 = 100L; private static final long VIBRATOR_PATTERN_2 = 150L; @@ -150,6 +151,8 @@ public class AccessibilityShortcutControllerTest { new AccessibilityManager(mHandler, mAccessibilityManagerService, 0); when(mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)) .thenReturn(accessibilityManager); + when(mContext.getSystemService(Context.ACCESSIBILITY_SERVICE)) + .thenReturn(accessibilityManager); when(mFrameworkObjectProvider.getAlertDialogBuilder(mContext)) .thenReturn(mAlertDialogBuilder); when(mFrameworkObjectProvider.makeToastFromText(eq(mContext), anyObject(), anyInt())) @@ -166,13 +169,13 @@ public class AccessibilityShortcutControllerTest { ResolveInfo resolveInfo = mock(ResolveInfo.class); resolveInfo.serviceInfo = mock(ServiceInfo.class); resolveInfo.serviceInfo.applicationInfo = mApplicationInfo; - when(resolveInfo.loadLabel(anyObject())).thenReturn("Service name"); + when(resolveInfo.loadLabel(anyObject())).thenReturn(PACKAGE_NAME_STRING); when(mServiceInfo.getResolveInfo()).thenReturn(resolveInfo); when(mServiceInfo.getComponentName()) .thenReturn(ComponentName.unflattenFromString(SERVICE_NAME_STRING)); when(mServiceInfo.loadSummary(any())).thenReturn(SERVICE_NAME_SUMMARY); - when(mAlertDialogBuilder.setTitle(anyInt())).thenReturn(mAlertDialogBuilder); + when(mAlertDialogBuilder.setTitle(anyObject())).thenReturn(mAlertDialogBuilder); when(mAlertDialogBuilder.setCancelable(anyBoolean())).thenReturn(mAlertDialogBuilder); when(mAlertDialogBuilder.setMessage(anyObject())).thenReturn(mAlertDialogBuilder); when(mAlertDialogBuilder.setPositiveButton(anyInt(), anyObject())) @@ -324,7 +327,8 @@ public class AccessibilityShortcutControllerTest { assertEquals(1, Settings.Secure.getInt( mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0)); - verify(mResources).getString(R.string.accessibility_shortcut_toogle_warning); + verify(mResources).getString( + R.string.accessibility_shortcut_single_service_warning_title, PACKAGE_NAME_STRING); verify(mAlertDialog).show(); verify(mAccessibilityManagerService, atLeastOnce()).getInstalledAccessibilityServiceList( anyInt()); @@ -376,16 +380,20 @@ public class AccessibilityShortcutControllerTest { ArgumentCaptor<DialogInterface.OnClickListener> captor = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mAlertDialogBuilder).setNegativeButton(eq(R.string.disable_accessibility_shortcut), + verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.accessibility_shortcut_off), captor.capture()); - // Call the button callback - captor.getValue().onClick(null, 0); + // Call the button callback, if one exists + if (captor.getValue() != null) { + captor.getValue().onClick(null, 0); + } assertTrue(TextUtils.isEmpty( Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))); + assertEquals(0, Settings.Secure.getInt( + mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)); } @Test - public void testClickingLeaveOnButtonInDialog_shouldLeaveShortcutReady() throws Exception { + public void testClickingTurnOnButtonInDialog_shouldLeaveShortcutReady() throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); @@ -393,8 +401,8 @@ public class AccessibilityShortcutControllerTest { ArgumentCaptor<DialogInterface.OnClickListener> captor = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.leave_accessibility_shortcut_on), - captor.capture()); + verify(mAlertDialogBuilder).setNegativeButton(eq(R.string.accessibility_shortcut_on), + captor.capture()); // Call the button callback, if one exists if (captor.getValue() != null) { captor.getValue().onClick(null, 0); @@ -402,7 +410,7 @@ public class AccessibilityShortcutControllerTest { assertEquals(SERVICE_NAME_STRING, Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)); assertEquals(1, Settings.Secure.getInt( - mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)); + mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)); } @Test diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java index 0390ac6b8e9c..1cdc75aa1f40 100644 --- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java +++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java @@ -225,7 +225,8 @@ public class ActivityThreadClientTest { CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null /* referrer */, null /* voiceInteractor */, null /* state */, null /* persistentState */, null /* pendingResults */, null /* pendingNewIntents */, true /* isForward */, - null /* profilerInfo */, mThread /* client */, null /* asssitToken */); + null /* profilerInfo */, mThread /* client */, null /* asssitToken */, + null /* fixedRotationAdjustments */); } @Override diff --git a/data/etc/car/Android.bp b/data/etc/car/Android.bp index dfb7a16e6771..1b1a624cda50 100644 --- a/data/etc/car/Android.bp +++ b/data/etc/car/Android.bp @@ -87,6 +87,13 @@ prebuilt_etc { } prebuilt_etc { + name: "privapp_whitelist_com.android.car.secondaryhome", + sub_dir: "permissions", + src: "com.android.car.secondaryhome.xml", + filename_from_src: true, +} + +prebuilt_etc { name: "privapp_whitelist_com.android.car.settings", sub_dir: "permissions", src: "com.android.car.settings.xml", diff --git a/data/etc/car/com.android.car.secondaryhome.xml b/data/etc/car/com.android.car.secondaryhome.xml index c74b86ed8ae1..a8af90683db5 100644 --- a/data/etc/car/com.android.car.secondaryhome.xml +++ b/data/etc/car/com.android.car.secondaryhome.xml @@ -20,5 +20,7 @@ <permission name="android.permission.ACTIVITY_EMBEDDING"/> <!-- Required to send notification to current user--> <permission name="android.permission.MANAGE_USERS"/> + <!-- Required for CarNotificationLib --> + <permission name="android.permission.INTERACT_ACROSS_USERS"/> </privapp-permissions> </permissions> diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 97b448aa8ff0..c8f065ad094c 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -70,9 +70,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * {@link Bitmap} objects. * * <p>To use it, first create a {@link Source Source} using one of the - * {@code createSource} overloads. For example, to decode from a {@link File}, call - * {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)} - * or {@link #decodeBitmap(Source)}: + * {@code createSource} overloads. For example, to decode from a {@link Uri}, call + * {@link #createSource(ContentResolver, Uri)} and pass the result to + * {@link #decodeDrawable(Source)} or {@link #decodeBitmap(Source)}: * * <pre class="prettyprint"> * File file = new File(...); @@ -1032,7 +1032,11 @@ public final class ImageDecoder implements AutoCloseable { /** * Create a new {@link Source Source} from a {@link java.io.File}. - * + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with files hosted outside your app, use an API like + * {@link #createSource(Callable)} or + * {@link #createSource(ContentResolver, Uri)}. * @return a new Source object, which can be passed to * {@link #decodeDrawable decodeDrawable} or * {@link #decodeBitmap decodeBitmap}. diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 7d14ef5542cc..6179b483ca53 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -98,7 +98,7 @@ public final class MediaRouter2 { final Handler mHandler; @GuardedBy("sRouterLock") - private boolean mShouldUpdateRoutes; + private boolean mShouldUpdateRoutes = true; private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList(); private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 0dc019cc7abd..4ebfce830a70 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -100,6 +100,7 @@ public final class MediaRouter2Manager { .getSystemService(Context.MEDIA_SESSION_SERVICE); mPackageName = mContext.getPackageName(); mHandler = new Handler(context.getMainLooper()); + mHandler.post(this::getOrCreateClient); } /** @@ -118,18 +119,6 @@ public final class MediaRouter2Manager { Log.w(TAG, "Ignoring to add the same callback twice."); return; } - - synchronized (sLock) { - if (mClient == null) { - Client client = new Client(); - try { - mMediaRouterService.registerManager(client, mPackageName); - mClient = client; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to register media router manager.", ex); - } - } - } } /** @@ -144,21 +133,6 @@ public final class MediaRouter2Manager { Log.w(TAG, "unregisterCallback: Ignore unknown callback. " + callback); return; } - - synchronized (sLock) { - if (mCallbackRecords.size() == 0) { - if (mClient != null) { - try { - mMediaRouterService.unregisterManager(mClient); - } catch (RemoteException ex) { - Log.e(TAG, "Unable to unregister media router manager", ex); - } - mClient = null; - } - mRoutes.clear(); - mPreferredFeaturesMap.clear(); - } - } } /** @@ -314,10 +288,7 @@ public final class MediaRouter2Manager { */ @NonNull public List<RoutingSessionInfo> getActiveSessions() { - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { return mMediaRouterService.getActiveSessions(client); @@ -380,10 +351,7 @@ public final class MediaRouter2Manager { return; } - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); @@ -419,10 +387,7 @@ public final class MediaRouter2Manager { return; } - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); @@ -451,10 +416,7 @@ public final class MediaRouter2Manager { return; } - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); @@ -710,15 +672,12 @@ public final class MediaRouter2Manager { return; } - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); mMediaRouterService.selectRouteWithManager( - mClient, requestId, sessionInfo.getId(), route); + client, requestId, sessionInfo.getId(), route); } catch (RemoteException ex) { Log.e(TAG, "selectRoute: Failed to send a request.", ex); } @@ -755,15 +714,12 @@ public final class MediaRouter2Manager { return; } - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); mMediaRouterService.deselectRouteWithManager( - mClient, requestId, sessionInfo.getId(), route); + client, requestId, sessionInfo.getId(), route); } catch (RemoteException ex) { Log.e(TAG, "deselectRoute: Failed to send a request.", ex); } @@ -794,14 +750,11 @@ public final class MediaRouter2Manager { int requestId = mNextRequestId.getAndIncrement(); mTransferRequests.add(new TransferRequest(requestId, sessionInfo, route)); - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { mMediaRouterService.transferToRouteWithManager( - mClient, requestId, sessionInfo.getId(), route); + client, requestId, sessionInfo.getId(), route); } catch (RemoteException ex) { Log.e(TAG, "transferToRoute: Failed to send a request.", ex); } @@ -821,15 +774,12 @@ public final class MediaRouter2Manager { public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); - Client client; - synchronized (sLock) { - client = mClient; - } + Client client = getOrCreateClient(); if (client != null) { try { int requestId = mNextRequestId.getAndIncrement(); mMediaRouterService.releaseSessionWithManager( - mClient, requestId, sessionInfo.getId()); + client, requestId, sessionInfo.getId()); } catch (RemoteException ex) { Log.e(TAG, "releaseSession: Failed to send a request", ex); } @@ -857,6 +807,23 @@ public final class MediaRouter2Manager { sessionInfo.getOwnerPackageName()); } + private Client getOrCreateClient() { + synchronized (sLock) { + if (mClient != null) { + return mClient; + } + Client client = new Client(); + try { + mMediaRouterService.registerManager(client, mPackageName); + mClient = client; + return client; + } catch (RemoteException ex) { + Log.e(TAG, "Unable to register media router manager.", ex); + } + } + return null; + } + /** * Interface for receiving events about media routing changes. */ diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index b5e221324c97..c4d27eca02f8 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -23,8 +23,6 @@ import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.media.projection.IMediaProjection; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -86,6 +84,12 @@ public final class MediaProjectionManager { * capture request. Will be null if the result from the * startActivityForResult() is anything other than RESULT_OK. * + * Starting from Android {@link android.os.Build.VERSION_CODES#R}, if your application requests + * the {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission, and the + * user has not explicitly denied it, the permission will be automatically granted until the + * projection is stopped. This allows for user controls to be displayed on top of the screen + * being captured. + * * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, * int, android.content.Intent)} * @param resultData The resulting data from {@link android.app.Activity#onActivityResult(int, diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index eee797af40d7..c05c21cf2752 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -603,6 +603,11 @@ public class MediaRouter2ManagerTest { assertTrue(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } + @Test + public void testGetActiveSessions_returnsNonEmptyList() { + assertFalse(mManager.getActiveSessions().isEmpty()); + } + Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> routeFeatures) throws Exception { CountDownLatch addedLatch = new CountDownLatch(1); diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 0c70e104f9a6..8f919c3d86ca 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -275,6 +275,13 @@ public class ExternalStorageProvider extends FileSystemProvider { return projection != null ? projection : DEFAULT_ROOT_PROJECTION; } + @Override + public Cursor queryChildDocumentsForManage( + String parentDocId, String[] projection, String sortOrder) + throws FileNotFoundException { + return queryChildDocumentsShowAll(parentDocId, projection, sortOrder); + } + /** * Check that the directory is the root of storage or blocked file from tree. * diff --git a/packages/InputDevices/OWNERS b/packages/InputDevices/OWNERS new file mode 100644 index 000000000000..0313a40f7270 --- /dev/null +++ b/packages/InputDevices/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +svv@google.com diff --git a/packages/SystemUI/res/color/kg_user_switcher_rounded_background_color.xml b/packages/SystemUI/res/color/kg_user_switcher_rounded_background_color.xml index b16d038f68f2..3660dc4b17f0 100644 --- a/packages/SystemUI/res/color/kg_user_switcher_rounded_background_color.xml +++ b/packages/SystemUI/res/color/kg_user_switcher_rounded_background_color.xml @@ -17,6 +17,7 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_activated="true" android:color="@color/kg_user_switcher_activated_background_color" /> + <item android:state_activated="true" android_state_enabled="true" android:color="@color/kg_user_switcher_activated_background_color" /> + <item android:state_pressed="true" android:state_enabled="true" android:color="@color/kg_user_switcher_activated_background_color" /> <item android:color="@android:color/transparent" /> -</selector>
\ No newline at end of file +</selector> diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index e79c9a40918c..80db3bec86c1 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -19,4 +19,4 @@ systemui:rippleMinSize="30dp" systemui:rippleMaxSize="135dp" systemui:highlight="15" - systemui:cornerRadius="@dimen/qs_media_corner_radius" />
\ No newline at end of file + systemui:cornerRadius="?android:attr/dialogCornerRadius" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index d89f329039c6..da76c8d0b11a 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" style="@style/TextAppearance.AuthCredential.Description"/> - <EditText + <ImeAwareEditText android:id="@+id/lockPassword" android:layout_width="208dp" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/layout/keyguard_media_header.xml b/packages/SystemUI/res/layout/keyguard_media_header.xml index 20ec10ca1e1b..a520719566ab 100644 --- a/packages/SystemUI/res/layout/keyguard_media_header.xml +++ b/packages/SystemUI/res/layout/keyguard_media_header.xml @@ -45,109 +45,4 @@ android:layout_height="match_parent" /> - <!-- Layout for media controls. --> - <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/keyguard_media_view" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center" - android:padding="16dp" - > - <ImageView - android:id="@+id/album_art" - android:layout_width="@dimen/qs_media_album_size" - android:layout_height="@dimen/qs_media_album_size" - android:layout_marginRight="16dp" - android:layout_weight="0" - /> - - <!-- Media information --> - <LinearLayout - android:orientation="vertical" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - > - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/app_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Song name --> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:singleLine="true" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:textSize="18sp" - android:paddingBottom="6dp" - android:gravity="center"/> - - <!-- Artist name --> - <TextView - android:id="@+id/header_artist" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center" - android:layout_gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - </LinearLayout> - </LinearLayout> - </com.android.systemui.statusbar.notification.stack.MediaHeaderView> diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 149446c55fc5..03e74676f03e 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -16,20 +16,22 @@ --> <!-- Carousel for media controls --> -<HorizontalScrollView +<com.android.systemui.media.UnboundHorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/qs_media_padding" android:scrollbars="none" - android:visibility="gone" + android:clipChildren="false" + android:clipToPadding="false" > <LinearLayout android:id="@+id/media_carousel" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" + android:clipChildren="false" + android:clipToPadding="false" > <!-- QSMediaPlayers will be added here dynamically --> </LinearLayout> -</HorizontalScrollView> +</com.android.systemui.media.UnboundHorizontalScrollView> diff --git a/packages/SystemUI/res/layout/qqs_media_panel.xml b/packages/SystemUI/res/layout/qqs_media_panel.xml deleted file mode 100644 index 2e86732f3cad..000000000000 --- a/packages/SystemUI/res/layout/qqs_media_panel.xml +++ /dev/null @@ -1,90 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2019 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 - --> - -<!-- Layout for QQS media controls --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/qqs_media_controls" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:gravity="center" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:paddingBottom="12dp" - android:background="@drawable/qs_media_background" - > - <!-- Top line: icon + song name --> - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:clipChildren="false" - android:gravity="center" - android:layout_marginBottom="12dp" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="14dp" - android:layout_height="14dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Bottom section: controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - </LinearLayout> -</LinearLayout> diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 0c9ce3938420..ebfd0a0fd537 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -23,7 +23,6 @@ android:layout_height="@dimen/qs_footer_height" android:layout_marginStart="@dimen/qs_footer_margin" android:layout_marginEnd="@dimen/qs_footer_margin" - android:elevation="4dp" android:background="@android:color/transparent" android:baselineAligned="false" android:clickable="false" @@ -128,13 +127,4 @@ </com.android.systemui.statusbar.AlphaOptimizedFrameLayout> </com.android.keyguard.AlphaOptimizedLinearLayout> </LinearLayout> - <View - android:id="@+id/qs_drag_handle_view" - android:layout_width="48dp" - android:layout_height="4dp" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" - android:layout_gravity="center_horizontal|bottom" - android:background="@drawable/qs_footer_drag_handle" /> - </com.android.systemui.qs.QSFooterImpl> diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml index d633ff40df9e..9ad380d260c0 100644 --- a/packages/SystemUI/res/layout/qs_media_panel.xml +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -16,236 +16,173 @@ --> <!-- Layout for media controls inside QSPanel carousel --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/qs_media_controls" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" android:gravity="center_horizontal|fill_vertical" - android:paddingTop="@dimen/qs_media_panel_outer_padding" - android:paddingBottom="@dimen/qs_media_panel_outer_padding" - android:background="@drawable/qs_media_background" - > + app:layoutDescription="@xml/media_scene"> - <!-- Buttons to remove this view when no longer needed --> - <include - layout="@layout/qs_media_panel_options" - android:visibility="gone"/> + <View + android:id="@+id/media_background" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/qs_media_background" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> - <LinearLayout - android:id="@+id/media_guts" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <!-- Header section --> - <LinearLayout - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="16dp" - > - - <ImageView - android:id="@+id/album_art" - android:layout_width="@dimen/qs_media_album_size" - android:layout_height="@dimen/qs_media_album_size" - android:layout_marginRight="16dp" - android:layout_weight="0" - /> - - <LinearLayout - android:orientation="vertical" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - > - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/app_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Song name --> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:singleLine="true" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:textSize="18sp" - android:paddingBottom="6dp" - android:gravity="center"/> - - <!-- Artist name --> - <TextView - android:id="@+id/header_artist" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Output chip --> - <LinearLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:visibility="gone" - android:paddingTop="6dp" - android:paddingBottom="6dp" - android:paddingLeft="12dp" - android:paddingRight="12dp" - android:gravity="center" - android:id="@+id/media_seamless" - android:background="@*android:drawable/media_seamless_background" - android:layout_weight="1" - android:forceHasOverlappingRendering="false" - > - <ImageView - android:layout_width="@dimen/qs_seamless_icon_size" - android:layout_height="@dimen/qs_seamless_icon_size" - android:src="@*android:drawable/ic_media_seamless" - android:layout_marginRight="8dp" - android:id="@+id/media_seamless_image" - /> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:text="@*android:string/ext_media_seamless_action" - android:textSize="14sp" - android:id="@+id/media_seamless_text" - android:singleLine="true" - /> - </LinearLayout> - </LinearLayout> - - <!-- Seek Bar --> - <SeekBar - android:id="@+id/media_progress_bar" - style="@android:style/Widget.ProgressBar.Horizontal" - android:clickable="true" + <FrameLayout + android:id="@+id/notification_media_progress_time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:forceHasOverlappingRendering="false"> + <!-- width is set to "match_parent" to avoid extra layout calls --> + <TextView + android:id="@+id/media_elapsed_time" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxHeight="3dp" - android:paddingTop="24dp" - android:paddingBottom="24dp" - android:layout_marginBottom="-24dp" - android:layout_marginTop="-24dp" - android:splitTrack="false" - /> + android:layout_alignParentLeft="true" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:gravity="left" + android:textSize="14sp" /> - <FrameLayout - android:id="@+id/notification_media_progress_time" + <TextView + android:id="@+id/media_total_time" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="@dimen/qs_media_panel_outer_padding" - android:layout_marginBottom="10dp" - android:layout_gravity="center" - > - <!-- width is set to "match_parent" to avoid extra layout calls --> - <TextView - android:id="@+id/media_elapsed_time" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:gravity="left" - /> - <TextView - android:id="@+id/media_total_time" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:layout_alignParentRight="true" - android:textSize="14sp" - android:gravity="right" - /> - </FrameLayout> - - <!-- Controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="match_parent" + android:layout_alignParentRight="true" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:gravity="right" + android:textSize="14sp" /> + </FrameLayout> + + <ImageButton + android:id="@+id/action0" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action1" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action2" + style="@style/MediaPlayer.Button" + android:layout_width="52dp" + android:layout_height="52dp" /> + + <ImageButton + android:id="@+id/action3" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action4" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <!-- Album Art --> + <ImageView + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" /> + + <!-- Seamless Output Switcher --> + <LinearLayout + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="@*android:drawable/media_seamless_background" + android:orientation="horizontal" + android:forceHasOverlappingRendering="false" + android:paddingLeft="12dp" + android:paddingTop="6dp" + android:paddingRight="12dp" + android:paddingBottom="6dp"> + + <ImageView + android:id="@+id/media_seamless_image" + android:layout_width="@dimen/qs_seamless_icon_size" + android:layout_height="@dimen/qs_seamless_icon_size" + android:layout_marginRight="8dp" + android:src="@*android:drawable/ic_media_seamless" /> + + <TextView + android:id="@+id/media_seamless_text" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="@dimen/qs_media_panel_outer_padding" - android:gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="52dp" - android:layout_height="52dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action3" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action4" - /> - </LinearLayout> + android:fontFamily="@*android:string/config_bodyFontFamily" + android:singleLine="true" + android:text="@*android:string/ext_media_seamless_action" + android:textSize="14sp" /> </LinearLayout> -</LinearLayout> + + <!-- Seek Bar --> + <SeekBar + android:id="@+id/media_progress_bar" + style="@android:style/Widget.ProgressBar.Horizontal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:clickable="true" + android:maxHeight="3dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:splitTrack="false" /> + + <!-- App name --> + <TextView + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:singleLine="true" + android:textSize="14sp" /> + + <!-- Song name --> + <TextView + android:id="@+id/header_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:singleLine="true" + android:textSize="18sp" /> + + <!-- Artist name --> + <TextView + android:id="@+id/header_artist" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:singleLine="true" + android:textSize="14sp" /> + + <com.android.internal.widget.CachingIconView + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" /> + + <!-- Buttons to remove this view when no longer needed --> + <include + layout="@layout/qs_media_panel_options" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/view_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="300dp" /> +</androidx.constraintlayout.motion.widget.MotionLayout> diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index 01dfeb281e1b..cdf84260e399 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -54,20 +54,32 @@ android:layout_marginTop="@*android:dimen/quick_qs_offset_height" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/qs_footer_height" android:elevation="4dp" android:background="@android:color/transparent" android:focusable="true" - android:accessibilityTraversalBefore="@android:id/edit" - /> + android:accessibilityTraversalBefore="@android:id/edit"> + <include layout="@layout/qs_footer_impl" /> + </com.android.systemui.qs.QSPanel> <include layout="@layout/quick_status_bar_expanded_header" /> - <include layout="@layout/qs_footer_impl" /> - <include android:id="@+id/qs_detail" layout="@layout/qs_detail" /> <include android:id="@+id/qs_customize" layout="@layout/qs_customize_panel" android:visibility="gone" /> + <FrameLayout + android:id="@+id/qs_drag_handle_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:elevation="4dp" + android:paddingBottom="5dp"> + <View + android:layout_width="46dp" + android:layout_height="3dp" + android:background="@drawable/qs_footer_drag_handle" /> + </FrameLayout> + + </com.android.systemui.qs.QSContainerImpl> diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index e99b91787072..9a7c344baf20 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" - android:layout_height="@*android:dimen/quick_qs_total_height" + android:layout_height="wrap_content" android:layout_gravity="@integer/notification_panel_layout_gravity" android:background="@android:color/transparent" android:baselineAligned="false" @@ -29,6 +29,7 @@ android:clipToPadding="false" android:paddingTop="0dp" android:paddingEnd="0dp" + android:paddingBottom="10dp" android:paddingStart="0dp" android:elevation="4dp" > @@ -45,8 +46,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/quick_qs_status_icons" - android:layout_marginStart="@dimen/qs_header_tile_margin_horizontal" - android:layout_marginEnd="@dimen/qs_header_tile_margin_horizontal" android:accessibilityTraversalAfter="@+id/date_time_group" android:accessibilityTraversalBefore="@id/expand_indicator" android:clipChildren="false" @@ -54,15 +53,6 @@ android:focusable="true" android:importantForAccessibility="yes" /> - <com.android.systemui.statusbar.AlphaOptimizedImageView - android:id="@+id/qs_detail_header_progress" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:alpha="0" - android:background="@color/qs_detail_progress_track" - android:src="@drawable/indeterminate_anim"/> - <TextView android:id="@+id/header_debug_info" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 99e347eb1a69..b4a05c6da780 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -498,6 +498,7 @@ <dimen name="qs_quick_tile_padding">12dp</dimen> <dimen name="qs_header_gear_translation">16dp</dimen> <dimen name="qs_header_tile_margin_horizontal">4dp</dimen> + <dimen name="qs_header_tile_margin_bottom">18dp</dimen> <dimen name="qs_page_indicator_width">16dp</dimen> <dimen name="qs_page_indicator_height">8dp</dimen> <dimen name="qs_tile_icon_size">24dp</dimen> @@ -1043,6 +1044,10 @@ <dimen name="bottom_padding">48dp</dimen> <dimen name="edge_margin">8dp</dimen> + <!-- The absolute side margins of quick settings --> + <dimen name="quick_settings_side_margins">16dp</dimen> + <dimen name="quick_settings_expanded_bottom_margin">16dp</dimen> + <dimen name="quick_settings_media_extra_bottom_margin">4dp</dimen> <dimen name="rounded_corner_content_padding">0dp</dimen> <dimen name="nav_content_padding">0dp</dimen> <dimen name="nav_quick_scrub_track_edge_padding">24dp</dimen> @@ -1230,12 +1235,12 @@ <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_width">350dp</dimen> - <dimen name="qs_media_padding">8dp</dimen> + <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_panel_outer_padding">16dp</dimen> - <dimen name="qs_media_corner_radius">10dp</dimen> - <dimen name="qs_media_album_size">72dp</dimen> + <dimen name="qs_media_album_size">52dp</dimen> <dimen name="qs_seamless_icon_size">20dp</dimen> <dimen name="qqs_media_spacing">8dp</dimen> + <dimen name="qqs_horizonal_tile_padding_bottom">8dp</dimen> <dimen name="magnification_border_size">5dp</dimen> <dimen name="magnification_frame_move_short">5dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 8156e8dc9bf1..76ca385bd9d9 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -70,6 +70,26 @@ <item type="id" name="panel_alpha_animator_end_tag"/> <item type="id" name="cross_fade_layer_type_changed_tag"/> + <item type="id" name="absolute_x_animator_tag"/> + <item type="id" name="absolute_x_animator_start_tag"/> + <item type="id" name="absolute_x_animator_end_tag"/> + <item type="id" name="absolute_x_current_value"/> + + <item type="id" name="absolute_y_animator_tag"/> + <item type="id" name="absolute_y_animator_start_tag"/> + <item type="id" name="absolute_y_animator_end_tag"/> + <item type="id" name="absolute_y_current_value"/> + + <item type="id" name="view_height_animator_tag"/> + <item type="id" name="view_height_animator_start_tag"/> + <item type="id" name="view_height_animator_end_tag"/> + <item type="id" name="view_height_current_value"/> + + <item type="id" name="view_width_animator_tag"/> + <item type="id" name="view_width_animator_start_tag"/> + <item type="id" name="view_width_animator_end_tag"/> + <item type="id" name="view_width_current_value"/> + <!-- Whether the icon is from a notification for which targetSdk < L --> <item type="id" name="icon_is_pre_L"/> diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml new file mode 100644 index 000000000000..f61b2b096d3c --- /dev/null +++ b/packages/SystemUI/res/xml/media_scene.xml @@ -0,0 +1,447 @@ +<?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 + --> +<MotionScene + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <Transition + app:constraintSetStart="@id/collapsed" + app:constraintSetEnd="@id/expanded" + app:duration="1000" > + <KeyFrameSet > + <KeyPosition + app:motionTarget="@+id/action0" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action1" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action2" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action3" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action4" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/media_progress_bar" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyAttribute + app:motionTarget="@id/media_progress_bar" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/media_progress_bar" + app:framePosition="70" + android:alpha="0.0"/> + <KeyPosition + app:motionTarget="@+id/notification_media_progress_time" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyAttribute + app:motionTarget="@id/notification_media_progress_time" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/notification_media_progress_time" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action0" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action0" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action1" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action1" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action2" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action2" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action3" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action3" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action4" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action4" + app:framePosition="70" + android:alpha="0.0"/> + </KeyFrameSet> + </Transition> + + <ConstraintSet android:id="@+id/expanded"> + <Constraint + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginStart="18dp" + android:layout_marginTop="22dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> + + <Constraint + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="10dp" + android:layout_marginStart="10dp" + android:layout_marginTop="20dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintEnd_toStartOf="@id/media_seamless" + app:layout_constraintHorizontal_bias="0" + /> + + <Constraint + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_min="60dp" + android:layout_marginTop="@dimen/qs_media_panel_outer_padding" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + /> + + <Constraint + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" + android:layout_marginTop="14dp" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@+id/app_name" + app:layout_constraintStart_toStartOf="parent" + /> + + <!-- Song name --> + <Constraint + android:id="@+id/header_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginTop="17dp" + android:layout_marginStart="16dp" + app:layout_constraintTop_toBottomOf="@+id/app_name" + app:layout_constraintStart_toEndOf="@id/album_art" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Artist name --> + <Constraint + android:id="@+id/header_artist" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginTop="3dp" + app:layout_constraintTop_toBottomOf="@id/header_title" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Seek Bar --> + <Constraint + android:id="@+id/media_progress_bar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="3dp" + app:layout_constraintTop_toBottomOf="@id/header_artist" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + /> + + <Constraint + android:id="@+id/notification_media_progress_time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="38dp" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@id/header_artist" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + /> + + <Constraint + android:id="@+id/action0" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginTop="5dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@id/action1" + app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action1" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action0" + app:layout_constraintRight_toLeftOf="@id/action2" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action2" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action1" + app:layout_constraintRight_toLeftOf="@id/action3" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action3" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + app:layout_constraintLeft_toRightOf="@id/action2" + app:layout_constraintRight_toLeftOf="@id/action4" + app:layout_constraintTop_toTopOf="@id/action0" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action4" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action3" + app:layout_constraintRight_toRightOf="@id/view_width" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + </ConstraintSet> + + <ConstraintSet android:id="@+id/collapsed"> + <Constraint + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginStart="18dp" + android:layout_marginTop="22dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> + + <Constraint + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="10dp" + android:layout_marginStart="10dp" + android:layout_marginTop="20dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintEnd_toStartOf="@id/media_seamless" + app:layout_constraintHorizontal_bias="0" + /> + + <Constraint + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_min="60dp" + android:layout_marginTop="@dimen/qs_media_panel_outer_padding" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + /> + + <Constraint + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" + android:layout_marginTop="16dp" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + android:layout_marginBottom="24dp" + app:layout_constraintTop_toBottomOf="@id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> + + <!-- Song name --> + <Constraint + android:id="@+id/header_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="17dp" + android:layout_marginStart="16dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintBottom_toTopOf="@id/header_artist" + app:layout_constraintStart_toEndOf="@id/album_art" + app:layout_constraintEnd_toStartOf="@id/action0" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Artist name --> + <Constraint + android:id="@+id/header_artist" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="3dp" + android:layout_marginBottom="24dp" + app:layout_constraintTop_toBottomOf="@id/header_title" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Seek Bar --> + <Constraint + android:id="@+id/media_progress_bar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:alpha="0.0" + app:layout_constraintTop_toBottomOf="@id/album_art" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + android:visibility="gone" + /> + + <Constraint + android:id="@+id/notification_media_progress_time" + android:alpha="0.0" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="35dp" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@id/album_art" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + android:visibility="gone" + /> + + <Constraint + android:id="@+id/action0" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginTop="16dp" + android:visibility="gone" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/header_title" + app:layout_constraintRight_toLeftOf="@id/action1" + > + </Constraint> + + <Constraint + android:id="@+id/action1" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action0" + app:layout_constraintRight_toLeftOf="@id/action2" + > + </Constraint> + + <Constraint + android:id="@+id/action2" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action1" + app:layout_constraintRight_toLeftOf="@id/action3" + > + </Constraint> + + <Constraint + android:id="@+id/action3" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action2" + app:layout_constraintRight_toLeftOf="@id/action4" + > + </Constraint> + + <Constraint + android:id="@+id/action4" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:visibility="gone" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action3" + app:layout_constraintRight_toRightOf="@id/view_width" + > + </Constraint> + </ConstraintSet> +</MotionScene> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java deleted file mode 100644 index af5196f92bcb..000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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.keyguard; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; -import androidx.palette.graphics.Palette; - -import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.media.MediaControllerFactory; -import com.android.systemui.statusbar.notification.MediaNotificationProcessor; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.stack.MediaHeaderView; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Media controls to display on the lockscreen - * - * TODO: Should extend MediaControlPanel to avoid code duplication. - * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is - * different. - */ -@Singleton -public class KeyguardMediaPlayer { - - private static final String TAG = "KeyguardMediaPlayer"; - // Buttons that can be displayed on lock screen media controls. - private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - private final Context mContext; - private final Executor mBackgroundExecutor; - private final KeyguardMediaViewModel mViewModel; - private KeyguardMediaObserver mObserver; - - @Inject - public KeyguardMediaPlayer(Context context, MediaControllerFactory factory, - @Background Executor backgroundExecutor) { - mContext = context; - mBackgroundExecutor = backgroundExecutor; - mViewModel = new KeyguardMediaViewModel(context, factory); - } - - /** Binds media controls to a view hierarchy. */ - public void bindView(View v) { - if (mObserver != null) { - throw new IllegalStateException("cannot bind views, already bound"); - } - mViewModel.loadDimens(); - mObserver = new KeyguardMediaObserver(v); - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = v.findViewById(ACTION_IDS[i]); - if (button == null) { - continue; - } - final int index = i; - button.setOnClickListener(unused -> mViewModel.onActionClick(index)); - } - mViewModel.getKeyguardMedia().observeForever(mObserver); - } - - /** Unbinds media controls. */ - public void unbindView() { - if (mObserver == null) { - throw new IllegalStateException("cannot unbind views, nothing bound"); - } - mViewModel.getKeyguardMedia().removeObserver(mObserver); - mObserver = null; - } - - /** Clear the media controls because there isn't an active session. */ - public void clearControls() { - mBackgroundExecutor.execute(mViewModel::clearControls); - } - - /** - * Update the media player - * - * TODO: consider registering a MediaLister instead of exposing this update method. - * - * @param entry Media notification that will be used to update the player - * @param appIcon Icon for the app playing the media - * @param mediaMetadata Media metadata that will be used to update the player - */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - if (mObserver == null) { - throw new IllegalStateException("cannot update controls, views not bound"); - } - if (mediaMetadata == null) { - Log.d(TAG, "media metadata was null, closing media controls"); - // Note that clearControls() executes on the same background executor, so there - // shouldn't be an issue with an outdated update running after clear. However, if stale - // controls are observed then consider removing any enqueued updates. - clearControls(); - return; - } - mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata)); - } - - /** ViewModel for KeyguardMediaControls. */ - private static final class KeyguardMediaViewModel { - - private final Context mContext; - private final MediaControllerFactory mMediaControllerFactory; - private final MutableLiveData<KeyguardMedia> mMedia = new MutableLiveData<>(); - private final Object mActionsLock = new Object(); - private List<PendingIntent> mActions; - private float mAlbumArtRadius; - private int mAlbumArtSize; - - KeyguardMediaViewModel(Context context, MediaControllerFactory factory) { - mContext = context; - mMediaControllerFactory = factory; - loadDimens(); - } - - /** Close the media player because there isn't an active session. */ - public void clearControls() { - synchronized (mActionsLock) { - mActions = null; - } - mMedia.postValue(null); - } - - /** Update the media player with information about the active session. */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - - // Check the playback state of the media controller. If it is null, then the session was - // probably destroyed. Don't update in this case. - final MediaSession.Token token = entry.getSbn().getNotification().extras - .getParcelable(Notification.EXTRA_MEDIA_SESSION); - final MediaController controller = token != null - ? mMediaControllerFactory.create(token) : null; - if (controller != null && controller.getPlaybackState() == null) { - clearControls(); - return; - } - - // Foreground and Background colors computed from album art - Notification notif = entry.getSbn().getNotification(); - int fgColor = notif.color; - int bgColor = entry.getRow() == null ? -1 : entry.getRow().getCurrentBackgroundTint(); - Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - if (artworkBitmap == null) { - artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - if (artworkBitmap != null) { - // If we have art, get colors from that - Palette p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) - .generate(); - Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(p); - bgColor = swatch.getRgb(); - fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p); - } - // Make sure colors will be legible - boolean isDark = !ContrastColorUtil.isColorLight(bgColor); - fgColor = ContrastColorUtil.resolveContrastColor(mContext, fgColor, bgColor, - isDark); - fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark); - - // Album art - RoundedBitmapDrawable artwork = null; - if (artworkBitmap != null) { - Bitmap original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true); - Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize, - false); - artwork = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); - artwork.setCornerRadius(mAlbumArtRadius); - } - - // App name - Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif); - String app = builder.loadHeaderAppName(); - - // App Icon - Drawable appIconDrawable = appIcon.loadDrawable(mContext); - - // Song name - String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - - // Artist name - String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - - // Control buttons - List<Drawable> actionIcons = new ArrayList<>(); - final List<PendingIntent> intents = new ArrayList<>(); - Notification.Action[] actions = notif.actions; - final int[] actionsToShow = notif.extras.getIntArray( - Notification.EXTRA_COMPACT_ACTIONS); - - Context packageContext = entry.getSbn().getPackageContext(mContext); - for (int i = 0; i < ACTION_IDS.length; i++) { - if (actionsToShow != null && actions != null && i < actionsToShow.length - && actionsToShow[i] < actions.length) { - final int idx = actionsToShow[i]; - actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext)); - intents.add(actions[idx].actionIntent); - } else { - actionIcons.add(null); - intents.add(null); - } - } - synchronized (mActionsLock) { - mActions = intents; - } - - KeyguardMedia data = new KeyguardMedia(fgColor, bgColor, app, appIconDrawable, artist, - song, artwork, actionIcons); - mMedia.postValue(data); - } - - /** Gets state for the lock screen media controls. */ - public LiveData<KeyguardMedia> getKeyguardMedia() { - return mMedia; - } - - /** - * Handle user clicks on media control buttons (actions). - * - * @param index position of the button that was clicked. - */ - public void onActionClick(int index) { - PendingIntent intent = null; - // This might block the ui thread to wait for the lock. Currently, however, the - // lock is held by the bg thread to assign a member, which should be fast. An - // alternative could be to add the intents to the state and let the observer set - // the onClick listeners. - synchronized (mActionsLock) { - if (mActions != null && index < mActions.size()) { - intent = mActions.get(index); - } - } - if (intent != null) { - try { - intent.send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "failed to send action intent", e); - } - } - } - - void loadDimens() { - mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - mAlbumArtSize = (int) mContext.getResources().getDimension( - R.dimen.qs_media_album_size); - } - } - - /** Observer for state changes of lock screen media controls. */ - private static final class KeyguardMediaObserver implements Observer<KeyguardMedia> { - - private final View mRootView; - private final MediaHeaderView mMediaHeaderView; - private final ImageView mAlbumView; - private final ImageView mAppIconView; - private final TextView mAppNameView; - private final TextView mTitleView; - private final TextView mArtistView; - private final List<ImageButton> mButtonViews = new ArrayList<>(); - - KeyguardMediaObserver(View v) { - mRootView = v; - mMediaHeaderView = v instanceof MediaHeaderView ? (MediaHeaderView) v : null; - mAlbumView = v.findViewById(R.id.album_art); - mAppIconView = v.findViewById(R.id.icon); - mAppNameView = v.findViewById(R.id.app_name); - mTitleView = v.findViewById(R.id.header_title); - mArtistView = v.findViewById(R.id.header_artist); - for (int i = 0; i < ACTION_IDS.length; i++) { - mButtonViews.add(v.findViewById(ACTION_IDS[i])); - } - } - - /** Updates lock screen media player views when state changes. */ - @Override - public void onChanged(KeyguardMedia data) { - if (data == null) { - mRootView.setVisibility(View.GONE); - return; - } - mRootView.setVisibility(View.VISIBLE); - - // Background color - if (mMediaHeaderView != null) { - mMediaHeaderView.setBackgroundColor(data.getBackgroundColor()); - } - - // Album art - if (mAlbumView != null) { - mAlbumView.setImageDrawable(data.getArtwork()); - mAlbumView.setVisibility(data.getArtwork() == null ? View.GONE : View.VISIBLE); - } - - // App icon - if (mAppIconView != null) { - Drawable iconDrawable = data.getAppIcon(); - iconDrawable.setTint(data.getForegroundColor()); - mAppIconView.setImageDrawable(iconDrawable); - } - - // App name - if (mAppNameView != null) { - String appNameString = data.getApp(); - mAppNameView.setText(appNameString); - mAppNameView.setTextColor(data.getForegroundColor()); - } - - // Song name - if (mTitleView != null) { - mTitleView.setText(data.getSong()); - mTitleView.setTextColor(data.getForegroundColor()); - } - - // Artist name - if (mArtistView != null) { - mArtistView.setText(data.getArtist()); - mArtistView.setTextColor(data.getForegroundColor()); - } - - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = mButtonViews.get(i); - if (button == null) { - continue; - } - Drawable icon = data.getActionIcons().get(i); - if (icon == null) { - button.setVisibility(View.GONE); - button.setImageDrawable(null); - } else { - button.setVisibility(View.VISIBLE); - button.setImageDrawable(icon); - button.setImageTintList(ColorStateList.valueOf(data.getForegroundColor())); - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 96494cfe640f..3a37c0fd4634 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -451,7 +451,8 @@ public class KeyguardSliceProvider extends SliceProvider implements * @param metadata New metadata. */ @Override - public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) { + public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) { synchronized (this) { boolean nextVisible = NotificationMediaManager.isPlayingState(state); mMediaHandler.removeCallbacksAndMessages(null); diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 123cf78d74f8..9c89fee5cba1 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -61,6 +61,18 @@ public class LogModule { return buffer; } + /** Provides a logging buffer for all logs related to managing notification sections. */ + @Provides + @Singleton + @NotificationSectionLog + public static LogBuffer provideNotificationSectionLogBuffer( + LogcatEchoTracker bufferFilter, + DumpManager dumpManager) { + LogBuffer buffer = new LogBuffer("NotifSectionLog", 500, 10, bufferFilter); + buffer.attach(dumpManager); + return buffer; + } + /** Provides a logging buffer for all logs related to the data layer of notifications. */ @Provides @Singleton diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java index 487c29573a14..7259eebf19b6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java @@ -14,20 +14,20 @@ * limitations under the License. */ -package com.android.keyguard +package com.android.systemui.log.dagger; -import android.graphics.drawable.Drawable +import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.util.List +import com.android.systemui.log.LogBuffer; -/** State for lock screen media controls. */ -data class KeyguardMedia( - val foregroundColor: Int, - val backgroundColor: Int, - val app: String?, - val appIcon: Drawable?, - val artist: String?, - val song: String?, - val artwork: Drawable?, - val actionIcons: List<Drawable> -) +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for notification sections-related messages. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface NotificationSectionLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt new file mode 100644 index 000000000000..2fe0d9f4711f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt @@ -0,0 +1,52 @@ +/* + * 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.graphics.Rect +import android.view.View +import android.view.ViewGroup + +private val EMPTY_RECT = Rect(0,0,0,0) + +private val LAYOUT_CHANGE_LISTENER = object : View.OnLayoutChangeListener { + + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, + oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + v?.let { + if (v.visibility == View.GONE) { + v.clipBounds = EMPTY_RECT + } else { + v.clipBounds = null + } + } + } +} +/** + * A helper class that clips all GONE children. Useful for transitions in motionlayout which + * don't clip its children. + */ +class GoneChildrenHideHelper private constructor() { + companion object { + @JvmStatic + fun clipGoneChildrenOnLayout(layout: ViewGroup) { + val childCount = layout.childCount + for (i in 0 until childCount) { + val child = layout.getChildAt(i) + child.addOnLayoutChangeListener(LAYOUT_CHANGE_LISTENER) + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt index 937472735bb0..743216556434 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -6,6 +6,7 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.res.ColorStateList import android.content.res.Resources +import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter @@ -49,6 +50,7 @@ private data class RippleData( @Keep class IlluminationDrawable : Drawable() { + private var themeAttrs: IntArray? = null private var cornerRadius = 0f private var highlightColor = Color.TRANSPARENT private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f) @@ -139,13 +141,41 @@ class IlluminationDrawable : Drawable() { theme: Resources.Theme? ) { val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable) - cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) - rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) - rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f + themeAttrs = a.extractThemeAttrs() + updateStateFromTypedArray(a) a.recycle() } + private fun updateStateFromTypedArray(a: TypedArray) { + if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { + cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, + cornerRadius) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) { + rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) { + rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { + rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / + 100f + } + } + + override fun canApplyTheme(): Boolean { + return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme() + } + + override fun applyTheme(t: Resources.Theme) { + super.applyTheme(t) + themeAttrs?.let { + val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable) + updateStateFromTypedArray(a) + a.recycle() + } + } + override fun setColorFilter(p0: ColorFilter?) { throw UnsupportedOperationException("Color filters are not supported") } diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt new file mode 100644 index 000000000000..524c6955ba4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -0,0 +1,71 @@ +/* + * 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.view.View +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.MediaHeaderView +import com.android.systemui.statusbar.phone.KeyguardBypassController +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class that controls the media notifications on the lock screen, handles its visibility and + * is responsible for the embedding of he media experience. + */ +@Singleton +class KeyguardMediaController @Inject constructor( + private val mediaHost: MediaHost, + private val bypassController: KeyguardBypassController, + private val statusBarStateController: SysuiStatusBarStateController +) { + + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + updateVisibility() + } + }) + } + private var view: MediaHeaderView? = null + + /** + * Attach this controller to a media view, initializing its state + */ + fun attach(mediaView: MediaHeaderView) { + view = mediaView + // First let's set the desired state that we want for this host + mediaHost.visibleChangedListener = { updateVisibility() } + mediaHost.expansion = 0.0f + mediaHost.showsOnlyActiveMedia = true + + // Let's now initialize this view, which also creates the host view for us. + mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) + mediaView.setContentView(mediaHost.hostView) + } + + private fun updateVisibility() { + val shouldBeVisible = mediaHost.visible + && !bypassController.bypassEnabled + && (statusBarStateController.state == StatusBarState.KEYGUARD || + statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER) + view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt new file mode 100644 index 000000000000..a366725a4398 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt @@ -0,0 +1,115 @@ +/* + * 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.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.systemui.statusbar.notification.AnimatableProperty +import com.android.systemui.statusbar.notification.PropertyAnimator +import com.android.systemui.statusbar.notification.stack.AnimationProperties + +/** + * A utility class that helps with animations of bound changes designed for motionlayout which + * doesn't work together with regular changeBounds. + */ +class LayoutAnimationHelper { + + private val layout: ViewGroup + private var sizeAnimationPending = false + private val desiredBounds = mutableMapOf<View, Rect>() + private val animationProperties = AnimationProperties() + private val layoutListener = object : View.OnLayoutChangeListener { + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, + oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + v?.let { + if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 || + oldTop - oldBottom == 0) { + return + } + if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) { + val rect = desiredBounds.getOrPut(v, { Rect() }) + rect.set(left, top, right, bottom) + onDesiredLocationChanged(v, rect) + } + } + } + } + + constructor(layout: ViewGroup) { + this.layout = layout + val childCount = this.layout.childCount + for (i in 0 until childCount) { + val child = this.layout.getChildAt(i) + child.addOnLayoutChangeListener(layoutListener) + } + } + + private fun onDesiredLocationChanged(v: View, rect: Rect) { + if (!sizeAnimationPending) { + applyBounds(v, rect, animate = false) + } + // We need to reapply the current bounds in every frame since the layout may override + // the layout bounds making this view jump and not all calls to apply bounds actually + // reapply them, for example if there's already an animator to the same target + reapplyProperty(v, AnimatableProperty.ABSOLUTE_X); + reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y); + reapplyProperty(v, AnimatableProperty.WIDTH); + reapplyProperty(v, AnimatableProperty.HEIGHT); + } + + private fun reapplyProperty(v: View, property: AnimatableProperty) { + property.property.set(v, property.property.get(v)) + } + + private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) { + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(), + animationProperties, animate) + } + + private fun startBoundAnimation(v: View) { + val target = desiredBounds[v] ?: return + applyBounds(v, target, animate = true) + } + + fun animatePendingSizeChange(duration: Long, delay: Long) { + animationProperties.duration = duration + animationProperties.delay = delay + if (!sizeAnimationPending) { + sizeAnimationPending = true + layout.viewTreeObserver.addOnPreDrawListener ( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + layout.viewTreeObserver.removeOnPreDrawListener(this) + sizeAnimationPending = false + val childCount = layout.childCount + for (i in 0 until childCount) { + val child = layout.getChildAt(i) + startBoundAnimation(child) + } + return true + } + }) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 557132bdf08e..60c2ed2fa2be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,10 +16,8 @@ package com.android.systemui.media; -import android.annotation.LayoutRes; import android.app.PendingIntent; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -27,35 +25,38 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; -import android.graphics.ImageDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; -import android.media.MediaDescription; -import android.media.MediaMetadata; -import android.media.ThumbnailUtils; import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.net.Uri; import android.service.media.MediaBrowserService; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.constraintlayout.motion.widget.Key; +import androidx.constraintlayout.motion.widget.KeyAttributes; +import androidx.constraintlayout.motion.widget.KeyFrames; +import androidx.constraintlayout.motion.widget.MotionLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import com.android.settingslib.Utils; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputSliceConstants; @@ -64,23 +65,40 @@ import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; +import com.android.systemui.util.concurrency.DelayableExecutor; -import java.io.IOException; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** - * Base media control panel for System UI + * A view controller used for Media Playback. */ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @Nullable private final LocalMediaManager mLocalMediaManager; + + // Button IDs for QS controls + static final int[] ACTION_IDS = { + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4 + }; + + private final SeekBarViewModel mSeekBarViewModel; + private final SeekBarObserver mSeekBarObserver; private final Executor mForegroundExecutor; protected final Executor mBackgroundExecutor; private final ActivityStarter mActivityStarter; + private final LayoutAnimationHelper mLayoutAnimationHelper; private Context mContext; - protected LinearLayout mMediaNotifView; + private MotionLayout mMediaNotifView; + private final View mBackground; private View mSeamless; private MediaSession.Token mToken; private MediaController mController; @@ -89,9 +107,11 @@ public class MediaControlPanel { private MediaDevice mDevice; protected ComponentName mServiceComponent; private boolean mIsRegistered = false; + private final List<KeyFrames> mKeyFrames; private String mKey; - - private final int[] mActionIds; + 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"; @@ -100,22 +120,6 @@ public class MediaControlPanel { private boolean mIsRemotePlayback; private QSMediaBrowser mQSMediaBrowser; - // Button IDs used in notifications - protected static final int[] NOTIF_ACTION_IDS = { - com.android.internal.R.id.action0, - com.android.internal.R.id.action1, - com.android.internal.R.id.action2, - com.android.internal.R.id.action3, - com.android.internal.R.id.action4 - }; - - // URI fields to try loading album art from - private static final String[] ART_URIS = { - MediaMetadata.METADATA_KEY_ALBUM_ART_URI, - MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI - }; - private final MediaController.Callback mSessionCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { @@ -135,17 +139,6 @@ public class MediaControlPanel { } }; - private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View unused) { - makeActive(); - } - @Override - public void onViewDetachedFromWindow(View unused) { - makeInactive(); - } - }; - private final LocalMediaManager.DeviceCallback mDeviceCallback = new LocalMediaManager.DeviceCallback() { @Override @@ -175,41 +168,65 @@ public class MediaControlPanel { * @param context * @param parent * @param routeManager Manager used to listen for device change events. - * @param layoutId layout resource to use for this control panel - * @param actionIds resource IDs for action buttons in the layout * @param foregroundExecutor foreground executor * @param backgroundExecutor background executor, used for processing artwork * @param activityStarter activity starter */ public MediaControlPanel(Context context, ViewGroup parent, - @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds, - Executor foregroundExecutor, Executor backgroundExecutor, - ActivityStarter activityStarter) { + @Nullable LocalMediaManager routeManager, Executor foregroundExecutor, + DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) { mContext = context; LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false); - // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views - // mStateListener shouldn't need to be unregistered since this object shares the same - // lifecycle with the inflated view. It would be better, however, if this controller used an - // attach/detach of views instead of inflating them in the constructor, which would allow - // mStateListener to be unregistered in detach. - mMediaNotifView.addOnAttachStateChangeListener(mStateListener); + mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false); + mBackground = mMediaNotifView.findViewById(R.id.media_background); + mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView); + GoneChildrenHideHelper.clipGoneChildrenOnLayout(mMediaNotifView); + mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList(); mLocalMediaManager = routeManager; - mActionIds = actionIds; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; + mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); + mSeekBarObserver = new SeekBarObserver(getView()); + mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); + SeekBar bar = getView().findViewById(R.id.media_progress_bar); + bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); + bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); + loadDimens(); + } + + public void onDestroy() { + mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); + makeInactive(); + } + + private void loadDimens() { + mAlbumArtRadius = mContext.getResources().getDimensionPixelSize( + Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius)); + mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size); } /** * Get the view used to display media controls * @return the view */ - public View getView() { + public MotionLayout getView() { return mMediaNotifView; } /** + * Sets the listening state of the player. + * + * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid + * unnecessary work when the QS panel is closed. + * + * @param listening True when player should be active. Otherwise, false. + */ + public void setListening(boolean listening) { + mSeekBarViewModel.setListening(listening); + } + + /** * Get the context * @return context */ @@ -218,20 +235,12 @@ public class MediaControlPanel { } /** - * Update the media panel view for the given media session - * @param token - * @param iconDrawable - * @param largeIcon - * @param iconColor - * @param bgColor - * @param contentIntent - * @param appNameString - * @param key + * Bind this view based on the data given */ - public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon, - int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, - String key) { - // Ensure that component names are updated if token has changed + public void bind(@NotNull MediaData data) { + MediaSession.Token token = data.getToken(); + mForegroundColor = data.getForegroundColor(); + mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { if (mQSMediaBrowser != null) { Log.d(TAG, "Disconnecting old media browser"); @@ -243,20 +252,21 @@ public class MediaControlPanel { mCheckedForResumption = false; } - mForegroundColor = iconColor; - mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); - mKey = key; + + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); // 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 = mController.getPackageName(); + 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())) { @@ -271,25 +281,51 @@ public class MediaControlPanel { mController.registerCallback(mSessionCallback); - mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); + mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList( + ColorStateList.valueOf(mBackgroundColor)); // Click action - if (contentIntent != null) { + PendingIntent clickIntent = data.getClickIntent(); + if (clickIntent != null) { mMediaNotifView.setOnClickListener(v -> { - mActivityStarter.postStartActivityDismissingKeyguard(contentIntent); + mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); }); } + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + // 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); + // App icon - ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); + ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon); + Drawable iconDrawable = data.getAppIcon().mutate(); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); + // Song name + TextView titleText = mMediaNotifView.requireViewById(R.id.header_title); + titleText.setText(data.getSong()); + titleText.setTextColor(data.getForegroundColor()); + + // App title + TextView appName = mMediaNotifView.requireViewById(R.id.app_name); + appName.setText(data.getApp()); + appName.setTextColor(mForegroundColor); + + // Artist name + TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist); + artistText.setText(data.getArtist()); + artistText.setTextColor(mForegroundColor); + // Transfer chip mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); if (mSeamless != null) { if (mLocalMediaManager != null) { mSeamless.setVisibility(View.VISIBLE); + setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); + setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); updateDevice(mLocalMediaManager.getCurrentConnectedDevice()); mSeamless.setOnClickListener(v -> { final Intent intent = new Intent() @@ -311,43 +347,110 @@ public class MediaControlPanel { Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback."); mIsRemotePlayback = false; } + List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); + // Media controls + int i = 0; + List<MediaAction> actionIcons = data.getActions(); + for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) { + int actionId = ACTION_IDS[i]; + final ImageButton button = mMediaNotifView.findViewById(actionId); + MediaAction mediaAction = actionIcons.get(i); + button.setImageDrawable(mediaAction.getDrawable()); + button.setContentDescription(mediaAction.getContentDescription()); + button.setImageTintList(ColorStateList.valueOf(mForegroundColor)); + PendingIntent actionIntent = mediaAction.getIntent(); + + if (mBackground.getBackground() instanceof IlluminationDrawable) { + ((IlluminationDrawable) mBackground.getBackground()) + .setupTouch(button, mMediaNotifView); + } - makeActive(); + button.setOnClickListener(v -> { + if (actionIntent != null) { + try { + actionIntent.send(); + } catch (PendingIntent.CanceledException e) { + e.printStackTrace(); + } + } + }); + boolean visibleInCompat = actionsWhenCollapsed.contains(i); + updateKeyFrameVisibility(actionId, visibleInCompat); + setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); + setVisibleAndAlpha(expandedSet, actionId, true /*visible */); + } - // App title (not in mini player) - TextView appName = mMediaNotifView.findViewById(R.id.app_name); - if (appName != null) { - appName.setText(appNameString); - appName.setTextColor(mForegroundColor); + // Hide any unused buttons + for (; i < ACTION_IDS.length; i++) { + setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); } - // Can be null! - MediaMetadata mediaMetadata = mController.getMetadata(); + // Seek Bar + final MediaController controller = getController(); + mBackgroundExecutor.execute( + () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor())); - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView)); - } + // Set up long press menu + // TODO: b/156036025 bring back media guts - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - String songName = ""; - if (mediaMetadata != null) { - songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + makeActive(); + + // Update both constraint sets to regenerate the animation. + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); + } + + @UiThread + private Drawable createRoundedBitmap(Icon icon) { + if (icon == null) { + return null; } - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); - - // Artist name (not in mini player) - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - String artistName = ""; - if (mediaMetadata != null) { - artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + // Let's scale down the View, such that the content always nicely fills the view. + // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect + // ratios + Drawable drawable = icon.loadDrawable(mContext); + float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); + Rect bounds; + if (aspectRatio > 1.0f) { + bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio)); + } else { + bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize); + } + if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { + float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; + float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; + bounds.offset((int) -offsetX,(int) -offsetY); + } + drawable.setBounds(bounds); + Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(scaled); + drawable.draw(canvas); + RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create( + mContext.getResources(), scaled); + artwork.setCornerRadius(mAlbumArtRadius); + return artwork; + } + + /** + * Updates the keyframe visibility such that only views that are not visible actually go + * through a transition and fade in. + * + * @param actionId the id to change + * @param visible is the view visible + */ + private void updateKeyFrameVisibility(int actionId, boolean visible) { + for (int i = 0; i < mKeyFrames.size(); i++) { + KeyFrames keyframe = mKeyFrames.get(i); + ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId); + for (int j = 0; j < viewKeyFrames.size(); j++) { + Key key = viewKeyFrames.get(j); + if (key instanceof KeyAttributes) { + KeyAttributes attributes = (KeyAttributes) key; + attributes.setValue("alpha", visible ? 1.0f : 0.0f); + } } - artistText.setText(artistName); - artistText.setTextColor(mForegroundColor); } } @@ -421,120 +524,6 @@ public class MediaControlPanel { } /** - * Process album art for layout - * @param description media description - * @param albumView view to hold the album art - */ - protected void processAlbumArt(MediaDescription description, ImageView albumView) { - Bitmap albumArt = null; - - // First try loading from URI - albumArt = loadBitmapFromUri(description.getIconUri()); - - // Then check bitmap - if (albumArt == null) { - albumArt = description.getIconBitmap(); - } - - processAlbumArtInternal(albumArt, albumView); - } - - /** - * Process album art for layout - * @param metadata media metadata - * @param largeIcon from notification, checked as a fallback if metadata does not have art - * @param albumView view to hold the album art - */ - private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) { - Bitmap albumArt = null; - - if (metadata != null) { - // First look in URI fields - for (String field : ART_URIS) { - String uriString = metadata.getString(field); - if (!TextUtils.isEmpty(uriString)) { - albumArt = loadBitmapFromUri(Uri.parse(uriString)); - if (albumArt != null) { - Log.d(TAG, "loaded art from " + field); - break; - } - } - } - - // Then check bitmap field - if (albumArt == null) { - albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - } - - // Finally try the notification's largeIcon - if (albumArt == null && largeIcon != null) { - albumArt = largeIcon.getBitmap(); - } - - processAlbumArtInternal(albumArt, albumView); - } - - /** - * Load a bitmap from a URI - * @param uri - * @return bitmap, or null if couldn't be loaded - */ - private Bitmap loadBitmapFromUri(Uri uri) { - // ImageDecoder requires a scheme of the following types - if (uri.getScheme() == null) { - return null; - } - - if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) - && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE) - && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { - return null; - } - - ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri); - try { - return ImageDecoder.decodeBitmap(source); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - /** - * Resize and crop the image if provided and update the control view - * @param albumArt Bitmap of art to display, or null to hide view - * @param albumView View that will hold the art - */ - private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) { - // Resize - RoundedBitmapDrawable roundedDrawable = null; - if (albumArt != null) { - float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); - int albumSize = (int) mContext.getResources().getDimension( - R.dimen.qs_media_album_size); - Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize); - roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); - roundedDrawable.setCornerRadius(radius); - } else { - Log.e(TAG, "No album art available"); - } - - // Now that it's resized, update the UI - final RoundedBitmapDrawable result = roundedDrawable; - mForegroundExecutor.execute(() -> { - if (result != null) { - albumView.setImageDrawable(result); - albumView.setVisibility(View.VISIBLE); - } else { - albumView.setImageDrawable(null); - albumView.setVisibility(View.GONE); - } - }); - } - - /** * Update the current device information * @param device device information to display */ @@ -613,15 +602,16 @@ public class MediaControlPanel { */ protected void resetButtons() { // Hide all the old buttons - for (int i = 0; i < mActionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); - if (thisBtn != null) { - thisBtn.setVisibility(View.GONE); - } + + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + 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 = mMediaNotifView.findViewById(mActionIds[0]); + ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); if (mQSMediaBrowser != null) { @@ -643,7 +633,25 @@ public class MediaControlPanel { }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - btn.setVisibility(View.VISIBLE); + 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 = mMediaNotifView.findViewById(R.id.qs_media_controls_options); + + mMediaNotifView.setOnLongClickListener(v -> { + // Replace player view with close/cancel view +// guts.setVisibility(View.GONE); + options.setVisibility(View.VISIBLE); + return true; // consumed click + }); + } + + 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); } private void makeActive() { @@ -667,7 +675,6 @@ public class MediaControlPanel { mIsRegistered = false; } } - /** * Verify that we can connect to the given component with a MediaBrowser, and if so, add that * component to the list of resumption components @@ -739,4 +746,25 @@ public class MediaControlPanel { * Called when a player can't be resumed to give it an opportunity to hide or remove itself */ protected void removePlayer() { } + + public void measure(@Nullable MediaMeasurementInput input) { + if (input != null) { + int width = input.getWidth(); + setPlayerWidth(width); + mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); + } + } + + public void setPlayerWidth(int width) { + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + collapsedSet.setGuidelineBegin(R.id.view_width, width); + expandedSet.setGuidelineBegin(R.id.view_width, width); + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); + } + + public void animatePendingSizeChange(long duration, long startDelay) { + mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt new file mode 100644 index 000000000000..6a2646170e85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -0,0 +1,46 @@ +/* + * 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.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.media.session.MediaSession + +/** State of a media view. */ +data class MediaData( + val initialized: Boolean = false, + val foregroundColor: Int, + val backgroundColor: Int, + val app: String?, + val appIcon: Drawable?, + val artist: CharSequence?, + val song: CharSequence?, + val artwork: Icon?, + val actions: List<MediaAction>, + val actionsToShowInCompact: List<Int>, + val packageName: String?, + val token: MediaSession.Token?, + val clickIntent: PendingIntent? +) + +/** State of a media action. */ +data class MediaAction( + val drawable: Drawable?, + val intent: PendingIntent?, + val contentDescription: CharSequence? +) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt new file mode 100644 index 000000000000..e7d0f7ec1a37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -0,0 +1,306 @@ +/* + * 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.Notification +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ImageDecoder +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.media.MediaMetadata +import android.media.session.MediaSession +import android.net.Uri +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.text.TextUtils +import android.util.Log +import com.android.internal.util.ContrastColorUtil +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.notification.MediaNotificationProcessor +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import java.io.IOException +import java.util.* +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.LinkedHashMap + +// URI fields to try loading album art from +private val ART_URIS = arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI +) + +private const val TAG = "MediaDataManager" + +private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, + emptyList(), emptyList(), null, null, null) + +/** + * A class that facilitates management and loading of Media Data, ready for binding. + */ +@Singleton +class MediaDataManager @Inject constructor( + private val context: Context, + private val mediaControllerFactory: MediaControllerFactory, + @Background private val backgroundExecutor: Executor, + @Main private val foregroundExcecutor: Executor +) { + + private val listeners: MutableSet<Listener> = mutableSetOf() + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + + fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (isMediaNotification(sbn)) { + if (!mediaEntries.containsKey(key)) { + mediaEntries.put(key, LOADING) + } + loadMediaData(key, sbn) + } else { + onNotificationRemoved(key) + } + } + + private fun loadMediaData(key: String, sbn: StatusBarNotification) { + backgroundExecutor.execute { + loadMediaDataInBg(key, sbn) + } + } + + /** + * Add a listener for changes in this class + */ + fun addListener(listener: Listener) = listeners.add(listener) + + /** + * Remove a listener for changes in this class + */ + fun removeListener(listener: Listener) = listeners.remove(listener) + + private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) { + val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) + as MediaSession.Token? + val metadata = mediaControllerFactory.create(token).metadata + + if (metadata == null) { + // TODO: handle this better, removing media notification + return + } + + // Foreground and Background colors computed from album art + val notif: Notification = sbn.notification + var fgColor = notif.color + var bgColor = -1 + var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART) + if (artworkBitmap == null) { + artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + if (artworkBitmap == null) { + artworkBitmap = loadBitmapFromUri(metadata) + } + val artWorkIcon = if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + if (artWorkIcon != null) { + // If we have art, get colors from that + if (artworkBitmap == null) { + if (artWorkIcon.type == Icon.TYPE_BITMAP + || artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) { + artworkBitmap = artWorkIcon.bitmap + } else { + val drawable: Drawable = artWorkIcon.loadDrawable(context) + artworkBitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888) + val canvas = Canvas(artworkBitmap) + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + } + } + val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) + .generate() + val swatch = MediaNotificationProcessor.findBackgroundSwatch(p) + bgColor = swatch.rgb + fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p) + } + // Make sure colors will be legible + val isDark = !ContrastColorUtil.isColorLight(bgColor) + fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor, + isDark) + fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark) + + // App name + val builder = Notification.Builder.recoverBuilder(context, notif) + val app = builder.loadHeaderAppName() + + // App Icon + val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context) + + // Song name + var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song == null) { + song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song == null) { + song = HybridGroupManager.resolveTitle(notif) + } + + // Artist name + var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist == null) { + artist = HybridGroupManager.resolveText(notif) + } + + // Control buttons + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + val actionsToShowCollapsed = notif.extras.getIntArray( + Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList() + // TODO: b/153736623 look into creating actions when this isn't a media style notification + + val packageContext: Context = sbn.getPackageContext(context) + for (action in actions) { + val mediaAction = MediaAction( + action.getIcon().loadDrawable(packageContext), + action.actionIntent, + action.title) + actionIcons.add(mediaAction) + } + + foregroundExcecutor.execute { + onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist, + song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, + notif.contentIntent)) + } + + } + + /** + * Load a bitmap from the various Art metadata URIs + */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + Log.d(TAG, "loaded art from $uri") + break + } + } + } + return null + } + + /** + * Load a bitmap from a URI + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.getScheme() == null) { + return null; + } + + if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) + && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE) + && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { + return null; + } + + val source = ImageDecoder.createSource(context.getContentResolver(), uri) + return try { + ImageDecoder.decodeBitmap(source) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + fun onMediaDataLoaded(key: String, data: MediaData) { + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + listeners.forEach { + it.onMediaDataLoaded(key, data) + } + } + } + + fun onNotificationRemoved(key: String) { + val removed = mediaEntries.remove(key) + if (removed != null) { + listeners.forEach { + it.onMediaDataRemoved(key) + } + } + } + + private fun isMediaNotification(sbn: StatusBarNotification) : Boolean { + if (!useUniversalMediaPlayer()) { + return false + } + if (!sbn.notification.hasMediaSession()) { + return false + } + val notificationStyle = sbn.notification.notificationStyle + if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) + || Notification.MediaStyle::class.java.equals(notificationStyle)) { + return true + } + return false + } + + /** + * are we using the universal media player + */ + private fun useUniversalMediaPlayer() + = Settings.System.getInt(context.contentResolver, "qs_media_player", 1) > 0 + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaEntries.size > 0 + + fun hasAnyMedia(): Boolean { + // TODO: implement this when we implemented resumption + return hasActiveMedia() + } + + interface Listener { + + /** + * Called whenever there's new MediaData Loaded for the consumption in views + */ + fun onMediaDataLoaded(key: String, data: MediaData) {} + + /** + * Called whenever a previously existing Media notification was removed + */ + fun onMediaDataRemoved(key: String) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt new file mode 100644 index 000000000000..6b1c520db7b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -0,0 +1,425 @@ +/* + * 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.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.annotation.IntDef +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroupOverlay +import com.android.systemui.Interpolators +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.animation.UniqueObjectHostView +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This manager is responsible for placement of the unique media view between the different hosts + * and animate the positions of the views to achieve seamless transitions. + */ +@Singleton +class MediaHierarchyManager @Inject constructor( + private val context: Context, + private val statusBarStateController: SysuiStatusBarStateController, + private val keyguardStateController: KeyguardStateController, + private val bypassController: KeyguardBypassController, + private val mediaViewManager: MediaViewManager, + private val mediaMeasurementProvider: MediaMeasurementManager +) { + /** + * The root overlay of the hierarchy. This is where the media notification is attached to + * whenever the view is transitioning from one host to another. It also make sure that the + * view is always in its final state when it is attached to a view host. + */ + private var rootOverlay: ViewGroupOverlay? = null + private lateinit var currentState: MediaState + private val mediaCarousel + get() = mediaViewManager.mediaCarousel + private var animationStartState: MediaState? = null + private var statusbarState: Int = statusBarStateController.state + private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { + updateTargetState() + applyState(animationStartState!!.interpolate(targetState!!, animatedFraction)) + } + addListener(object : AnimatorListenerAdapter() { + private var cancelled: Boolean = false + + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + } + override fun onAnimationEnd(animation: Animator?) { + if (!cancelled) { + applyTargetStateIfNotAnimating() + } + } + + override fun onAnimationStart(animation: Animator?) { + cancelled = false + } + }) + } + private var targetState: MediaState? = null + private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1) + + /** + * The last location where this view was at before going to the desired location. This is + * useful for guided transitions. + */ + @MediaLocation private var previousLocation = -1 + + /** + * The desired location where the view will be at the end of the transition. + */ + @MediaLocation private var desiredLocation = -1 + + /** + * The current attachment location where the view is currently attached. + * Usually this matches the desired location except for animations whenever a view moves + * to the new desired location, during which it is in [IN_OVERLAY]. + */ + @MediaLocation private var currentAttachmentLocation = -1 + + var qsExpansion: Float = 0.0f + set(value) { + if (field != value) { + field = value + updateDesiredLocation() + if (getQSTransformationProgress() >= 0) { + updateTargetState() + applyTargetStateIfNotAnimating() + } + } + } + + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStatePreChange(oldState: Int, newState: Int) { + // We're updating the location before the state change happens, since we want the + // location of the previous state to still be up to date when the animation starts + statusbarState = newState + updateDesiredLocation() + } + + override fun onStateChanged(newState: Int) { + updateTargetState() + } + }) + } + + /** + * Register a media host and create a view can be attached to a view hierarchy + * and where the players will be placed in when the host is the currently desired state. + * + * @return the hostView associated with this location + */ + fun register(mediaObject: MediaHost) : ViewGroup { + val viewHost = createUniqueObjectHost(mediaObject) + mediaObject.hostView = viewHost; + mediaHosts[mediaObject.location] = mediaObject + if (mediaObject.location == desiredLocation) { + // In case we are overriding a view that is already visible, make sure we attach it + // to this new host view in the below call + desiredLocation = -1 + } + if (mediaObject.location == currentAttachmentLocation) { + currentAttachmentLocation = -1 + } + updateDesiredLocation() + return viewHost + } + + private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView { + val viewHost = UniqueObjectHostView(context) + viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host) + viewHost.onMeasureListener = { input -> + if (host.location == desiredLocation) { + // Measurement of the currently active player is happening, Let's make + // sure the player width is up to date + val measuringInput = host.getMeasuringInput(input) + mediaViewManager.setPlayerWidth(measuringInput.width) + } + } + + viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(p0: View?) { + if (rootOverlay == null) { + rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay) + } + viewHost.removeOnAttachStateChangeListener(this) + } + + override fun onViewDetachedFromWindow(p0: View?) { + } + }) + return viewHost + } + + /** + * Updates the location that the view should be in. If it changes, an animation may be triggered + * going from the old desired location to the new one. + */ + private fun updateDesiredLocation() { + val desiredLocation = calculateLocation() + if (desiredLocation != this.desiredLocation) { + if (this.desiredLocation >= 0) { + previousLocation = this.desiredLocation + } + val isNewView = this.desiredLocation == -1 + this.desiredLocation = desiredLocation + // Let's perform a transition + val animate = shouldAnimateTransition(desiredLocation, previousLocation) + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState, + animate, animDuration, delay) + performTransitionToNewLocation(isNewView, animate) + } + } + + private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) { + if (previousLocation < 0 || isNewView) { + cancelAnimationAndApplyDesiredState() + return + } + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost == null || previousHost == null) { + cancelAnimationAndApplyDesiredState() + return + } + updateTargetState() + if (isCurrentlyInGuidedTransformation()) { + applyTargetStateIfNotAnimating() + } else if (animate) { + animator.cancel() + if (currentAttachmentLocation == IN_OVERLAY + || !previousHost.hostView.isAttachedToWindow) { + // Let's animate to the new position, starting from the current position + // We also go in here in case the view was detached, since the bounds wouldn't + // be correct anymore + animationStartState = currentState.copy() + } else { + // otherwise, let's take the freshest state, since the current one could + // be outdated + animationStartState = previousHost.currentState.copy() + } + adjustAnimatorForTransition(desiredLocation, previousLocation) + animator.start() + } else { + cancelAnimationAndApplyDesiredState() + } + } + + private fun shouldAnimateTransition( + @MediaLocation currentLocation: Int, + @MediaLocation previousLocation: Int + ): Boolean { + if (currentLocation == LOCATION_QQS + && previousLocation == LOCATION_LOCKSCREEN + && (statusBarStateController.leaveOpenOnKeyguardHide() + || statusbarState == StatusBarState.SHADE_LOCKED)) { + // Usually listening to the isShown is enough to determine this, but there is some + // non-trivial reattaching logic happening that will make the view not-shown earlier + return true + } + return mediaCarousel.isShown || animator.isRunning + } + + private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + animator.apply { + duration = animDuration + startDelay = delay + } + + } + + private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> { + var animDuration = 200L + var delay = 0L + if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { + // Going to the full shade, let's adjust the animation duration + if (statusbarState == StatusBarState.SHADE + && keyguardStateController.isKeyguardFadingAway) { + delay = keyguardStateController.keyguardFadingAwayDelay + } + animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong() + } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) { + animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong() + } + return animDuration to delay + } + + private fun applyTargetStateIfNotAnimating() { + if (!animator.isRunning) { + // Let's immediately apply the target state (which is interpolated) if there is + // no animation running. Otherwise the animation update will already update + // the location + applyState(targetState!!) + } + } + + /** + * Updates the state that the view wants to be in at the end of the animation. + */ + private fun updateTargetState() { + if (isCurrentlyInGuidedTransformation()) { + val progress = getTransformationProgress() + val currentHost = getHost(desiredLocation)!! + val previousHost = getHost(previousLocation)!! + val newState = currentHost.currentState + val previousState = previousHost.currentState + targetState = previousState.interpolate(newState, progress) + } else { + targetState = getHost(desiredLocation)?.currentState + } + } + + /** + * @return true if this transformation is guided by an external progress like a finger + */ + private fun isCurrentlyInGuidedTransformation() : Boolean { + return getTransformationProgress() >= 0 + } + + /** + * @return the current transformation progress if we're in a guided transformation and -1 + * otherwise + */ + private fun getTransformationProgress(): Float { + val progress = getQSTransformationProgress() + if (progress >= 0) { + return progress + } + return -1.0f + } + + private fun getQSTransformationProgress(): Float { + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost?.location == LOCATION_QS) { + if (previousHost?.location == LOCATION_QQS) { + return qsExpansion + } + } + return -1.0f + } + + private fun getHost(@MediaLocation location: Int): MediaHost? { + if (location < 0) { + return null + } + return mediaHosts[location] + } + + private fun cancelAnimationAndApplyDesiredState() { + animator.cancel() + getHost(desiredLocation)?.let { + applyState(it.currentState) + } + } + + private fun applyState(state: MediaState) { + currentState = state.copy() + mediaViewManager.setCurrentState(currentState) + updateHostAttachment() + if (currentAttachmentLocation == IN_OVERLAY) { + val boundsOnScreen = state.boundsOnScreen + mediaCarousel.setLeftTopRightBottom( + boundsOnScreen.left, + boundsOnScreen.top, + boundsOnScreen.right, + boundsOnScreen.bottom) + } + } + + private fun updateHostAttachment() { + val inOverlay = isTransitionRunning() && rootOverlay != null + val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation + if (currentAttachmentLocation != newLocation) { + currentAttachmentLocation = newLocation + + // Remove the carousel from the old host + (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel) + + // Add it to the new one + val targetHost = getHost(desiredLocation)!!.hostView + if (inOverlay) { + rootOverlay!!.add(mediaCarousel) + } else { + targetHost.addView(mediaCarousel) + mediaViewManager.onViewReattached() + } + } + } + + private fun isTransitionRunning(): Boolean { + return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f + || animator.isRunning + } + + @MediaLocation + private fun calculateLocation() : Int { + val onLockscreen = (!bypassController.bypassEnabled + && (statusbarState == StatusBarState.KEYGUARD + || statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) + return when { + qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS + qsExpansion > 0.4f && onLockscreen -> LOCATION_QS + onLockscreen -> LOCATION_LOCKSCREEN + else -> LOCATION_QQS + } + } + + /** + * The expansion of quick settings + */ + @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN]) + @Retention(AnnotationRetention.SOURCE) + annotation class MediaLocation + + companion object { + /** + * Attached in expanded quick settings + */ + const val LOCATION_QS = 0 + + /** + * Attached in the collapsed QS + */ + const val LOCATION_QQS = 1 + + /** + * Attached on the lock screen + */ + const val LOCATION_LOCKSCREEN = 2 + + /** + * Attached at the root of the hierarchy in an overlay + */ + const val IN_OVERLAY = -1000 + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt new file mode 100644 index 000000000000..6e7b6bcb7502 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -0,0 +1,159 @@ +package com.android.systemui.media + +import android.graphics.Rect +import android.util.MathUtils +import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.ViewGroup +import com.android.systemui.media.MediaHierarchyManager.MediaLocation +import com.android.systemui.util.animation.MeasurementInput +import javax.inject.Inject + +class MediaHost @Inject constructor( + private val state: MediaHostState, + private val mediaHierarchyManager: MediaHierarchyManager, + private val mediaDataManager: MediaDataManager +) : MediaState by state { + lateinit var hostView: ViewGroup + var location: Int = -1 + private set + var visibleChangedListener: ((Boolean) -> Unit)? = null + var visible: Boolean = false + private set + + private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0) + + /** + * Get the current Media state. This also updates the location on screen + */ + val currentState : MediaState + get () { + hostView.getLocationOnScreen(tmpLocationOnScreen) + var left = tmpLocationOnScreen[0] + hostView.paddingLeft + var top = tmpLocationOnScreen[1] + hostView.paddingTop + var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight + var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom + // Handle cases when the width or height is 0 but it has padding. In those cases + // the above could return negative widths, which is wrong + if (right < left) { + left = 0 + right = 0; + } + if (bottom < top) { + bottom = 0 + top = 0; + } + state.boundsOnScreen.set(left, top, right, bottom) + return state + } + + private val listener = object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateViewVisibility() + } + + override fun onMediaDataRemoved(key: String) { + updateViewVisibility() + } + } + + /** + * Initialize this MediaObject and create a host view. + * + * @param location the location this host name has. Used to identify the host during + * transitions. + */ + fun init(@MediaLocation location: Int) { + this.location = location; + hostView = mediaHierarchyManager.register(this) + hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + mediaDataManager.addListener(listener) + updateViewVisibility() + } + + override fun onViewDetachedFromWindow(v: View?) { + mediaDataManager.removeListener(listener) + } + }) + updateViewVisibility() + } + + private fun updateViewVisibility() { + if (showsOnlyActiveMedia) { + visible = mediaDataManager.hasActiveMedia() + } else { + visible = mediaDataManager.hasAnyMedia() + } + hostView.visibility = if (visible) View.VISIBLE else View.GONE + visibleChangedListener?.invoke(visible) + } + + class MediaHostState @Inject constructor() : MediaState { + var measurementInput: MediaMeasurementInput? = null + override var expansion: Float = 0.0f + override var showsOnlyActiveMedia: Boolean = false + override val boundsOnScreen: Rect = Rect() + + override fun copy() : MediaState { + val mediaHostState = MediaHostState() + mediaHostState.expansion = expansion + mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia + mediaHostState.boundsOnScreen.set(boundsOnScreen) + mediaHostState.measurementInput = measurementInput + return mediaHostState + } + + override fun interpolate(other: MediaState, amount: Float) : MediaState { + val result = MediaHostState() + result.expansion = MathUtils.lerp(expansion, other.expansion, amount) + val left = MathUtils.lerp(boundsOnScreen.left.toFloat(), + other.boundsOnScreen.left.toFloat(), amount).toInt() + val top = MathUtils.lerp(boundsOnScreen.top.toFloat(), + other.boundsOnScreen.top.toFloat(), amount).toInt() + val right = MathUtils.lerp(boundsOnScreen.right.toFloat(), + other.boundsOnScreen.right.toFloat(), amount).toInt() + val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(), + other.boundsOnScreen.bottom.toFloat(), amount).toInt() + result.boundsOnScreen.set(left, top, right, bottom) + result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia + if (amount > 0.0f) { + if (other is MediaHostState) { + result.measurementInput = other.measurementInput + } + } else { + result.measurementInput + } + return result + } + + override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput { + measurementInput = MediaMeasurementInput(input, expansion) + return measurementInput as MediaMeasurementInput + } + } +} + +interface MediaState { + var expansion: Float + var showsOnlyActiveMedia: Boolean + val boundsOnScreen: Rect + fun copy() : MediaState + fun interpolate(other: MediaState, amount: Float) : MediaState + fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput +} +/** + * The measurement input for a Media View + */ +data class MediaMeasurementInput( + private val viewInput: MeasurementInput, + val expansion: Float) : MeasurementInput by viewInput { + + override fun sameAs(input: MeasurementInput?): Boolean { + if (!(input is MediaMeasurementInput)) { + return false + } + return width == input.width && expansion == input.expansion + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt new file mode 100644 index 000000000000..4bbf5eb9f0dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt @@ -0,0 +1,59 @@ +/* + * 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 com.android.systemui.util.animation.BaseMeasurementCache +import com.android.systemui.util.animation.GuaranteedMeasurementCache +import com.android.systemui.util.animation.MeasurementCache +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.MeasurementOutput +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class responsible creating measurement caches for media hosts which also coordinates with + * the view manager to obtain sizes for unknown measurement inputs. + */ +@Singleton +class MediaMeasurementManager @Inject constructor( + private val mediaViewManager: MediaViewManager +) { + private val baseCache: MeasurementCache + + init { + baseCache = BaseMeasurementCache() + } + + private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + return mediaViewManager.obtainMeasurement(input) + } + + /** + * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that + * requesting any size from the cache will always return the correct value. + */ + fun obtainCache(host: MediaState): GuaranteedMeasurementCache { + val remapper = { input: MeasurementInput -> + host.getMeasuringInput(input) + } + val provider = { input: MeasurementInput -> + provideMeasurement(input as MediaMeasurementInput) + } + return GuaranteedMeasurementCache(baseCache, remapper, provider) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt new file mode 100644 index 000000000000..49d2d8860a2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -0,0 +1,302 @@ +package com.android.systemui.media + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.HorizontalScrollView +import android.widget.LinearLayout +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.media.InfoMediaManager +import com.android.settingslib.media.LocalMediaManager +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.statusbar.notification.VisualStabilityManager +import com.android.systemui.util.animation.MeasurementOutput +import com.android.systemui.util.animation.UniqueObjectHostView +import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Class that is responsible for keeping the view carousel up to date. + * This also handles changes in state and applies them to the media carousel like the expansion. + */ +@Singleton +class MediaViewManager @Inject constructor( + private val context: Context, + @Main private val foregroundExecutor: Executor, + @Background private val backgroundExecutor: DelayableExecutor, + private val localBluetoothManager: LocalBluetoothManager?, + private val visualStabilityManager: VisualStabilityManager, + private val activityStarter: ActivityStarter, + mediaManager: MediaDataManager +) { + private var playerWidth: Int = 0 + private var playerWidthPlusPadding: Int = 0 + private var desiredState: MediaHost.MediaHostState? = null + private var currentState: MediaState? = null + val mediaCarousel: HorizontalScrollView + private val mediaContent: ViewGroup + private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() + private val visualStabilityCallback = ::reorderAllPlayers + private var activeMediaIndex: Int = 0 + private var scrollIntoCurrentMedia: Int = 0 + + private var currentlyExpanded = true + set(value) { + if (field != value) { + field = value + for (player in mediaPlayers.values) { + player.setListening(field) + } + } + } + private val scrollChangedListener = object : View.OnScrollChangeListener { + override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int, + oldScrollY: Int) { + if (playerWidthPlusPadding == 0) { + return + } + onMediaScrollingChanged(scrollX / playerWidthPlusPadding, + scrollX % playerWidthPlusPadding) + } + } + + init { + mediaCarousel = inflateMediaCarousel() + mediaCarousel.setOnScrollChangeListener(scrollChangedListener) + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + mediaManager.addListener(object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateView(key, data) + updatePlayerVisibilities() + } + + override fun onMediaDataRemoved(key: String) { + val removed = mediaPlayers.remove(key) + removed?.apply { + val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex + mediaContent.removeView(removed.view) + removed.onDestroy() + updateMediaPaddings() + if (beforeActive) { + // also update the index here since the scroll below might not always lead + // to a scrolling changed + activeMediaIndex = Math.max(0, activeMediaIndex - 1) + mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX + - playerWidthPlusPadding, 0) + } + updatePlayerVisibilities() + } + } + }) + } + + private fun inflateMediaCarousel(): HorizontalScrollView { + return LayoutInflater.from(context).inflate(R.layout.media_carousel, + UniqueObjectHostView(context), false) as HorizontalScrollView + } + + private fun reorderAllPlayers() { + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { + mediaContent.removeView(view) + mediaContent.addView(view, 0) + } + } + updateMediaPaddings() + updatePlayerVisibilities() + } + + private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { + val wasScrolledIn = scrollIntoCurrentMedia != 0 + scrollIntoCurrentMedia = scrollInAmount + val nowScrolledIn = scrollIntoCurrentMedia != 0 + if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { + activeMediaIndex = newIndex + updatePlayerVisibilities() + } + } + + private fun updatePlayerVisibilities() { + val scrolledIn = scrollIntoCurrentMedia != 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) + view.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + } + + private fun updateView(key: String, data: MediaData) { + var existingPlayer = mediaPlayers[key] + if (existingPlayer == null) { + // Set up listener for device changes + // TODO: integrate with MediaTransferManager? + val imm = InfoMediaManager(context, data.packageName, + null /* notification */, localBluetoothManager) + val routeManager = LocalMediaManager(context, localBluetoothManager, + imm, data.packageName) + + existingPlayer = MediaControlPanel(context, mediaContent, routeManager, + foregroundExecutor, backgroundExecutor, activityStarter) + mediaPlayers[key] = existingPlayer + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + existingPlayer.view.setLayoutParams(lp) + existingPlayer.setListening(currentlyExpanded) + if (existingPlayer.isPlaying) { + mediaContent.addView(existingPlayer.view, 0) + } else { + mediaContent.addView(existingPlayer.view) + } + updatePlayerToCurrentState(existingPlayer) + } else if (existingPlayer.isPlaying && + mediaContent.indexOfChild(existingPlayer.view) != 0) { + if (visualStabilityManager.isReorderingAllowed) { + mediaContent.removeView(existingPlayer.view) + mediaContent.addView(existingPlayer.view, 0) + } else { + visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback) + } + } + existingPlayer.bind(data) + // Resetting the progress to make sure it's taken into account for the latest + // motion model + existingPlayer.view.progress = currentState?.expansion ?: 0.0f + updateMediaPaddings() + } + + private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) { + if (desiredState != null && desiredState!!.measurementInput != null) { + // make sure the player width is set to the current state + existingPlayer.setPlayerWidth(playerWidth) + } + } + + private fun updateMediaPaddings() { + val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val childCount = mediaContent.childCount + for (i in 0 until childCount) { + val mediaView = mediaContent.getChildAt(i) + val desiredPaddingEnd = if (i == childCount - 1) 0 else padding + val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams + if (layoutParams.marginEnd != desiredPaddingEnd) { + layoutParams.marginEnd = desiredPaddingEnd + mediaView.layoutParams = layoutParams + } + } + + } + + /** + * Set the current state of a view. This is updated often during animations and we shouldn't + * do anything expensive. + */ + fun setCurrentState(state: MediaState) { + currentState = state + currentlyExpanded = state.expansion > 0 + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + view.progress = state.expansion + } + } + + /** + * The desired location of this view has changed. We should remeasure the view to match + * the new bounds and kick off bounds animations if necessary. + * If an animation is happening, an animation is kicked of externally, which sets a new + * current state until we reach the targetState. + * + * @param desiredState the target state we're transitioning to + * @param animate should this be animated + */ + fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long, + startDelay: Long) { + if (desiredState is MediaHost.MediaHostState) { + // This is a hosting view, let's remeasure our players + this.desiredState = desiredState + val width = desiredState.boundsOnScreen.width() + if (playerWidth != width) { + setPlayerWidth(width) + for (mediaPlayer in mediaPlayers.values) { + if (animate && mediaPlayer.view.visibility == View.VISIBLE) { + mediaPlayer.animatePendingSizeChange(duration, startDelay) + } + } + val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0 + var left = 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + view.measure(widthSpec, heightSpec) + view.layout(left, 0, left + width, view.measuredHeight) + left = left + playerWidthPlusPadding + } + } + } + } + + fun setPlayerWidth(width: Int) { + if (width != playerWidth) { + playerWidth = width + playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize( + R.dimen.qs_media_padding) + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.setPlayerWidth(width) + } + // The player width has changed, let's update the scroll position to make sure + // it's still at the same place + var newScroll = activeMediaIndex * playerWidthPlusPadding + if (scrollIntoCurrentMedia > playerWidthPlusPadding) { + newScroll += playerWidthPlusPadding + - (scrollIntoCurrentMedia - playerWidthPlusPadding) + } else { + newScroll += scrollIntoCurrentMedia + } + mediaCarousel.scrollX = newScroll + } + } + + /** + * Get a measurement for the given input state. This measures the first player and returns + * its bounds as if it were measured with the given measurement dimensions + */ + fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null + // Let's measure the size of the first player and return its height + val previousProgress = firstPlayer.view.progress + val previousRight = firstPlayer.view.right + val previousBottom = firstPlayer.view.bottom + firstPlayer.view.progress = input.expansion + firstPlayer.measure(input) + // Relayouting is necessary in motionlayout to obtain its size properly .... + firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) + val result = MeasurementOutput(firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) + firstPlayer.view.progress = previousProgress + if (desiredState != null) { + // remeasure it to the old size again! + firstPlayer.measure(desiredState!!.measurementInput) + firstPlayer.view.layout(0, 0, previousRight, previousBottom) + } + return result + } + + fun onViewReattached() { + if (desiredState is MediaHost.MediaHostState) { + // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of + // a measure to force it. + val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0 + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.view.measure(widthSpec, heightSpec) + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt new file mode 100644 index 000000000000..8efc9549068a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt @@ -0,0 +1,31 @@ +package com.android.systemui.media + +import android.content.Context +import android.util.AttributeSet +import android.widget.HorizontalScrollView + +/** + * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful + * when only measuring children but not the parent, when trying to apply a new scroll position + */ +class UnboundHorizontalScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + : HorizontalScrollView(context, attrs, defStyleAttr) { + + /** + * Allow all scrolls to go through, use base implementation + */ + override fun scrollTo(x: Int, y: Int) { + if (mScrollX != x || mScrollY != y) { + val oldX: Int = mScrollX + val oldY: Int = mScrollY + mScrollX = x + mScrollY = y + invalidateParentCaches() + onScrollChanged(mScrollX, mScrollY, oldX, oldY) + if (!awakenScrollBars()) { + postInvalidateOnAnimation() + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index 8d6ce4718aef..7e2efc04ea8e 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -552,6 +552,9 @@ public class PipTaskOrganizer extends TaskOrganizer { ? null : destinationBounds; // As for the final windowing mode, simply reset it to undefined. wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { + wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); + } } else { taskBounds = destinationBounds; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt index c5ae3ab2c9fb..40d317c7bb22 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -117,7 +117,7 @@ class DoubleLineTileLayout( it.tileView.measure(exactly(smallTileSize), exactly(smallTileSize)) } - val height = twoLineHeight + val height = twoLineHeight + paddingBottom + paddingTop setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index a0ea7fae493d..ce002297e1a1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -269,14 +269,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha count++; } - - if (Utils.useQsMediaPlayer(mQsPanel.getContext())) { - View qsMediaView = mQsPanel.getMediaPanel(); - View qqsMediaView = mQuickQsPanel.getMediaPlayer().getView(); - translationXBuilder.addFloat(qsMediaView, "alpha", 0, 1); - translationXBuilder.addFloat(qqsMediaView, "alpha", 1, 0); - } - if (mAllowFancy) { // Make brightness appear static position and alpha in through second half. View brightness = mQsPanel.getBrightnessView(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index be8a8fd26150..6b0775f6c2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -42,7 +42,7 @@ public class QSContainerImpl extends FrameLayout { private QuickStatusBarHeader mHeader; private float mQsExpansion; private QSCustomizer mQSCustomizer; - private View mQSFooter; + private View mDragHandle; private View mBackground; private View mBackgroundGradient; @@ -62,7 +62,7 @@ public class QSContainerImpl extends FrameLayout { mQSDetail = findViewById(R.id.qs_detail); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); - mQSFooter = findViewById(R.id.qs_footer); + mDragHandle = findViewById(R.id.qs_drag_handle_view); mBackground = findViewById(R.id.quick_settings_background); mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); @@ -167,8 +167,8 @@ public class QSContainerImpl extends FrameLayout { int height = calculateContainerHeight(); setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height); - // Pin QS Footer to the bottom of the panel. - mQSFooter.setTranslationY(height - mQSFooter.getHeight()); + // Pin the drag handle to the bottom of the panel. + mDragHandle.setTranslationY(height - mDragHandle.getHeight()); mBackground.setTop(mQSPanel.getTop()); mBackground.setBottom(height); } @@ -192,13 +192,13 @@ public class QSContainerImpl extends FrameLayout { public void setExpansion(float expansion) { mQsExpansion = expansion; + mDragHandle.setAlpha(1.0f - expansion); updateExpansion(); } private void setMargins() { setMargins(mQSDetail); setMargins(mBackground); - setMargins(mQSFooter); mQSPanel.setMargins(mSideMargins); mHeader.setMargins(mSideMargins); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index 5de6d1c42b4f..fc8e36ff22cf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -98,7 +98,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private TouchAnimator mSettingsCogAnimator; private View mActionsContainer; - private View mDragHandle; private OnClickListener mExpandClickListener; @@ -146,7 +145,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mMultiUserSwitch = findViewById(R.id.multi_user_switch); mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mDragHandle = findViewById(R.id.qs_drag_handle_view); mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); @@ -219,7 +217,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, return new TouchAnimator.Builder() .addFloat(mActionsContainer, "alpha", 0, 1) .addFloat(mEditContainer, "alpha", 0, 1) - .addFloat(mDragHandle, "alpha", 1, 0, 0) .addFloat(mPageIndicator, "alpha", 0, 1) .setStartDelay(0.15f) .build(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 5b09267a9e68..865fd079234e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -37,6 +37,7 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.R.id; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizer; @@ -47,6 +48,7 @@ import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.InjectionInflationController; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import javax.inject.Inject; @@ -91,6 +93,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca */ private int mState; private QSContainerImplController mQSContainerImplController; + private int[] mTmpLocation = new int[2]; @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, @@ -377,8 +380,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mLastKeyguardAndExpanded = onKeyguardAndExpanded; boolean fullyExpanded = expansion == 1; - int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom() - + mFooter.getHeight(); + int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom(); float panelTranslationY = translationScaleY * heightDiff; // Let the views animate their contents correctly by giving them the necessary context. @@ -404,6 +406,32 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mQSAnimator != null) { mQSAnimator.setPosition(expansion); } + updateMediaPositions(); + } + + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + mContainer.getLocationOnScreen(mTmpLocation); + float absoluteBottomPosition = mTmpLocation[1] + mContainer.getHeight(); + pinToBottom(absoluteBottomPosition, mQSPanel.getMediaHost()); + pinToBottom(absoluteBottomPosition - mHeader.getPaddingBottom(), + mHeader.getHeaderQsPanel().getMediaHost()); + } + } + + private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost) { + View hostView = mediaHost.getHostView(); + if (mLastQSExpansion > 0) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) hostView.getLayoutParams(); + float targetPosition = absoluteBottomPosition - params.bottomMargin + - hostView.getHeight(); + float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top + - hostView.getTranslationY(); + hostView.setTranslationY(targetPosition - currentPosition); + } else { + hostView.setTranslationY(0); + } } private boolean headerWillBeAnimating() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java deleted file mode 100644 index 174441bdf065..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2019 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.qs; - -import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaDescription; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import com.android.settingslib.media.LocalMediaManager; -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.media.SeekBarObserver; -import com.android.systemui.media.SeekBarViewModel; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import java.util.concurrent.Executor; - -/** - * Single media player for carousel in QSPanel - */ -public class QSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QSMediaPlayer"; - - // Button IDs for QS controls - static final int[] QS_ACTION_IDS = { - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - }; - - private final QSPanel mParent; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private final SeekBarViewModel mSeekBarViewModel; - private final SeekBarObserver mSeekBarObserver; - private String mPackageName; - - /** - * Initialize quick shade version of player - * @param context - * @param parent - * @param routeManager Provides information about device - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QSMediaPlayer(Context context, ViewGroup parent, LocalMediaManager routeManager, - Executor foregroundExecutor, DelayableExecutor backgroundExecutor, - ActivityStarter activityStarter) { - super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - mParent = (QSPanel) parent; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); - mSeekBarObserver = new SeekBarObserver(getView()); - // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust - // priority of players. As soon as it is removed, the lifecycle will end and the seek bar - // will stop updating. So, use the lifecycle of the parent instead. - mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver); - SeekBar bar = getView().findViewById(R.id.media_progress_bar); - bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); - bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); - } - - /** - * Add a media panel view based on a media description. Used for resumption - * @param description - * @param iconColor - * @param bgColor - * @param contentIntent - * @param pkgName - */ - public void setMediaSession(MediaSession.Token token, MediaDescription description, - int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { - mPackageName = pkgName; - PackageManager pm = getContext().getPackageManager(); - Drawable icon = null; - CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); - try { - icon = pm.getApplicationIcon(pkgName); - appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Error getting package information", e); - } - - // Set what we can normally - super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent, - appName.toString(), null); - - // Then add info from MediaDescription - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); - } - - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - CharSequence songName = description.getTitle(); - titleText.setText(songName); - titleText.setTextColor(iconColor); - - // Artist name (not in mini player) - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - CharSequence artistName = description.getSubtitle(); - artistText.setText(artistName); - artistText.setTextColor(iconColor); - } - - initLongPressMenu(iconColor); - - // Set buttons to resume state - resetButtons(); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param contentIntent Intent to send when user taps on player - * @param appName Application title - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent, - String appName, String key) { - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, - key); - - // Media controls - if (actionsContainer != null) { - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - final ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } - - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); - } - } - - // Seek Bar - final MediaController controller = new MediaController(getContext(), token); - mBackgroundExecutor.execute( - () -> mSeekBarViewModel.updateController(controller, iconColor)); - - initLongPressMenu(iconColor); - } - - private void initLongPressMenu(int iconColor) { - // Set up long press menu - View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - options.setMinimumHeight(guts.getHeight()); - - View clearView = options.findViewById(R.id.remove); - clearView.setOnClickListener(b -> { - removePlayer(); - }); - ImageView removeIcon = options.findViewById(R.id.remove_icon); - removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); - TextView removeText = options.findViewById(R.id.remove_text); - removeText.setTextColor(iconColor); - - TextView cancelView = options.findViewById(R.id.cancel); - cancelView.setTextColor(iconColor); - cancelView.setOnClickListener(b -> { - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); - }); - // ... but don't enable it yet, and make sure is reset when the session is updated - mMediaNotifView.setOnLongClickListener(null); - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); - } - - @Override - protected void resetButtons() { - super.resetButtons(); - mSeekBarViewModel.clearController(); - View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - - mMediaNotifView.setOnLongClickListener(v -> { - // Replace player view with close/cancel view - guts.setVisibility(View.GONE); - options.setVisibility(View.VISIBLE); - return true; // consumed click - }); - } - - /** - * Sets the listening state of the player. - * - * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid - * unnecessary work when the QS panel is closed. - * - * @param listening True when player should be active. Otherwise, false. - */ - public void setListening(boolean listening) { - mSeekBarViewModel.setListening(listening); - } - - @Override - public void removePlayer() { - Log.d(TAG, "removing player from parent: " + mParent); - // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) - mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); - } - - @Override - public String getMediaPlayerPackage() { - if (getController() == null) { - return mPackageName; - } - return super.getMediaPlayerPackage(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index e8f6c9668e9b..80e5071c6b43 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -32,23 +32,19 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.media.MediaDescription; -import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; -import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import com.android.internal.logging.MetricsLogger; @@ -56,17 +52,14 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.NotificationVisibility; import com.android.settingslib.Utils; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.media.InfoMediaManager; -import com.android.settingslib.media.LocalMediaManager; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaControlPanel; +import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; @@ -77,20 +70,16 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.statusbar.notification.NotificationEntryListener; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.inject.Inject; @@ -108,21 +97,13 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; + protected final MediaHost mMediaHost; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final QSTileRevealController mQsTileRevealController; - private final LinearLayout mMediaCarousel; - private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>(); - private final LocalBluetoothManager mLocalBluetoothManager; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private boolean mUpdateCarousel = false; - private ActivityStarter mActivityStarter; - private NotificationEntryManager mNotificationEntryManager; - protected boolean mExpanded; protected boolean mListening; @@ -158,15 +139,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } }; - private final NotificationEntryListener mNotificationEntryListener = - new NotificationEntryListener() { - @Override - public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility, - boolean removedByUser, int reason) { - checkToRemoveMediaNotification(entry); - } - }; - @Inject public QSPanel( @Named(VIEW_CONTEXT) Context context, @@ -174,23 +146,15 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, - ActivityStarter activityStarter, - NotificationEntryManager entryManager, + MediaHost mediaHost, UiEventLogger uiEventLogger ) { super(context, attrs); + mMediaHost = mediaHost; mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mLocalBluetoothManager = localBluetoothManager; mBroadcastDispatcher = broadcastDispatcher; - mActivityStarter = activityStarter; - mNotificationEntryManager = entryManager; mUiEventLogger = uiEventLogger; setOrientation(VERTICAL); @@ -210,16 +174,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne addDivider(); - // Add media carousel - if (useQsMediaPlayer(context)) { - HorizontalScrollView mediaScrollView = (HorizontalScrollView) LayoutInflater.from( - mContext).inflate(R.layout.media_carousel, this, false); - mMediaCarousel = mediaScrollView.findViewById(R.id.media_carousel); - addView(mediaScrollView, 0); - } else { - mMediaCarousel = null; - } - mFooter = new QSSecurityFooter(this, context); addView(mFooter.getView()); @@ -230,145 +184,39 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - if (!isVisible && mUpdateCarousel) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.isPlaying()) { - LayoutParams lp = (LayoutParams) player.getView().getLayoutParams(); - mMediaCarousel.removeView(player.getView()); - mMediaCarousel.addView(player.getView(), 0, lp); - ((HorizontalScrollView) mMediaCarousel.getParent()).fullScroll(View.FOCUS_LEFT); - mUpdateCarousel = false; - break; - } - } - } - } - - /** - * Add or update a player for the associated media session - * @param token - * @param icon - * @param largeIcon - * @param iconColor - * @param bgColor - * @param actionsContainer - * @param notif - * @param key - */ - public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, - String key) { - if (!useQsMediaPlayer(mContext)) { - // Shouldn't happen, but just in case - Log.e(TAG, "Tried to add media session without player!"); - return; - } - if (token == null) { - Log.e(TAG, "Media session token was null!"); - return; - } - - String packageName = notif.getPackageName(); - QSMediaPlayer player = findMediaPlayer(packageName, token, key); - - int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); - int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); - LayoutParams lp = new LayoutParams(playerWidth, ViewGroup.LayoutParams.MATCH_PARENT); - lp.setMarginStart(padding); - lp.setMarginEnd(padding); - - if (player == null) { - Log.d(TAG, "creating new player for " + packageName); - // Set up listener for device changes - // TODO: integrate with MediaTransferManager? - InfoMediaManager imm = new InfoMediaManager(mContext, notif.getPackageName(), - notif.getNotification(), mLocalBluetoothManager); - LocalMediaManager routeManager = new LocalMediaManager(mContext, mLocalBluetoothManager, - imm, notif.getPackageName()); - - player = new QSMediaPlayer(mContext, this, routeManager, mForegroundExecutor, - mBackgroundExecutor, mActivityStarter); - player.setListening(mListening); - if (player.isPlaying()) { - mMediaCarousel.addView(player.getView(), 0, lp); // add in front - } else { - mMediaCarousel.addView(player.getView(), lp); // add at end - } - mMediaPlayers.add(player); - } else if (player.isPlaying()) { - mUpdateCarousel = true; - } - - Log.d(TAG, "setting player session"); - String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) - .loadHeaderAppName(); - player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer, - notif.getNotification().contentIntent, appName, key); - - if (mMediaPlayers.size() > 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); - } - } - - /** - * Check for an existing media player using the given information - * @param packageName - * @param token - * @param key - * @return a player, or null if no match found - */ - private QSMediaPlayer findMediaPlayer(String packageName, MediaSession.Token token, - String key) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.getKey() == null || key == null) { - // No notification key = loaded via mediabrowser, so just match on package - if (packageName.equals(player.getMediaPlayerPackage())) { - Log.d(TAG, "Found matching resume player by package: " + packageName); - return player; - } - } else if (player.getMediaSessionToken().equals(token)) { - Log.d(TAG, "Found matching player by token " + packageName); - return player; - } else if (packageName.equals(player.getMediaPlayerPackage()) - && key.equals(player.getKey())) { - // Also match if it's the same package and notification key - Log.d(TAG, "Found matching player by package " + packageName + ", " + key); - return player; - } - } - return null; - } - - protected View getMediaPanel() { - return mMediaCarousel; - } - - /** - * Remove the media player from the carousel - * @param player Player to remove - * @return true if removed, false if player was not found - */ - protected boolean removeMediaPlayer(QSMediaPlayer player) { - // Remove from list - if (!mMediaPlayers.remove(player)) { - return false; - } - - // Check if we need to collapse the carousel now - mMediaCarousel.removeView(player.getView()); - if (mMediaPlayers.size() == 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.GONE); - } - return true; + protected void onFinishInflate() { + super.onFinishInflate(); + // Add media carousel at the end + if (useQsMediaPlayer(getContext())) { + addMediaHostView(); + } + } + + protected void addMediaHostView() { + mMediaHost.init(MediaHierarchyManager.LOCATION_QS); + mMediaHost.setExpansion(1.0f); + mMediaHost.setShowsOnlyActiveMedia(false); + ViewGroup hostView = mMediaHost.getHostView(); + addView(hostView); + int sidePaddings = getResources().getDimensionPixelSize( + R.dimen.quick_settings_side_margins); + int bottomPadding = getResources().getDimensionPixelSize( + R.dimen.quick_settings_expanded_bottom_margin); + MarginLayoutParams layoutParams = (MarginLayoutParams) hostView.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.bottomMargin = bottomPadding; + hostView.setLayoutParams(layoutParams); + hostView.setPadding(sidePaddings, hostView.getPaddingTop(), sidePaddings, + hostView.getPaddingBottom()); } private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { @Override public void addTrack(MediaDescription desc, ComponentName component, QSMediaBrowser browser) { - if (component == null) { + // TODO: Fix Resumption b/156104922 +/* if (component == null) { Log.e(TAG, "Component cannot be null"); return; } @@ -404,7 +252,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int iconColor = Color.DKGRAY; int bgColor = Color.LTGRAY; player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), - pkgName); + pkgName);*/ } }; @@ -441,27 +289,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mHasLoadedMediaControls = true; } - private void checkToRemoveMediaNotification(NotificationEntry entry) { - if (!useQsMediaPlayer(mContext)) { - return; - } - - if (!entry.isMediaNotification()) { - return; - } - - // If this entry corresponds to an existing set of controls, clear the controls - // This will handle apps that use an action to clear their notification - for (QSMediaPlayer p : mMediaPlayers) { - if (p.getKey() != null && p.getKey().equals(entry.getKey())) { - Log.d(TAG, "Clearing controls since notification removed " + entry.getKey()); - p.clearControls(); - return; - } - } - Log.d(TAG, "Media notification removed but no player found " + entry.getKey()); - } - protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -482,7 +309,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { View child = getChildAt(i); - if (child.getVisibility() != View.GONE) height += child.getMeasuredHeight(); + if (child.getVisibility() != View.GONE) { + height += child.getMeasuredHeight(); + MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); + height += layoutParams.topMargin + layoutParams.bottomMargin; + } } setMeasuredDimension(getMeasuredWidth(), height); } @@ -528,7 +359,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne loadMediaResumptionControls(); } } - mNotificationEntryManager.addNotificationEntryListener(mNotificationEntryListener); } @Override @@ -545,7 +375,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } mDumpManager.unregisterDumpable(getDumpableTag()); mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); - mNotificationEntryManager.removeNotificationEntryListener(mNotificationEntryListener); super.onDetachedFromWindow(); } @@ -716,7 +545,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public void setListening(boolean listening) { if (mListening == listening) return; - mListening = listening; if (mTileLayout != null) { mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); mTileLayout.setListening(listening); @@ -724,9 +552,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (mListening) { refreshAllTiles(); } - for (QSMediaPlayer player : mMediaPlayers) { - player.setListening(mListening); - } } private String getTilesSpecs() { @@ -1027,6 +852,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } + public MediaHost getMediaHost() { + return mMediaHost; + } + private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java deleted file mode 100644 index 5cb75e60e22a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019 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.qs; - -import android.app.PendingIntent; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.plugins.ActivityStarter; - -import java.util.concurrent.Executor; - -/** - * QQS mini media player - */ -public class QuickQSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QQSMediaPlayer"; - - // Button IDs for QS controls - private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - /** - * Initialize mini media player for QQS - * @param context - * @param parent - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor, - Executor backgroundExecutor, ActivityStarter activityStarter) { - super(context, parent, null, R.layout.qqs_media_panel, QQS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param actionsToShow indices of which actions to display in the mini player - * (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT) - * @param contentIntent Intent to send when user taps on the view - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, - PendingIntent contentIntent, String key) { - // Only update if this is a different session and currently playing - String oldPackage = ""; - if (getController() != null) { - oldPackage = getController().getPackageName(); - } - MediaController controller = new MediaController(getContext(), token); - MediaSession.Token currentToken = getMediaSessionToken(); - boolean samePlayer = currentToken != null - && currentToken.equals(token) - && oldPackage.equals(controller.getPackageName()); - if (getController() != null && !samePlayer && !isPlaying(controller)) { - return; - } - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key); - - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - if (actionsToShow != null) { - int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount()); - maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length); - for (; i < maxButtons; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - int thatId = NOTIF_ACTION_IDS[actionsToShow[i]]; - ImageButton thatBtn = parentActionsLayout.findViewById(thatId); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - thatBtn.performClick(); - }); - } - } - - // Hide any unused buttons - for (; i < QQS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 6683a1ce4f4f..dfd385dda8e5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -18,38 +18,34 @@ package com.android.systemui.qs; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; -import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.internal.logging.UiEventLogger; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -67,16 +63,17 @@ public class QuickQSPanel extends QSPanel { private boolean mDisabledByPolicy; private int mMaxTiles; protected QSPanel mFullPanel; - private QuickQSMediaPlayer mMediaPlayer; /** Whether or not the QS media player feature is enabled. */ private boolean mUsingMediaPlayer; /** Whether or not the QuickQSPanel currently contains a media player. */ - private boolean mHasMediaPlayer; + private boolean mShowHorizontalTileLayout; private LinearLayout mHorizontalLinearLayout; // Only used with media - private QSTileLayout mMediaTileLayout; + private QSTileLayout mHorizontalTileLayout; private QSTileLayout mRegularTileLayout; + private int mLastOrientation = -1; + private int mMediaBottomMargin; @Inject public QuickQSPanel( @@ -85,16 +82,11 @@ public class QuickQSPanel extends QSPanel { DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, - ActivityStarter activityStarter, - NotificationEntryManager entryManager, + MediaHost mediaHost, UiEventLogger uiEventLogger ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, - foregroundExecutor, backgroundExecutor, localBluetoothManager, activityStarter, - entryManager, uiEventLogger); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, + uiEventLogger); if (mFooter != null) { removeView(mFooter.getView()); } @@ -104,6 +96,8 @@ public class QuickQSPanel extends QSPanel { } removeView((View) mTileLayout); } + mMediaBottomMargin = getResources().getDimensionPixelSize( + R.dimen.quick_settings_media_extra_bottom_margin); mUsingMediaPlayer = Utils.useQsMediaPlayer(context); if (mUsingMediaPlayer) { @@ -112,40 +106,95 @@ public class QuickQSPanel extends QSPanel { mHorizontalLinearLayout.setClipChildren(false); mHorizontalLinearLayout.setClipToPadding(false); - int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); - mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout, - foregroundExecutor, backgroundExecutor, activityStarter); - LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); - lp2.setMarginEnd(marginSize); - lp2.setMarginStart(0); - mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp2); - - mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger); - mMediaTileLayout = mTileLayout; + DoubleLineTileLayout horizontalTileLayout = new DoubleLineTileLayout(context, + mUiEventLogger); + horizontalTileLayout.setPaddingRelative( + horizontalTileLayout.getPaddingStart(), + horizontalTileLayout.getPaddingTop(), + horizontalTileLayout.getPaddingEnd(), + mContext.getResources().getDimensionPixelSize( + R.dimen.qqs_horizonal_tile_padding_bottom)); + mHorizontalTileLayout = horizontalTileLayout; mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger); - LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); - lp.setMarginEnd(0); - lp.setMarginStart(marginSize); - mHorizontalLinearLayout.addView((View) mTileLayout, lp); + LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); + int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); + lp.setMarginStart(0); + lp.setMarginEnd(marginSize); + lp.gravity = Gravity.CENTER_VERTICAL; + mHorizontalLinearLayout.addView((View) mHorizontalTileLayout, lp); sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); + boolean useHorizontal = shouldUseHorizontalTileLayout(); + mTileLayout = useHorizontal ? mHorizontalTileLayout : mRegularTileLayout; mTileLayout.setListening(mListening); addView(mHorizontalLinearLayout, 0 /* Between brightness and footer */); - ((View) mRegularTileLayout).setVisibility(View.GONE); + ((View) mRegularTileLayout).setVisibility(!useHorizontal ? View.VISIBLE : View.GONE); + mHorizontalLinearLayout.setVisibility(useHorizontal ? View.VISIBLE : View.GONE); addView((View) mRegularTileLayout, 0); super.setPadding(0, 0, 0, 0); + applySideMargins(mHorizontalLinearLayout); + applyBottomMargin((View) mRegularTileLayout); } else { sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); mTileLayout = new HeaderTileLayout(context, mUiEventLogger); mTileLayout.setListening(mListening); addView((View) mTileLayout, 0 /* Between brightness and footer */); super.setPadding(0, 0, 0, 0); + applyBottomMargin((View) mTileLayout); } } - public QuickQSMediaPlayer getMediaPlayer() { - return mMediaPlayer; + private void applyBottomMargin(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_bottom); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.bottomMargin = margin; + view.setLayoutParams(layoutParams); + } + + private void applySideMargins(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMarginStart(margin); + layoutParams.setMarginEnd(margin); + view.setLayoutParams(layoutParams); + } + + private void reAttachMediaHost() { + if (mMediaHost == null) { + return; + } + boolean horizontal = shouldUseHorizontalTileLayout(); + ViewGroup host = mMediaHost.getHostView(); + ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; + ViewGroup currentParent = (ViewGroup) host.getParent(); + if (currentParent != newParent) { + if (currentParent != null) { + currentParent.removeView(host); + } + newParent.addView(host); + LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.weight = horizontal ? 1.5f : 0; + layoutParams.bottomMargin = mMediaBottomMargin; + int marginStart = horizontal + ? getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal) + : 0; + layoutParams.setMarginStart(marginStart); + } + } + + @Override + protected void addMediaHostView() { + mMediaHost.setVisibleChangedListener((visible) -> { + switchTileLayout(); + return null; + }); + mMediaHost.init(MediaHierarchyManager.LOCATION_QQS); + mMediaHost.setExpansion(0.0f); + mMediaHost.setShowsOnlyActiveMedia(true); + reAttachMediaHost(); } @Override @@ -191,10 +240,19 @@ public class QuickQSPanel extends QSPanel { super.drawTile(r, state); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation != mLastOrientation) { + mLastOrientation = newConfig.orientation; + switchTileLayout(); + } + } + boolean switchTileLayout() { if (!mUsingMediaPlayer) return false; - mHasMediaPlayer = mMediaPlayer.hasMediaSession(); - if (mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.GONE) { + mShowHorizontalTileLayout = shouldUseHorizontalTileLayout(); + if (mShowHorizontalTileLayout && mHorizontalLinearLayout.getVisibility() == View.GONE) { mHorizontalLinearLayout.setVisibility(View.VISIBLE); ((View) mRegularTileLayout).setVisibility(View.GONE); mTileLayout.setListening(false); @@ -202,11 +260,13 @@ public class QuickQSPanel extends QSPanel { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); } - mTileLayout = mMediaTileLayout; + mTileLayout = mHorizontalTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; - } else if (!mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { + } else if (!mShowHorizontalTileLayout + && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { mHorizontalLinearLayout.setVisibility(View.GONE); ((View) mRegularTileLayout).setVisibility(View.VISIBLE); mTileLayout.setListening(false); @@ -217,14 +277,21 @@ public class QuickQSPanel extends QSPanel { mTileLayout = mRegularTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; } return false; } - /** Returns true if this panel currently contains a media player. */ - public boolean hasMediaPlayer() { - return mHasMediaPlayer; + private boolean shouldUseHorizontalTileLayout() { + return mMediaHost.getVisible() + && getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + + /** Returns true if this panel currently uses a horizontal tile layout. */ + public boolean usesHorizontalLayout() { + return mShowHorizontalTileLayout; } @Override @@ -341,7 +408,7 @@ public class QuickQSPanel extends QSPanel { setClipChildren(false); setClipToPadding(false); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER_HORIZONTAL; setLayoutParams(lp); } @@ -354,6 +421,7 @@ public class QuickQSPanel extends QSPanel { @Override public void onFinishInflate(){ + super.onFinishInflate(); updateResources(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b15c6a3e3b59..3b2bea8f80f2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -220,6 +220,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements mNextAlarmTextView.setSelected(true); } + public QuickQSPanel getHeaderQsPanel() { + return mHeaderQsPanel; + } + private List<String> getIgnoredIconSlots() { ArrayList<String> ignored = new ArrayList<>(); ignored.add(mContext.getResources().getString( @@ -336,23 +340,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements com.android.internal.R.dimen.quick_qs_offset_height); mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - - if (mQsDisabled) { - lp.height = resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_offset_height); - } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.hasMediaPlayer()) { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height_with_media)); - } else { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height)); - } - - setLayoutParams(lp); - updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java index 6249f82ace9e..aa63b4077b1b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailItemView.java @@ -106,6 +106,7 @@ public class UserDetailItemView extends LinearLayout { } public void setEnabled(boolean enabled) { + super.setEnabled(enabled); mName.setEnabled(enabled); mAvatar.setEnabled(enabled); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index c247328078a7..abd7e7159260 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -72,6 +72,7 @@ public class ScreenRecordDialog extends Activity { window.getDecorView(); window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); window.setGravity(Gravity.TOP); + setTitle(R.string.screenrecord_name); setContentView(R.layout.screen_record_dialog); diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java index e67b3d715c84..02a7aca38abe 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java @@ -813,4 +813,12 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, updateVisibility(true /* visible */); } } + + /** @return the container token for the secondary split root task. */ + public WindowContainerToken getSecondaryRoot() { + if (mSplits == null || mSplits.mSecondary == null) { + return null; + } + return mSplits.mSecondary.token; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index e32d174d7c77..9f4932e74eaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -44,14 +44,17 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; +import androidx.annotation.NonNull; + import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.statusbar.NotificationVisibility; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaData; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; @@ -113,7 +116,6 @@ public class NotificationMediaManager implements Dumpable { private ScrimController mScrimController; @Nullable private LockscreenWallpaper mLockscreenWallpaper; - private final KeyguardMediaPlayer mMediaPlayer; private final Executor mMainExecutor; @@ -187,13 +189,12 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfig) { + DeviceConfigProxy deviceConfig, + MediaDataManager mediaDataManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; - mMediaPlayer = keyguardMediaPlayer; mMediaListeners = new ArrayList<>(); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state @@ -204,14 +205,26 @@ public class NotificationMediaManager implements Dumpable { mNotificationShadeWindowController = notificationShadeWindowController; mEntryManager = notificationEntryManager; mMainExecutor = mainExecutor; + notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + @Override public void onPendingEntryAdded(NotificationEntry entry) { - findAndUpdateMediaNotifications(); + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override public void onPreEntryUpdated(NotificationEntry entry) { + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + + @Override + public void onEntryInflated(NotificationEntry entry) { + findAndUpdateMediaNotifications(); + } + + @Override + public void onEntryReinflated(NotificationEntry entry) { findAndUpdateMediaNotifications(); } @@ -222,6 +235,7 @@ public class NotificationMediaManager implements Dumpable { boolean removedByUser, int reason) { onNotificationRemoved(entry.getKey()); + mediaDataManager.onNotificationRemoved(entry.getKey()); } }); @@ -278,7 +292,7 @@ public class NotificationMediaManager implements Dumpable { public void addCallback(MediaListener callback) { mMediaListeners.add(callback); - callback.onMetadataOrStateChanged(mMediaMetadata, + callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, getMediaControllerPlaybackState(mMediaController)); } @@ -392,7 +406,7 @@ public class NotificationMediaManager implements Dumpable { @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners); for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onMetadataOrStateChanged(mMediaMetadata, state); + callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state); } } @@ -473,7 +487,6 @@ public class NotificationMediaManager implements Dumpable { && mBiometricUnlockController.isWakeAndUnlock(); if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) { mBackdrop.setVisibility(View.INVISIBLE); - mMediaPlayer.clearControls(); Trace.endSection(); return; } @@ -496,14 +509,6 @@ public class NotificationMediaManager implements Dumpable { } } - NotificationEntry entry = mEntryManager - .getActiveNotificationUnfiltered(mMediaNotificationKey); - if (entry != null) { - mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata); - } else { - mMediaPlayer.clearControls(); - } - // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { @@ -626,7 +631,6 @@ public class NotificationMediaManager implements Dumpable { // We are unlocking directly - no animation! mBackdrop.setVisibility(View.GONE); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); if (windowController != null) { windowController.setBackdropShowing(false); } @@ -643,7 +647,6 @@ public class NotificationMediaManager implements Dumpable { mBackdrop.setVisibility(View.GONE); mBackdropFront.animate().cancel(); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); mMainExecutor.execute(mHideBackdropFront); }); if (mKeyguardStateController.isKeyguardFadingAway()) { @@ -750,6 +753,7 @@ public class NotificationMediaManager implements Dumpable { * @param state Current playback state * @see PlaybackState.State */ - void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state); + default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java index 3cb2a2aaeec7..1079f10497a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java @@ -52,7 +52,7 @@ public interface NotificationPresenter extends ExpandableNotificationRow.OnExpan /** * Updates the visual representation of the notifications. */ - void updateNotificationViews(); + void updateNotificationViews(String reason); /** * Returns the maximum number of notifications to show while locked. 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 de7e36d97b22..f0fed13114ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.MediaArtworkProcessor; @@ -95,9 +95,9 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfigProxy) { + DeviceConfigProxy deviceConfigProxy, + MediaDataManager mediaDataManager) { return new NotificationMediaManager( context, statusBarLazy, @@ -105,9 +105,9 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, - keyguardMediaPlayer, mainExecutor, - deviceConfigProxy); + deviceConfigProxy, + mediaDataManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java index 75b41ca3e162..eee9cc683e2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.notification; +import android.graphics.drawable.Drawable; import android.util.FloatProperty; +import android.util.Log; import android.util.Property; import android.view.View; @@ -35,6 +37,100 @@ public abstract class AnimatableProperty { public static final AnimatableProperty Y = AnimatableProperty.from(View.Y, R.id.y_animator_tag, R.id.y_animator_tag_start_value, R.id.y_animator_tag_end_value); + /** + * Similar to X, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_X = AnimatableProperty.from( + new FloatProperty<View>("ViewAbsoluteX") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_x_current_value, value); + View.X.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_x_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.X.get(view); + } + }, + R.id.absolute_x_animator_tag, + R.id.absolute_x_animator_start_tag, + R.id.absolute_x_animator_end_tag); + + /** + * Similar to Y, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_Y = AnimatableProperty.from( + new FloatProperty<View>("ViewAbsoluteY") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_y_current_value, value); + View.Y.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_y_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.Y.get(view); + } + }, + R.id.absolute_y_animator_tag, + R.id.absolute_y_animator_start_tag, + R.id.absolute_y_animator_end_tag); + + public static final AnimatableProperty WIDTH = AnimatableProperty.from( + new FloatProperty<View>("ViewWidth") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_width_current_value, value); + view.setRight((int) (view.getLeft() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_width_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getWidth(); + } + }, + R.id.view_width_animator_tag, + R.id.view_width_animator_start_tag, + R.id.view_width_animator_end_tag); + + public static final AnimatableProperty HEIGHT = AnimatableProperty.from( + new FloatProperty<View>("ViewHeight") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_height_current_value, value); + view.setBottom((int) (view.getTop() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_height_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getHeight(); + } + }, + R.id.view_height_animator_tag, + R.id.view_height_animator_start_tag, + R.id.view_height_animator_end_tag); + public abstract int getAnimationStartTag(); public abstract int getAnimationEndTag(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index d2517774ab2e..d6471243e053 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -674,7 +674,7 @@ public class NotificationEntryManager implements public void updateNotifications(String reason) { reapplyFilterAndSort(reason); if (mPresenter != null && !mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { - mPresenter.updateNotificationViews(); + mPresenter.updateNotificationViews(reason); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java index 1f9d3af70b4f..b1b6a1c12a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java @@ -34,13 +34,20 @@ import com.android.systemui.statusbar.notification.stack.ViewState; */ public class PropertyAnimator { + /** + * Set a property on a view, updating its value, even if it's already animating. + * The @param animated can be used to request an animation. + * If the view isn't animated, this utility will update the current animation if existent, + * such that the end value will point to @param newEndValue or apply it directly if there's + * no animation. + */ public static <T extends View> void setProperty(final T view, AnimatableProperty animatableProperty, float newEndValue, AnimationProperties properties, boolean animated) { int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); if (previousAnimator != null || animated) { - startAnimation(view, animatableProperty, newEndValue, properties); + startAnimation(view, animatableProperty, newEndValue, animated ? properties : null); } else { // no new animation needed, let's just apply the value animatableProperty.getProperty().set(view, newEndValue); @@ -60,8 +67,8 @@ public class PropertyAnimator { } int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); - AnimationFilter filter = properties.getAnimationFilter(); - if (!filter.shouldAnimateProperty(property)) { + AnimationFilter filter = properties != null ? properties.getAnimationFilter() : null; + if (filter == null || !filter.shouldAnimateProperty(property)) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the @@ -82,6 +89,14 @@ public class PropertyAnimator { } Float currentValue = property.get(view); + AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); + if (currentValue.equals(newEndValue)) { + // Skip the animation! + if (listener != null) { + listener.onAnimationEnd(null); + } + return; + } ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue); animator.addUpdateListener( animation -> property.set(view, (Float) animation.getAnimatedValue())); @@ -96,7 +111,6 @@ public class PropertyAnimator { || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } - AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); if (listener != null) { animator.addListener(listener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index cb0c2838c24d..634872d9d761 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -209,7 +209,7 @@ public final class NotificationEntry extends ListEntry { } /** The key for this notification. Guaranteed to be immutable and unique */ - public String getKey() { + @NonNull public String getKey() { return mKey; } @@ -217,7 +217,7 @@ public final class NotificationEntry extends ListEntry { * The StatusBarNotification that represents one half of a NotificationEntry (the other half * being the Ranking). This object is swapped out whenever a notification is updated. */ - public StatusBarNotification getSbn() { + @NonNull public StatusBarNotification getSbn() { return mSbn; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java index 59f119e987b4..3fab6f7c3857 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.notifcollection; +import android.annotation.NonNull; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; @@ -43,13 +44,13 @@ public interface NotifCollectionListener { * there is no guarantee of order and they may not have had a chance to initialize yet. Instead, * use {@link #onEntryAdded} which is called after all initialization. */ - default void onEntryInit(NotificationEntry entry) { + default void onEntryInit(@NonNull NotificationEntry entry) { } /** * Called whenever a notification with a new key is posted. */ - default void onEntryAdded(NotificationEntry entry) { + default void onEntryAdded(@NonNull NotificationEntry entry) { } /** @@ -64,7 +65,7 @@ public interface NotifCollectionListener { * immediately after a user dismisses a notification: we wait until we receive confirmation from * system server before considering the notification removed. */ - default void onEntryRemoved(NotificationEntry entry, @CancellationReason int reason) { + default void onEntryRemoved(@NonNull NotificationEntry entry, @CancellationReason int reason) { } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt index 88888d10e283..0fd865b603f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt @@ -75,7 +75,7 @@ class BypassHeadsUpNotifier @Inject constructor( mediaManager.addCallback(this) } - override fun onMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { + override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { val previous = currentMediaEntry var newEntry = entryManager .getActiveNotificationUnfiltered(mediaManager.mediaNotificationKey) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 5797944298d4..0831c0b66797 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -767,6 +767,10 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { return mContentTranslation; } + public boolean wantsAddAndRemoveAnimations() { + return true; + } + /** * A listener notifying when {@link #getActualHeight} changes. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index 0ccebc130b1d..56f8e087d64d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -109,7 +109,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveText(Notification notification) { + public static CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); if (contentText == null) { contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); @@ -118,7 +118,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveTitle(Notification notification) { + public static CharSequence resolveTitle(Notification notification) { CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE); if (titleText == null) { titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index b96cff830f31..93d3f3bdbe96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -178,38 +178,6 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras .getParcelable(Notification.EXTRA_MEDIA_SESSION); - if (Utils.useQsMediaPlayer(mContext) && token != null) { - final int[] compactActions = mRow.getEntry().getSbn().getNotification().extras - .getIntArray(Notification.EXTRA_COMPACT_ACTIONS); - int tintColor = getNotificationHeader().getOriginalIconColor(); - NotificationShadeWindowController ctrl = Dependency.get( - NotificationShadeWindowController.class); - QuickQSPanel panel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_qs_panel); - StatusBarNotification sbn = mRow.getEntry().getSbn(); - Notification notif = sbn.getNotification(); - Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); - panel.getMediaPlayer().setMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - compactActions, - notif.contentIntent, - sbn.getKey()); - QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_settings_panel); - bigPanel.addMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - sbn, - sbn.getKey()); - } - boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar(); if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) { if (mSeekBarView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java index ab055e1bdc36..3ac322fec071 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack; import android.content.Context; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -37,7 +38,6 @@ public class MediaHeaderView extends ActivatableNotificationView { @Override protected void onFinishInflate() { super.onFinishInflate(); - mContentView = findViewById(R.id.keyguard_media_view); } @Override @@ -52,4 +52,17 @@ public class MediaHeaderView extends ActivatableNotificationView { public void setBackgroundColor(int color) { setTintColor(color); } + + public void setContentView(ViewGroup contentView) { + mContentView = contentView; + addView(contentView); + ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + @Override + public boolean wantsAddAndRemoveAnimations() { + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt new file mode 100644 index 000000000000..9cf1f74ea418 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt @@ -0,0 +1,55 @@ +/* + * 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.statusbar.notification.stack + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.NotificationSectionLog +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "NotifSections" + +@Singleton +class NotificationSectionsLogger @Inject constructor( + @NotificationSectionLog private val logBuffer: LogBuffer +) { + + fun logStartSectionUpdate(reason: String) = logBuffer.log( + TAG, + LogLevel.DEBUG, + { str1 = reason }, + { "Updating section boundaries: $reason" } + ) + + fun logStr(str: String) = logBuffer.log( + TAG, + LogLevel.DEBUG, + { str1 = str }, + { str1 ?: "" } + ) + + fun logPosition(position: Int, label: String) = logBuffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = position + str1 = label + }, + { "$int1: $str1" } + ) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java index 6eec1ca33e14..dcf30ca81e95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java @@ -29,10 +29,12 @@ import android.content.Intent; import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import com.android.internal.annotations.VisibleForTesting; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.R; +import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -74,17 +76,17 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section private final StatusBarStateController mStatusBarStateController; private final ConfigurationController mConfigurationController; private final PeopleHubViewAdapter mPeopleHubViewAdapter; - private final KeyguardMediaPlayer mKeyguardMediaPlayer; private final NotificationSectionsFeatureManager mSectionsFeatureManager; + private final KeyguardMediaController mKeyguardMediaController; private final int mNumberOfSections; - + private final NotificationSectionsLogger mLogger; private final PeopleHubViewBoundary mPeopleHubViewBoundary = new PeopleHubViewBoundary() { @Override public void setVisible(boolean isVisible) { if (mPeopleHubVisible != isVisible) { mPeopleHubVisible = isVisible; if (mInitialized) { - updateSectionBoundaries(); + updateSectionBoundaries("PeopleHub visibility changed"); } } } @@ -123,15 +125,18 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section StatusBarStateController statusBarStateController, ConfigurationController configurationController, PeopleHubViewAdapter peopleHubViewAdapter, - KeyguardMediaPlayer keyguardMediaPlayer, - NotificationSectionsFeatureManager sectionsFeatureManager) { + KeyguardMediaController keyguardMediaController, + NotificationSectionsFeatureManager sectionsFeatureManager, + NotificationSectionsLogger logger) { + mActivityStarter = activityStarter; mStatusBarStateController = statusBarStateController; mConfigurationController = configurationController; mPeopleHubViewAdapter = peopleHubViewAdapter; - mKeyguardMediaPlayer = keyguardMediaPlayer; mSectionsFeatureManager = sectionsFeatureManager; mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets(); + mKeyguardMediaController = keyguardMediaController; + mLogger = logger; } NotificationSection[] createSectionsForBuckets() { @@ -205,12 +210,9 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section mIncomingHeader.setHeaderText(R.string.notification_section_header_incoming); mIncomingHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - if (mMediaControlsView != null) { - mKeyguardMediaPlayer.unbindView(); - } mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater, R.layout.keyguard_media_header); - mKeyguardMediaPlayer.bindView(mMediaControlsView); + mKeyguardMediaController.attach(mMediaControlsView); } /** Listener for when the "clear all" button is clicked on the gentle notification header. */ @@ -250,15 +252,70 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section return null; } + private void logShadeContents() { + final int childCount = mParent.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mParent.getChildAt(i); + if (child == mIncomingHeader) { + mLogger.logPosition(i, "INCOMING HEADER"); + continue; + } + if (child == mMediaControlsView) { + mLogger.logPosition(i, "MEDIA CONTROLS"); + continue; + } + if (child == mPeopleHubView) { + mLogger.logPosition(i, "CONVERSATIONS HEADER"); + continue; + } + if (child == mAlertingHeader) { + mLogger.logPosition(i, "ALERTING HEADER"); + continue; + } + if (child == mGentleHeader) { + mLogger.logPosition(i, "SILENT HEADER"); + continue; + } + + if (!(child instanceof ExpandableNotificationRow)) { + mLogger.logPosition(i, "other:" + child.getClass().getName()); + continue; + } + ExpandableNotificationRow row = (ExpandableNotificationRow) child; + // Once we enter a new section, calculate the target position for the header. + switch (row.getEntry().getBucket()) { + case BUCKET_HEADS_UP: + mLogger.logPosition(i, "Heads Up"); + break; + case BUCKET_PEOPLE: + mLogger.logPosition(i, "Conversation"); + break; + case BUCKET_ALERTING: + mLogger.logPosition(i, "Alerting"); + break; + case BUCKET_SILENT: + mLogger.logPosition(i, "Silent"); + break; + } + } + } + + @VisibleForTesting + void updateSectionBoundaries() { + updateSectionBoundaries("test"); + } + /** * Should be called whenever notifs are added, removed, or updated. Updates section boundary * bookkeeping and adds/moves/removes section headers if appropriate. */ - void updateSectionBoundaries() { + void updateSectionBoundaries(String reason) { if (!isUsingMultipleSections()) { return; } + mLogger.logStartSectionUpdate(reason); + // The overall strategy here is to iterate over the current children of mParent, looking // for where the sections headers are currently positioned, and where each section begins. // Then, once we find the start of a new section, we track that position as the "target" for @@ -267,7 +324,6 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); - final boolean isKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; final boolean usingMediaControls = mSectionsFeatureManager.isMediaControlsEnabled(); boolean peopleNotifsPresent = false; @@ -275,7 +331,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section int currentMediaControlsIdx = -1; // Currently, just putting media controls in the front and incrementing the position based // on the number of heads-up notifs. - int mediaControlsTarget = isKeyguard && usingMediaControls ? 0 : -1; + int mediaControlsTarget = usingMediaControls ? 0 : -1; int currentIncomingHeaderIdx = -1; int incomingHeaderTarget = -1; int currentPeopleHeaderIdx = -1; @@ -293,27 +349,33 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section // Track the existing positions of the headers if (child == mIncomingHeader) { + mLogger.logPosition(i, "INCOMING HEADER"); currentIncomingHeaderIdx = i; continue; } if (child == mMediaControlsView) { + mLogger.logPosition(i, "MEDIA CONTROLS"); currentMediaControlsIdx = i; continue; } if (child == mPeopleHubView) { + mLogger.logPosition(i, "CONVERSATIONS HEADER"); currentPeopleHeaderIdx = i; continue; } if (child == mAlertingHeader) { + mLogger.logPosition(i, "ALERTING HEADER"); currentAlertingHeaderIdx = i; continue; } if (child == mGentleHeader) { + mLogger.logPosition(i, "SILENT HEADER"); currentGentleHeaderIdx = i; continue; } if (!(child instanceof ExpandableNotificationRow)) { + mLogger.logPosition(i, "other"); continue; } lastNotifIndex = i; @@ -321,6 +383,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section // Once we enter a new section, calculate the target position for the header. switch (row.getEntry().getBucket()) { case BUCKET_HEADS_UP: + mLogger.logPosition(i, "Heads Up"); if (showHeaders && incomingHeaderTarget == -1) { incomingHeaderTarget = i; // Offset the target if there are other headers before this that will be @@ -346,6 +409,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } break; case BUCKET_PEOPLE: + mLogger.logPosition(i, "Conversation"); peopleNotifsPresent = true; if (showHeaders && peopleHeaderTarget == -1) { peopleHeaderTarget = i; @@ -363,6 +427,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } break; case BUCKET_ALERTING: + mLogger.logPosition(i, "Alerting"); if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) { alertingHeaderTarget = i; // Offset the target if there are other headers before this that will be @@ -376,6 +441,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } break; case BUCKET_SILENT: + mLogger.logPosition(i, "Silent"); if (showHeaders && gentleHeaderTarget == -1) { gentleHeaderTarget = i; // Offset the target if there are other headers before this that will be @@ -406,6 +472,14 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } } + mLogger.logStr("New header target positions:"); + + mLogger.logPosition(incomingHeaderTarget, "INCOMING HEADER"); + mLogger.logPosition(mediaControlsTarget, "MEDIA CONTROLS"); + mLogger.logPosition(peopleHeaderTarget, "CONVERSATIONS HEADER"); + mLogger.logPosition(alertingHeaderTarget, "ALERTING HEADER"); + mLogger.logPosition(gentleHeaderTarget, "SILENT HEADER"); + // Add headers in reverse order to preserve indices adjustHeaderVisibilityAndPosition( gentleHeaderTarget, mGentleHeader, currentGentleHeaderIdx); @@ -416,6 +490,13 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section adjustViewPosition(mediaControlsTarget, mMediaControlsView, currentMediaControlsIdx); adjustViewPosition(incomingHeaderTarget, mIncomingHeader, currentIncomingHeaderIdx); + + mLogger.logStr("Final order:"); + + logShadeContents(); + + mLogger.logStr("Section boundary update complete"); + // Update headers to reflect state of section contents mGentleHeader.setAreThereDismissableGentleNotifs( mParent.hasActiveClearableNotifications(ROWS_GENTLE)); @@ -588,7 +669,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section void hidePeopleRow() { mPeopleHubVisible = false; - updateSectionBoundaries(); + updateSectionBoundaries("PeopleHub dismissed"); } void setHeaderForegroundColor(@ColorInt int color) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7f32c004808a..1ccc2bde2288 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -29,7 +29,6 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.app.TaskStackBuilder; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeAnimator; @@ -51,7 +50,6 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.os.AsyncTask; import android.os.Bundle; import android.os.ServiceManager; import android.os.UserHandle; @@ -3074,6 +3072,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd */ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private boolean generateRemoveAnimation(ExpandableView child) { + if (!child.wantsAddAndRemoveAnimations()) { + return false; + } if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { mAddedHeadsUpChildren.remove(child); return false; @@ -3428,7 +3429,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @Override @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) { - if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) { + if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden() + && child.wantsAddAndRemoveAnimations()) { // Generate Animations mChildrenToAddAnimated.add(child); if (fromMoreCard) { @@ -5841,7 +5843,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd // Let's update the footer once the notifications have been updated (in the next frame) post(() -> { updateFooter(); - updateSectionBoundaries(); + updateSectionBoundaries("dynamic privacy changed"); }); } @@ -5922,8 +5924,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd /** Updates the indices of the boundaries between sections. */ @ShadeViewRefactor(RefactorComponent.INPUT) - public void updateSectionBoundaries() { - mSectionsManager.updateSectionBoundaries(); + public void updateSectionBoundaries(String reason) { + mSectionsManager.updateSectionBoundaries(reason); } private void updateContinuousBackgroundDrawing() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java index 84dd48b6eb6b..80785db6df3e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.phone; import android.annotation.Nullable; -import android.app.NotificationChannel; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; @@ -28,6 +27,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; @@ -42,6 +42,8 @@ import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; +import dagger.Lazy; + /** * A class to handle notifications and their corresponding groups. */ @@ -51,6 +53,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State private static final String TAG = "NotificationGroupManager"; private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>(); private final ArraySet<OnGroupChangeListener> mListeners = new ArraySet<>(); + private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier; private int mBarState = -1; private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; @@ -58,8 +61,11 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State @Nullable private BubbleController mBubbleController = null; @Inject - public NotificationGroupManager(StatusBarStateController statusBarStateController) { + public NotificationGroupManager( + StatusBarStateController statusBarStateController, + Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier) { statusBarStateController.addCallback(this); + mPeopleNotificationIdentifier = peopleNotificationIdentifier; } private BubbleController getBubbleController() { @@ -536,8 +542,9 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) { return false; } - NotificationChannel channel = entry.getChannel(); - if (channel != null && channel.isImportantConversation()) { + int peopleNotificationType = mPeopleNotificationIdentifier.get().getPeopleNotificationType( + entry.getSbn(), entry.getRanking()); + if (peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON) { return true; } if (mHeadsUpManager != null && !mHeadsUpManager.isAlerting(entry.getKey())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index c9716d39590e..35c33aec8d0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -69,6 +69,7 @@ import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -242,6 +243,7 @@ public class NotificationPanelViewController extends PanelViewController { private final KeyguardBypassController mKeyguardBypassController; private final KeyguardUpdateMonitor mUpdateMonitor; private final ConversationNotificationManager mConversationNotificationManager; + private final MediaHierarchyManager mMediaHierarchyManager; private KeyguardAffordanceHelper mAffordanceHelper; private KeyguardUserSwitcher mKeyguardUserSwitcher; @@ -456,7 +458,8 @@ public class NotificationPanelViewController extends PanelViewController { ConfigurationController configurationController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, StatusBarTouchableRegionManager statusBarTouchableRegionManager, - ConversationNotificationManager conversationNotificationManager) { + ConversationNotificationManager conversationNotificationManager, + MediaHierarchyManager mediaHierarchyManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, latencyTracker, flingAnimationUtilsBuilder, statusBarTouchableRegionManager); @@ -466,6 +469,7 @@ public class NotificationPanelViewController extends PanelViewController { mZenModeController = zenModeController; mConfigurationController = configurationController; mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; + mMediaHierarchyManager = mediaHierarchyManager; mView.setWillNotDraw(!DEBUG); mInjectionInflationController = injectionInflationController; mFalsingManager = falsingManager; @@ -1609,7 +1613,7 @@ public class NotificationPanelViewController extends PanelViewController { if (mQs == null) return; float qsExpansionFraction = getQsExpansionFraction(); mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation()); - int heightDiff = mQs.getDesiredHeight() - mQs.getQsMinExpansionHeight(); + mMediaHierarchyManager.setQsExpansion(qsExpansionFraction); mNotificationStackScroller.setQsExpansionFraction(qsExpansionFraction); } @@ -2880,8 +2884,8 @@ public class NotificationPanelViewController extends PanelViewController { return mNotificationStackScroller.createDelegate(); } - public void updateNotificationViews() { - mNotificationStackScroller.updateSectionBoundaries(); + void updateNotificationViews(String reason) { + mNotificationStackScroller.updateSectionBoundaries(reason); mNotificationStackScroller.updateSpeedBumpIndex(); mNotificationStackScroller.updateFooter(); updateShowEmptyShadeView(); @@ -3514,7 +3518,11 @@ public class NotificationPanelViewController extends PanelViewController { // Calculate quick setting heights. int oldMaxHeight = mQsMaxExpansionHeight; if (mQs != null) { + float previousMin = mQsMinExpansionHeight; mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight(); + if (mQsExpansionHeight == previousMin) { + mQsExpansionHeight = mQsMinExpansionHeight; + } mQsMaxExpansionHeight = mQs.getDesiredHeight(); mNotificationStackScroller.setMaxTopPadding( mQsMaxExpansionHeight + mQsNotificationTopPadding); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index aecbb9097c7a..84da35b63d0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -296,20 +296,20 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, } @Override - public void updateNotificationViews() { + public void updateNotificationViews(final String reason) { // The function updateRowStates depends on both of these being non-null, so check them here. // We may be called before they are set from DeviceProvisionedController's callback. if (mScrimController == null) return; // Do not modify the notifications during collapse. if (isCollapsing()) { - mShadeController.addPostCollapseAction(this::updateNotificationViews); + mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason)); return; } mViewHierarchyManager.updateNotificationViews(); - mNotificationPanel.updateNotificationViews(); + mNotificationPanel.updateNotificationViews(reason); } public void onNotificationRemoved(String key, StatusBarNotification old) { @@ -347,7 +347,7 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, updateNotificationOnUiModeChanged(); mDispatchUiModeChangeOnUserSwitched = false; } - updateNotificationViews(); + updateNotificationViews("user switched"); mMediaManager.clearCurrentMediaNotification(); mStatusBar.setLockscreenUser(newUserId); updateMediaMetaData(true, false); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java index f8da03a49b50..df3748a8606b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java @@ -298,6 +298,7 @@ public class KeyguardUserSwitcher { convertView.setAlpha( item.isCurrent || item.isSwitchToEnabled ? USER_SWITCH_ENABLED_ALPHA : USER_SWITCH_DISABLED_ALPHA); + convertView.setEnabled(item.isSwitchToEnabled); return convertView; } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt new file mode 100644 index 000000000000..2be698b4e796 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt @@ -0,0 +1,87 @@ +/* + * 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.util.animation + +/** + * A class responsible for caching view Measurements which guarantees that we always obtain a value + */ +class GuaranteedMeasurementCache constructor( + private val baseCache : MeasurementCache, + private val inputMapper: (MeasurementInput) -> MeasurementInput, + private val measurementProvider: (MeasurementInput) -> MeasurementOutput? +) : MeasurementCache { + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val mappedInput = inputMapper.invoke(input) + if (!baseCache.contains(mappedInput)) { + var measurement = measurementProvider.invoke(mappedInput) + if (measurement != null) { + // Only cache measurings that actually have a size + baseCache.putMeasurement(mappedInput, measurement) + } else { + measurement = MeasurementOutput(0, 0) + } + return measurement + } else { + return baseCache.obtainMeasurement(mappedInput) + } + } + + override fun contains(input: MeasurementInput): Boolean { + return baseCache.contains(inputMapper.invoke(input)) + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + if (output.measuredWidth == 0 || output.measuredHeight == 0) { + // Only cache measurings that actually have a size + return; + } + val remappedInput = inputMapper.invoke(input) + baseCache.putMeasurement(remappedInput, output) + } +} + +/** + * A base implementation class responsible for caching view Measurements + */ +class BaseMeasurementCache : MeasurementCache { + private val dataCache: MutableMap<MeasurementInput, MeasurementOutput> = mutableMapOf() + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val measurementOutput = dataCache[input] + if (measurementOutput == null) { + return MeasurementOutput(0, 0) + } else { + return measurementOutput + } + } + + override fun contains(input: MeasurementInput) : Boolean { + return dataCache[input] != null + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + dataCache[input] = output + } +} + +interface MeasurementCache { + fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput + fun contains(input: MeasurementInput) : Boolean + fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) +} + diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt new file mode 100644 index 000000000000..bf94c5d36ff7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -0,0 +1,108 @@ +/* + * 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.util.animation + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.widget.FrameLayout + +/** + * A special view that is designed to host a single "unique object". The unique object is + * dynamically added and removed from this view and may transition to other UniqueObjectHostViews + * available in the system. + * This is useful to share a singular instance of a view that can transition between completely + * independent parts of the view hierarchy. + * If the view currently hosts the unique object, it's measuring it normally, + * but if it's not attached, it will obtain the size by requesting a measure, as if it were + * always attached. + */ +class UniqueObjectHostView( + context: Context +) : FrameLayout(context) { + lateinit var measurementCache : GuaranteedMeasurementCache + var onMeasureListener: ((MeasurementInput) -> Unit)? = null + + @SuppressLint("DrawAllocation") + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val paddingHorizontal = paddingStart + paddingEnd + val paddingVertical = paddingTop + paddingBottom + val width = MeasureSpec.getSize(widthMeasureSpec) - paddingHorizontal + val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) + val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical + val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) + val measurementInput = MeasurementInputData(widthSpec, heightSpec) + onMeasureListener?.apply { + invoke(measurementInput) + } + if (!isCurrentHost()) { + // We're not currently the host, let's get the dimension from our cache (this might + // perform a measuring if the cache doesn't have it yet) + // The goal here is that the view will always have a consistent measuring, regardless + // if it's attached or not. + // The behavior is therefore very similar to the view being persistently attached to + // this host, which can prevent flickers. It also makes sure that we always know + // the size of the view during transitions even if it has never been attached here + // before. + val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) + setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + // Let's update our cache + val child = getChildAt(0)!! + val output = MeasurementOutput(child.measuredWidth, child.measuredHeight) + measurementCache.putMeasurement(measurementInput, output) + } + } + + private fun isCurrentHost() = childCount != 0 +} + +/** + * A basic view measurement input + */ +interface MeasurementInput { + fun sameAs(input: MeasurementInput?): Boolean { + return equals(input) + } + val width : Int + get() { + return View.MeasureSpec.getSize(widthMeasureSpec) + } + val height : Int + get() { + return View.MeasureSpec.getSize(heightMeasureSpec) + } + var widthMeasureSpec: Int + var heightMeasureSpec: Int +} + +/** + * The output of a view measurement + */ +data class MeasurementOutput( + val measuredWidth: Int, + val measuredHeight: Int +) + +/** + * The data object holding a basic view measurement input + */ +data class MeasurementInputData( + override var widthMeasureSpec: Int, + override var heightMeasureSpec: Int +) : MeasurementInput diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt deleted file mode 100644 index 4bcf917fa95d..000000000000 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.keyguard - -import android.app.Notification -import android.graphics.drawable.Icon -import android.media.MediaMetadata -import android.media.session.MediaController -import android.media.session.MediaSession -import android.media.session.PlaybackState -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.View -import android.widget.TextView -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.arch.core.executor.TaskExecutor -import androidx.test.filters.SmallTest - -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.media.MediaControllerFactory -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat - -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -public class KeyguardMediaPlayerTest : SysuiTestCase() { - - private lateinit var keyguardMediaPlayer: KeyguardMediaPlayer - @Mock private lateinit var mockMediaFactory: MediaControllerFactory - @Mock private lateinit var mockMediaController: MediaController - private lateinit var playbackState: PlaybackState - private lateinit var fakeExecutor: FakeExecutor - private lateinit var mediaMetadata: MediaMetadata.Builder - private lateinit var entry: NotificationEntry - @Mock private lateinit var mockView: View - private lateinit var songView: TextView - private lateinit var artistView: TextView - @Mock private lateinit var mockIcon: Icon - - private val taskExecutor: TaskExecutor = object : TaskExecutor() { - public override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - public override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - public override fun isMainThread(): Boolean { - return true - } - } - - @Before - public fun setup() { - playbackState = PlaybackState.Builder().run { - build() - } - mockMediaController = mock(MediaController::class.java) - whenever(mockMediaController.getPlaybackState()).thenReturn(playbackState) - mockMediaFactory = mock(MediaControllerFactory::class.java) - whenever(mockMediaFactory.create(any())).thenReturn(mockMediaController) - - fakeExecutor = FakeExecutor(FakeSystemClock()) - keyguardMediaPlayer = KeyguardMediaPlayer(context, mockMediaFactory, fakeExecutor) - mockIcon = mock(Icon::class.java) - - mockView = mock(View::class.java) - songView = TextView(context) - artistView = TextView(context) - whenever<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(songView) - whenever<TextView>(mockView.findViewById(R.id.header_artist)).thenReturn(artistView) - - mediaMetadata = MediaMetadata.Builder() - entry = NotificationEntryBuilder().build() - entry.getSbn().getNotification().extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token(1, null)) - - ArchTaskExecutor.getInstance().setDelegate(taskExecutor) - - keyguardMediaPlayer.bindView(mockView) - } - - @After - public fun tearDown() { - keyguardMediaPlayer.unbindView() - ArchTaskExecutor.getInstance().setDelegate(null) - } - - @Test - public fun testBind() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUnboundClearControls() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.clearControls() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUpdateControls() { - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.VISIBLE) - } - - @Test - public fun testClearControls() { - keyguardMediaPlayer.clearControls() - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testUpdateControlsNullPlaybackState() { - // GIVEN that the playback state is null (ie. the media session was destroyed) - whenever(mockMediaController.getPlaybackState()).thenReturn(null) - // WHEN updated - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - // THEN the controls are cleared (ie. visibility is set to GONE) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testSongName() { - val song: String = "Song" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(songView.getText()).isEqualTo(song) - } - - @Test - public fun testArtistName() { - val artist: String = "Artist" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(artistView.getText()).isEqualTo(artist) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java index 9629079aeb4a..eb43b8172c16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java @@ -34,6 +34,8 @@ import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import com.android.systemui.SysuiTestCase; + import org.junit.Rule; import org.junit.Test; @@ -41,7 +43,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; @LargeTest -public class GlobalActionsImeTest { +public class GlobalActionsImeTest extends SysuiTestCase { @Rule public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java index 92c1d7601106..f70fb4f55a8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java @@ -131,7 +131,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { MediaMetadata metadata = mock(MediaMetadata.class); when(metadata.getText(any())).thenReturn("metadata"); mProvider.onDozingChanged(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -144,7 +144,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { when(metadata.getText(any())).thenReturn("metadata"); when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true); when(mDozeParameters.getAlwaysOn()).thenReturn(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -210,7 +210,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { mProvider.onStateChanged(StatusBarState.KEYGUARD); mProvider.onDozingChanged(true); reset(mContentResolver); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null)); // Hides after waking up @@ -222,7 +223,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void onDozingChanged_updatesSliceIfMedia() { mProvider.onStateChanged(StatusBarState.KEYGUARD); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); reset(mContentResolver); // Show media when dozing mProvider.onDozingChanged(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index 128d6e5612f1..6c543c73456c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -43,6 +43,7 @@ import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizer; @@ -50,7 +51,6 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.policy.SecurityController; -import com.android.systemui.util.concurrency.DelayableExecutor; import org.junit.Before; import org.junit.Test; @@ -62,7 +62,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; -import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -90,9 +89,7 @@ public class QSPanelTest extends SysuiTestCase { @Mock private QSTileView mQSTileView; @Mock - private Executor mForegroundExecutor; - @Mock - private DelayableExecutor mBackgroundExecutor; + private MediaHost mMediaHost; @Mock private LocalBluetoothManager mLocalBluetoothManager; @Mock @@ -116,8 +113,7 @@ public class QSPanelTest extends SysuiTestCase { mTestableLooper.runWithLooper(() -> { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher, - mQSLogger, mForegroundExecutor, mBackgroundExecutor, - mLocalBluetoothManager, mActivityStarter, mEntryManager, mUiEventLogger); + mQSLogger, mMediaHost, mUiEventLogger); // Provides a parent with non-zero size for QSPanel mParentView = new FrameLayout(mContext); mParentView.addView(mQsPanel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index bb7f73a3a959..d583048fbb26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -243,7 +243,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // Ensure that update callbacks happen in correct order InOrder order = inOrder(mEntryListener, mPresenter, mEntryListener); order.verify(mEntryListener).onPreEntryUpdated(mEntry); - order.verify(mPresenter).updateNotificationViews(); + order.verify(mPresenter).updateNotificationViews(any()); order.verify(mEntryListener).onPostEntryUpdated(mEntry); } @@ -254,7 +254,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, UNDEFINED_DISMISS_REASON); - verify(mPresenter).updateNotificationViews(); + verify(mPresenter).updateNotificationViews(any()); verify(mEntryListener).onEntryRemoved( eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); verify(mRow).setRemoved(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java index 277ac244cec5..595ba89ca3b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -93,7 +94,9 @@ public class NotificationFilterTest extends SysuiTestCase { .thenReturn(PackageManager.PERMISSION_GRANTED); mDependency.injectTestDependency(ForegroundServiceController.class, mFsc); mDependency.injectTestDependency(NotificationGroupManager.class, - new NotificationGroupManager(mock(StatusBarStateController.class))); + new NotificationGroupManager( + mock(StatusBarStateController.class), + () -> mock(PeopleNotificationIdentifier.class))); mDependency.injectMockDependency(ShadeController.class); mDependency.injectMockDependency(NotificationLockscreenUserManager.class); mDependency.injectTestDependency(KeyguardEnvironment.class, mEnvironment); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java index 2894abb8f364..7dfead7575a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -335,7 +335,7 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); // THEN we update the presenter - verify(mPresenter).updateNotificationViews(); + verify(mPresenter).updateNotificationViews(any()); } @Test @@ -364,7 +364,7 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { verify(mEntryListener).onEntryReinflated(entry); // THEN we update the presenter - verify(mPresenter).updateNotificationViews(); + verify(mPresenter).updateNotificationViews(any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 07f2085a1b76..b9eb4d1e29c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -115,7 +115,9 @@ public class NotificationTestHelper { dependency.injectMockDependency(BubbleController.class); dependency.injectMockDependency(NotificationShadeWindowController.class); mStatusBarStateController = mock(StatusBarStateController.class); - mGroupManager = new NotificationGroupManager(mStatusBarStateController); + mGroupManager = new NotificationGroupManager( + mStatusBarStateController, + () -> mock(PeopleNotificationIdentifier.class)); mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarStateController, mock(KeyguardBypassController.class), mock(NotificationGroupManager.class), mock(ConfigurationControllerImpl.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 646bc9699ff8..0b86a78a1c5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -42,9 +42,9 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; @@ -74,10 +74,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; @Mock private PeopleHubViewAdapter mPeopleHubAdapter; - @Mock private KeyguardMediaPlayer mKeyguardMediaPlayer; + @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager; @Mock private NotificationRowComponent mNotificationRowComponent; @Mock private ActivatableNotificationViewController mActivatableNotificationViewController; + @Mock private NotificationSectionsLogger mLogger; private NotificationSectionsManager mSectionsManager; @@ -93,8 +94,9 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mStatusBarStateController, mConfigurationController, mPeopleHubAdapter, - mKeyguardMediaPlayer, - mSectionsFeatureManager + mKeyguardMediaController, + mSectionsFeatureManager, + mLogger ); // Required in order for the header inflation to work properly when(mNssl.generateLayoutParams(any(AttributeSet.class))) @@ -367,38 +369,6 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 1); } - @Test - public void testMediaControls_RemoveWhenExitKeyguard() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we leave the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - - @Test - public void testMediaControls_RemoveWhenPullDownShade() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we pull down the shade on the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - private void enablePeopleFiltering() { when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true); when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java index 67f941301e5f..885dff39f7b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java @@ -42,6 +42,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback; import com.android.systemui.statusbar.notification.row.RowContentBindParams; import com.android.systemui.statusbar.notification.row.RowContentBindStage; @@ -87,7 +88,9 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { when(mNotificationEntryManager.getPendingNotificationsIterator()) .thenReturn(mPendingEntries.values()); - mGroupManager = new NotificationGroupManager(mock(StatusBarStateController.class)); + mGroupManager = new NotificationGroupManager( + mock(StatusBarStateController.class), + () -> mock(PeopleNotificationIdentifier.class)); mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager); mGroupManager.setHeadsUpManager(mHeadsUpManager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java index 19ce1ea218c9..5a6f74a4c6aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java @@ -33,6 +33,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; @@ -63,7 +64,9 @@ public class NotificationGroupManagerTest extends SysuiTestCase { } private void initializeGroupManager() { - mGroupManager = new NotificationGroupManager(mock(StatusBarStateController.class)); + mGroupManager = new NotificationGroupManager( + mock(StatusBarStateController.class), + () -> mock(PeopleNotificationIdentifier.class)); mGroupManager.setHeadsUpManager(mHeadsUpManager); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index 57ef05544e7e..b5663d5dd19e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -55,6 +55,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.doze.DozeLog; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -172,6 +173,8 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private ConfigurationController mConfigurationController; @Mock + private MediaHierarchyManager mMediaHiearchyManager; + @Mock private ConversationNotificationManager mConversationNotificationManager; private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; @@ -228,7 +231,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor, mMetricsLogger, mActivityManager, mZenModeController, mConfigurationController, mFlingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, - mConversationNotificationManager); + mConversationNotificationManager, mMediaHiearchyManager); mNotificationPanelViewController.initDependencies(mStatusBar, mGroupManager, mNotificationShelf, mNotificationAreaController, mScrimController); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 086a8be6d6cf..ef17d1331a1e 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -19,6 +19,7 @@ package com.android.server.autofill; import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; +import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; import static android.view.autofill.AutofillManager.ACTION_RESPONSE_EXPIRED; import static android.view.autofill.AutofillManager.ACTION_START_SESSION; @@ -653,6 +654,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return mService.isInlineSuggestionsEnabled(); } + private boolean isViewFocusedLocked(int flags) { + return (flags & FLAG_VIEW_NOT_FOCUSED) == 0; + } + /** * Clears the existing response for the partition, reads a new structure, and then requests a * new fill response from the fill service. @@ -711,10 +716,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState cancelCurrentRequestLocked(); // Only ask IME to create inline suggestions request if Autofill provider supports it and - // the render service is available. + // the render service is available except the autofill is triggered manually and the view + // is also not focused. final RemoteInlineSuggestionRenderService remoteRenderService = mService.getRemoteInlineSuggestionRenderServiceLocked(); - if (isInlineSuggestionsEnabledByAutofillProviderLocked() && remoteRenderService != null) { + if (isInlineSuggestionsEnabledByAutofillProviderLocked() + && remoteRenderService != null + && isViewFocusedLocked(flags)) { Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer = mAssistReceiver.newAutofillRequestLocked(viewState, /*isInlineRequest=*/ true); @@ -3139,9 +3147,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } }; - // When the inline suggestion render service is available, there are 2 cases when - // augmented autofill should ask IME for inline suggestion request, because standard - // autofill flow didn't: + // When the inline suggestion render service is available and the view is focused, there + // are 2 cases when augmented autofill should ask IME for inline suggestion request, + // because standard autofill flow didn't: // 1. the field is augmented autofill only (when standard autofill provider is None or // when it returns null response) // 2. standard autofill provider doesn't support inline suggestion @@ -3149,7 +3157,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mService.getRemoteInlineSuggestionRenderServiceLocked(); if (remoteRenderService != null && (mForAugmentedAutofillOnly - || !isInlineSuggestionsEnabledByAutofillProviderLocked())) { + || !isInlineSuggestionsEnabledByAutofillProviderLocked()) + && isViewFocusedLocked(flags)) { if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill"); remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( (extras) -> { diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java index 905c489e1dcb..6402e07bddc3 100644 --- a/services/core/java/com/android/server/IpSecService.java +++ b/services/core/java/com/android/server/IpSecService.java @@ -1776,7 +1776,7 @@ public class IpSecService extends IIpSecService.Stub { socketRecord = userRecord.mEncapSocketRecords.getResourceOrThrow(c.getEncapSocketResourceId()); } - SpiRecord spiRecord = userRecord.mSpiRecords.getResourceOrThrow(c.getSpiResourceId()); + SpiRecord spiRecord = transformInfo.getSpiRecord(); int mark = (direction == IpSecManager.DIRECTION_OUT) @@ -1809,7 +1809,7 @@ public class IpSecService extends IIpSecService.Stub { // Set outbound SPI only. We want inbound to use any valid SA (old, new) on rekeys, // but want to guarantee outbound packets are sent over the new SA. - spi = transformInfo.getSpiRecord().getSpi(); + spi = spiRecord.getSpi(); } // Always update the policy with the relevant XFRM_IF_ID diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index be539456ae7c..43ed8538fb0c 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1941,10 +1941,13 @@ class StorageManagerService extends IStorageManager.Stub mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid); } - try { - mIAppOpsService.startWatchingMode(OP_REQUEST_INSTALL_PACKAGES, null, mAppOpsCallback); - mIAppOpsService.startWatchingMode(OP_LEGACY_STORAGE, null, mAppOpsCallback); - } catch (RemoteException e) { + if (!mIsFuseEnabled) { + try { + mIAppOpsService.startWatchingMode(OP_REQUEST_INSTALL_PACKAGES, null, + mAppOpsCallback); + mIAppOpsService.startWatchingMode(OP_LEGACY_STORAGE, null, mAppOpsCallback); + } catch (RemoteException e) { + } } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 63e01e034d7e..5ebfb0069931 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -2582,6 +2582,28 @@ public class AppOpsService extends IAppOpsService.Stub { } } + private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports, + int op, int uid, String packageName) { + boolean duplicate = false; + if (reports == null) { + reports = new ArrayList<>(); + } else { + final int reportCount = reports.size(); + for (int j = 0; j < reportCount; j++) { + ChangeRec report = reports.get(j); + if (report.op == op && report.pkg.equals(packageName)) { + duplicate = true; + break; + } + } + } + if (!duplicate) { + reports.add(new ChangeRec(op, uid, packageName)); + } + + return reports; + } + private static HashMap<ModeCallback, ArrayList<ChangeRec>> addCallbacks( HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks, int op, int uid, String packageName, ArraySet<ModeCallback> cbs) { @@ -2595,22 +2617,9 @@ public class AppOpsService extends IAppOpsService.Stub { for (int i=0; i<N; i++) { ModeCallback cb = cbs.valueAt(i); ArrayList<ChangeRec> reports = callbacks.get(cb); - boolean duplicate = false; - if (reports == null) { - reports = new ArrayList<>(); - callbacks.put(cb, reports); - } else { - final int reportCount = reports.size(); - for (int j = 0; j < reportCount; j++) { - ChangeRec report = reports.get(j); - if (report.op == op && report.pkg.equals(packageName)) { - duplicate = true; - break; - } - } - } - if (!duplicate) { - reports.add(new ChangeRec(op, uid, packageName)); + ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName); + if (changed != reports) { + callbacks.put(cb, changed); } } return callbacks; @@ -2648,6 +2657,7 @@ public class AppOpsService extends IAppOpsService.Stub { enforceManageAppOpsModes(callingPid, callingUid, reqUid); HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks = null; + ArrayList<ChangeRec> allChanges = new ArrayList<>(); synchronized (this) { boolean changed = false; for (int i = mUidStates.size() - 1; i >= 0; i--) { @@ -2668,6 +2678,9 @@ public class AppOpsService extends IAppOpsService.Stub { mOpModeWatchers.get(code)); callbacks = addCallbacks(callbacks, code, uidState.uid, packageName, mPackageModeWatchers.get(packageName)); + + allChanges = addChange(allChanges, code, uidState.uid, + packageName); } } } @@ -2707,6 +2720,7 @@ public class AppOpsService extends IAppOpsService.Stub { callbacks = addCallbacks(callbacks, curOp.op, uid, packageName, mPackageModeWatchers.get(packageName)); + allChanges = addChange(allChanges, curOp.op, uid, packageName); curOp.removeAttributionsWithNoTime(); if (curOp.mAttributions.isEmpty()) { pkgOps.removeAt(j); @@ -2741,6 +2755,15 @@ public class AppOpsService extends IAppOpsService.Stub { } } } + + if (allChanges != null) { + int numChanges = allChanges.size(); + for (int i = 0; i < numChanges; i++) { + ChangeRec change = allChanges.get(i); + notifyOpChangedSync(change.op, change.uid, change.pkg, + AppOpsManager.opToDefaultMode(change.op)); + } + } } private void evalAllForegroundOpsLocked() { diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 40b6f42309bd..befd6b1cec0e 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -211,7 +211,13 @@ import java.io.PrintWriter; } mForcedUseForComm = AudioSystem.FORCE_SPEAKER; } else if (mForcedUseForComm == AudioSystem.FORCE_SPEAKER) { - mForcedUseForComm = AudioSystem.FORCE_NONE; + if (mBtHelper.isBluetoothScoOn()) { + mForcedUseForComm = AudioSystem.FORCE_BT_SCO; + setForceUse_Async( + AudioSystem.FOR_RECORD, AudioSystem.FORCE_BT_SCO, eventSource); + } else { + mForcedUseForComm = AudioSystem.FORCE_NONE; + } } mForcedUseForCommExt = mForcedUseForComm; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 17baead84f9d..8068e378f2e3 100755 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7369,10 +7369,32 @@ public class AudioService extends IAudioService.Stub return false; } boolean suppress = false; - if (resolvedStream != AudioSystem.STREAM_MUSIC && mController != null) { + // Intended behavior: + // 1/ if the stream is not the default UI stream, do not suppress (as it is not involved + // in bringing up the UI) + // 2/ if the resolved and default stream is MUSIC, and media is playing, do not suppress + // 3/ otherwise suppress the first adjustments that occur during the "long press + // timeout" interval. Note this is true regardless of whether this is a "real long + // press" (where the user keeps pressing on the volume button), or repeated single + // presses (here we don't know if we are in a real long press, or repeated fast + // button presses). + // Once the long press timeout occurs (mNextLongPress reset to 0), do not suppress. + // Example: for a default and resolved stream of MUSIC, this allows modifying rapidly + // the volume when media is playing (whether by long press or repeated individual + // presses), or to bring up the volume UI when media is not playing, in order to make + // another change (e.g. switch ringer modes) without changing media volume. + if (resolvedStream == DEFAULT_VOL_STREAM_NO_PLAYBACK && mController != null) { + // never suppress media vol adjustement during media playback + if (resolvedStream == AudioSystem.STREAM_MUSIC + && AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, mLongPressTimeout)) + { + // media is playing, adjust the volume right away + return false; + } + final long now = SystemClock.uptimeMillis(); if ((flags & AudioManager.FLAG_SHOW_UI) != 0 && !mVisible) { - // ui will become visible + // UI is not visible yet, adjustment is ignored if (mNextLongPress < now) { mNextLongPress = now + mLongPressTimeout; } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 0654f86c6a67..9e7b428d2cca 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -307,8 +307,15 @@ public class BtHelper { case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: mDeviceBroker.setBluetoothScoOn(false, "BtHelper.receiveBtEvent"); scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; - // startBluetoothSco called after stopBluetoothSco - if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) { + // There are two cases where we want to immediately reconnect audio: + // 1) If a new start request was received while disconnecting: this was + // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ. + // 2) If audio was connected then disconnected via Bluetooth APIs and + // we still have pending activation requests by apps: this is indicated by + // state SCO_STATE_ACTIVE_EXTERNAL and the mScoClients list not empty. + if (mScoAudioState == SCO_STATE_ACTIVATE_REQ + || (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL + && !mScoClients.isEmpty())) { if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null && connectBluetoothScoAudioHelper(mBluetoothHeadset, mBluetoothHeadsetDevice, mScoAudioMode)) { @@ -318,7 +325,9 @@ public class BtHelper { } } // Tear down SCO if disconnected from external - clearAllScoClients(0, mScoAudioState == SCO_STATE_ACTIVE_INTERNAL); + if (mScoAudioState == SCO_STATE_DEACTIVATING) { + clearAllScoClients(0, false); + } mScoAudioState = SCO_STATE_INACTIVE; break; case BluetoothHeadset.STATE_AUDIO_CONNECTING: diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 53f9ebcbd8dd..1ed5cd824050 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -2207,13 +2207,7 @@ public class HdmiControlService extends SystemService { @Override public void setHdmiCecVolumeControlEnabled(final boolean isHdmiCecVolumeControlEnabled) { enforceAccessPermission(); - runOnServiceThread(new Runnable() { - @Override - public void run() { - HdmiControlService.this.setHdmiCecVolumeControlEnabled( - isHdmiCecVolumeControlEnabled); - } - }); + HdmiControlService.this.setHdmiCecVolumeControlEnabled(isHdmiCecVolumeControlEnabled); } @Override @@ -3014,7 +3008,6 @@ public class HdmiControlService extends SystemService { } void setHdmiCecVolumeControlEnabled(boolean isHdmiCecVolumeControlEnabled) { - assertRunOnServiceThread(); synchronized (mLock) { mHdmiCecVolumeControlEnabled = isHdmiCecVolumeControlEnabled; @@ -3030,7 +3023,6 @@ public class HdmiControlService extends SystemService { } boolean isHdmiCecVolumeControlEnabled() { - assertRunOnServiceThread(); synchronized (mLock) { return mHdmiCecVolumeControlEnabled; } diff --git a/services/core/java/com/android/server/lights/TEST_MAPPING b/services/core/java/com/android/server/lights/TEST_MAPPING new file mode 100644 index 000000000000..f868ea093500 --- /dev/null +++ b/services/core/java/com/android/server/lights/TEST_MAPPING @@ -0,0 +1,21 @@ +{ + "presubmit": [ + { + "name": "CtsHardwareTestCases", + "options": [ + {"include-filter": "com.android.hardware.lights"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.LargeTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "com.android.server.lights"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + } + ] +} diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 9e509f453921..1a749b34d85e 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -22,6 +22,7 @@ import android.app.AppOpsManager; import android.app.IProcessObserver; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; @@ -43,6 +44,7 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -399,11 +401,12 @@ public final class MediaProjectionManagerService extends SystemService public final UserHandle userHandle; private final int mTargetSdkVersion; private final boolean mIsPrivileged; + private final int mType; private IMediaProjectionCallback mCallback; private IBinder mToken; private IBinder.DeathRecipient mDeathEater; - private int mType; + private boolean mRestoreSystemAlertWindow; MediaProjection(int type, int uid, String packageName, int targetSdkVersion, boolean isPrivileged) { @@ -494,6 +497,35 @@ public final class MediaProjectionManagerService extends SystemService "MediaProjectionCallbacks must be valid, aborting MediaProjection", e); return; } + if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) { + final long token = Binder.clearCallingIdentity(); + try { + // We allow an app running a current screen capture session to use + // SYSTEM_ALERT_WINDOW for the duration of the session, to enable + // them to overlay their UX on top of what is being captured. + // We only do this if the app requests the permission, and the appop + // is in its default state (the user has neither explicitly allowed nor + // disallowed it). + final PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser( + packageName, PackageManager.GET_PERMISSIONS, + UserHandle.getUserId(uid)); + if (ArrayUtils.contains(packageInfo.requestedPermissions, + Manifest.permission.SYSTEM_ALERT_WINDOW)) { + final int currentMode = mAppOps.unsafeCheckOpRawNoThrow( + AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName); + if (currentMode == AppOpsManager.MODE_DEFAULT) { + mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, + packageName, AppOpsManager.MODE_ALLOWED); + mRestoreSystemAlertWindow = true; + } + } + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Package not found, aborting MediaProjection", e); + return; + } finally { + Binder.restoreCallingIdentity(token); + } + } startProjectionLocked(this); } } @@ -507,6 +539,24 @@ public final class MediaProjectionManagerService extends SystemService + "pid=" + Binder.getCallingPid() + ")"); return; } + if (mRestoreSystemAlertWindow) { + final long token = Binder.clearCallingIdentity(); + try { + // Put the appop back how it was, unless it has been changed from what + // we set it to. + // Note that WindowManager takes care of removing any existing overlay + // windows when we do this. + final int currentMode = mAppOps.unsafeCheckOpRawNoThrow( + AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName); + if (currentMode == AppOpsManager.MODE_ALLOWED) { + mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName, + AppOpsManager.MODE_DEFAULT); + } + mRestoreSystemAlertWindow = false; + } finally { + Binder.restoreCallingIdentity(token); + } + } stopProjectionLocked(this); mToken.unlinkToDeath(mDeathEater, 0); mToken = null; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 5b5f334803e5..236a6816b3e3 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -934,6 +934,21 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } @Override + public void uninstallExistingPackage(VersionedPackage versionedPackage, + String callerPackageName, IntentSender statusReceiver, int userId) { + final int callingUid = Binder.getCallingUid(); + mContext.enforceCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES, null); + mPermissionManager.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall"); + if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) { + mAppOps.checkPackage(callingUid, callerPackageName); + } + + final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, + statusReceiver, versionedPackage.getPackageName(), false, userId); + mPm.deleteExistingPackageAsUser(versionedPackage, adapter.getBinder(), userId); + } + + @Override public void installExistingPackage(String packageName, int installFlags, int installReason, IntentSender statusReceiver, int userId, List<String> whiteListedPermissions) { mPm.installExistingPackageAsUser(packageName, userId, installFlags, installReason, diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index bde9d5735960..3ead72ce018f 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -17911,8 +17911,46 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public void deleteExistingPackageAsUser(VersionedPackage versionedPackage, + final IPackageDeleteObserver2 observer, final int userId) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.DELETE_PACKAGES, null); + Preconditions.checkNotNull(versionedPackage); + Preconditions.checkNotNull(observer); + final String packageName = versionedPackage.getPackageName(); + final long versionCode = versionedPackage.getLongVersionCode(); + + int installedForUsersCount = 0; + synchronized (mLock) { + // Normalize package name to handle renamed packages and static libs + final String internalPkgName = resolveInternalPackageNameLPr(packageName, versionCode); + final PackageSetting ps = mSettings.getPackageLPr(internalPkgName); + if (ps != null) { + int[] installedUsers = ps.queryInstalledUsers(mUserManager.getUserIds(), true); + installedForUsersCount = installedUsers.length; + } + } + + if (installedForUsersCount > 1) { + deletePackageVersionedInternal(versionedPackage, observer, userId, 0, true); + } else { + try { + observer.onPackageDeleted(packageName, PackageManager.DELETE_FAILED_INTERNAL_ERROR, + null); + } catch (RemoteException re) { + } + } + } + + @Override public void deletePackageVersioned(VersionedPackage versionedPackage, final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) { + deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, false); + } + + private void deletePackageVersionedInternal(VersionedPackage versionedPackage, + final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags, + final boolean allowSilentUninstall) { final int callingUid = Binder.getCallingUid(); mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DELETE_PACKAGES, null); @@ -17933,6 +17971,7 @@ public class PackageManagerService extends IPackageManager.Stub final int uid = Binder.getCallingUid(); if (!isOrphaned(internalPackageName) + && !allowSilentUninstall && !isCallerAllowedToSilentlyUninstall(uid, internalPackageName)) { mHandler.post(() -> { try { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index ed74e897cfd0..131e44963033 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1286,12 +1286,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (stack != null && stack.topRunningActivity() == this) { - // carry over the PictureInPictureParams to the parent stack without calling - // TaskOrganizerController#dispatchTaskInfoChanged. - // this is to ensure the stack holding up-to-dated pinned stack information - // when activity is re-parented to enter pip mode, see also - // RootWindowContainer#moveActivityToPinnedStack - stack.mPictureInPictureParams.copyOnlySet(pictureInPictureArgs); // make ensure the TaskOrganizer still works after re-parenting if (firstWindowDrawn) { stack.setHasBeenVisible(true); @@ -7785,6 +7779,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void setPictureInPictureParams(PictureInPictureParams p) { pictureInPictureArgs.copyOnlySet(p); - getTask().getRootTask().setPictureInPictureParams(p); + getTask().getRootTask().onPictureInPictureParamsChanged(); } } diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index a84635de32cc..dca086034dd0 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -702,8 +702,10 @@ class ActivityStack extends Task { // Need to make sure windowing mode is supported. If we in the process of creating the stack // no need to resolve the windowing mode again as it is already resolved to the right mode. if (!creating) { - windowingMode = taskDisplayArea.validateWindowingMode(windowingMode, - null /* ActivityRecord */, topTask, getActivityType()); + if (!taskDisplayArea.isValidWindowingMode(windowingMode, null /* ActivityRecord */, + topTask, getActivityType())) { + windowingMode = WINDOWING_MODE_UNDEFINED; + } } if (taskDisplayArea.getRootSplitScreenPrimaryTask() == this && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index fb7ba62b5fd2..3e7e0c8b936d 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -842,7 +842,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { r.launchedFromPackage, task.voiceInteractor, proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(), results, newIntents, dc.isNextTransitionForward(), proc.createProfilerInfoIfNeeded(), - r.assistToken)); + r.assistToken, r.createFixedRotationAdjustmentsIfNeeded())); // Set desired final state. final ActivityLifecycleItem lifecycleItem; @@ -1440,6 +1440,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { mService.deferWindowLayout(); try { stack.setWindowingMode(WINDOWING_MODE_UNDEFINED); + stack.setBounds(null); if (toDisplay.getDisplayId() != stack.getDisplayId()) { stack.reparent(toDisplay.getDefaultTaskDisplayArea(), false /* onTop */); } else { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 317bb43adb98..d02be88ef0d4 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -60,7 +60,37 @@ class InsetsPolicy { private final IntArray mShowingTransientTypes = new IntArray(); /** For resetting visibilities of insets sources. */ - private final InsetsControlTarget mDummyControlTarget = new InsetsControlTarget() { }; + private final InsetsControlTarget mDummyControlTarget = new InsetsControlTarget() { + + @Override + public void notifyInsetsControlChanged() { + boolean hasLeash = false; + final InsetsSourceControl[] controls = + mStateController.getControlsForDispatch(this); + if (controls == null) { + return; + } + for (InsetsSourceControl control : controls) { + final @InternalInsetsType int type = control.getType(); + if (mShowingTransientTypes.indexOf(type) != -1) { + // The visibilities of transient bars will be handled with animations. + continue; + } + final SurfaceControl leash = control.getLeash(); + if (leash != null) { + hasLeash = true; + + // We use alpha to control the visibility here which aligns the logic at + // SurfaceAnimator.createAnimationLeash + mDisplayContent.getPendingTransaction().setAlpha( + leash, InsetsState.getDefaultVisibility(type) ? 1f : 0f); + } + } + if (hasLeash) { + mDisplayContent.scheduleAnimation(); + } + } + }; private WindowState mFocusedWin; private BarWindow mStatusBar = new BarWindow(StatusBarManager.WINDOW_STATUS_BAR); diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index 4cd31806f99d..513be7a6becc 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -146,7 +146,10 @@ class LaunchParamsController { if (mTmpParams.hasWindowingMode() && mTmpParams.mWindowingMode != task.getStack().getWindowingMode()) { - task.getStack().setWindowingMode(mTmpParams.mWindowingMode); + final int activityType = activity != null + ? activity.getActivityType() : task.getActivityType(); + task.getStack().setWindowingMode(task.getDisplayArea().validateWindowingMode( + mTmpParams.mWindowingMode, activity, task, activityType)); } if (mTmpParams.mBounds.isEmpty()) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 9a30f1c8e11d..c93b7354999b 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2170,7 +2170,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> final boolean singleActivity = task.getChildCount() == 1; final ActivityStack stack; if (singleActivity) { - stack = r.getRootTask(); + stack = (ActivityStack) task; } else { // In the case of multiple activities, we will create a new task for it and then // move the PIP activity into the task. @@ -2183,6 +2183,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // up-to-dated pinned stack information on this newly created stack. r.reparent(stack, MAX_VALUE, reason); } + if (stack.getParent() != taskDisplayArea) { + // stack is nested, but pinned tasks need to be direct children of their + // display area, so reparent. + stack.reparent(taskDisplayArea, true /* onTop */); + } stack.setWindowingMode(WINDOWING_MODE_PINNED); // Reset the state that indicates it can enter PiP while pausing after we've moved it diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 4845da192638..b9e65137665a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -107,7 +107,6 @@ import android.app.ActivityManager.TaskSnapshot; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.AppGlobals; -import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.ComponentName; @@ -488,12 +487,6 @@ class Task extends WindowContainer<WindowContainer> { boolean mTaskAppearedSent; /** - * Last Picture-in-Picture params applicable to the task. Updated when the app - * enters Picture-in-Picture or when setPictureInPictureParams is called. - */ - PictureInPictureParams mPictureInPictureParams = new PictureInPictureParams.Builder().build(); - - /** * This task was created by the task organizer which has the following implementations. * <ul> * <lis>The task won't be removed when it is empty. Removal has to be an explicit request @@ -3571,10 +3564,11 @@ class Task extends WindowContainer<WindowContainer> { info.resizeMode = top != null ? top.mResizeMode : mResizeMode; info.topActivityType = top.getActivityType(); - if (mPictureInPictureParams.empty()) { + ActivityRecord rootActivity = top.getRootActivity(); + if (rootActivity == null || rootActivity.pictureInPictureArgs.empty()) { info.pictureInPictureParams = null; } else { - info.pictureInPictureParams = mPictureInPictureParams; + info.pictureInPictureParams = rootActivity.pictureInPictureArgs; } info.topActivityInfo = mReuseActivitiesReport.top != null ? mReuseActivitiesReport.top.info @@ -4490,8 +4484,7 @@ class Task extends WindowContainer<WindowContainer> { updateShadowsRadius(hasFocus, getPendingTransaction()); } - void setPictureInPictureParams(PictureInPictureParams p) { - mPictureInPictureParams.copyOnlySet(p); + void onPictureInPictureParamsChanged() { if (isOrganized()) { mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(this, true /* force */); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 0a1ee2b79711..37a4c1f6849b 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; @@ -1333,16 +1332,16 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } /** - * Check that the requested windowing-mode is appropriate for the specified task and/or activity + * Check if the requested windowing-mode is appropriate for the specified task and/or activity * on this display. * * @param windowingMode The windowing-mode to validate. * @param r The {@link ActivityRecord} to check against. * @param task The {@link Task} to check against. * @param activityType An activity type. - * @return The provided windowingMode or the closest valid mode which is appropriate. + * @return {@code true} if windowingMode is valid, {@code false} otherwise. */ - int validateWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, + boolean isValidWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, int activityType) { // Make sure the windowing mode we are trying to use makes sense for what is supported. boolean supportsMultiWindow = mAtmService.mSupportsMultiWindow; @@ -1362,24 +1361,35 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } } + return windowingMode != WINDOWING_MODE_UNDEFINED + && isWindowingModeSupported(windowingMode, supportsMultiWindow, supportsSplitScreen, + supportsFreeform, supportsPip, activityType); + } + + /** + * Check that the requested windowing-mode is appropriate for the specified task and/or activity + * on this display. + * + * @param windowingMode The windowing-mode to validate. + * @param r The {@link ActivityRecord} to check against. + * @param task The {@link Task} to check against. + * @param activityType An activity type. + * @return The provided windowingMode or the closest valid mode which is appropriate. + */ + int validateWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, + int activityType) { final boolean inSplitScreenMode = isSplitScreenModeActivated(); - if (!inSplitScreenMode - && windowingMode == WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY) { + if (!inSplitScreenMode && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { // Switch to the display's windowing mode if we are not in split-screen mode and we are // trying to launch in split-screen secondary. windowingMode = WINDOWING_MODE_UNDEFINED; - } else if (inSplitScreenMode && (windowingMode == WINDOWING_MODE_FULLSCREEN - || windowingMode == WINDOWING_MODE_UNDEFINED) - && supportsSplitScreen) { + } else if (inSplitScreenMode && windowingMode == WINDOWING_MODE_UNDEFINED) { windowingMode = WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; } - - if (windowingMode != WINDOWING_MODE_UNDEFINED - && isWindowingModeSupported(windowingMode, supportsMultiWindow, supportsSplitScreen, - supportsFreeform, supportsPip, activityType)) { - return windowingMode; + if (!isValidWindowingMode(windowingMode, r, task, activityType)) { + return WINDOWING_MODE_UNDEFINED; } - return WINDOWING_MODE_UNDEFINED; + return windowingMode; } boolean isTopStack(ActivityStack stack) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 13a0b2c536e7..4c1d6f3b9892 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2259,7 +2259,7 @@ public class WindowManagerService extends IWindowManager.Stub win.mRelayoutCalled = true; win.mInRelayout = true; - win.mViewVisibility = viewVisibility; + win.setViewVisibility(viewVisibility); ProtoLog.i(WM_DEBUG_SCREEN_ON, "Relayout %s: oldVis=%d newVis=%d. %s", win, oldVisibility, viewVisibility, new RuntimeException().fillInStackTrace()); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 49d6889b95f9..e925ce5c2dac 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5689,6 +5689,17 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mSession.mPid == pid && isNonToastOrStarting() && isVisibleNow(); } + void setViewVisibility(int viewVisibility) { + mViewVisibility = viewVisibility; + // The viewVisibility is set to GONE with a client request to relayout. If this occurs and + // there's a blast sync transaction waiting, finishDrawing will never be called since the + // client will not render when visibility is GONE. Therefore, call finishDrawing here to + // prevent system server from blocking on a window that will not draw. + if (viewVisibility == View.GONE && mUsingBLASTSyncTransaction) { + finishDrawing(null); + } + } + SurfaceControl getClientViewRootSurface() { return mWinAnimator.getClientViewRootSurface(); } diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index cf585df87f24..768f89eff774 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.os.Process.INVALID_UID; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; @@ -40,6 +41,7 @@ import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER; import android.annotation.CallSuper; import android.app.IWindowToken; +import android.app.servertransaction.FixedRotationAdjustmentsItem; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Debug; @@ -47,6 +49,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import android.view.DisplayAdjustments.FixedRotationAdjustments; import android.view.DisplayInfo; import android.view.InsetsState; import android.view.SurfaceControl; @@ -529,6 +532,7 @@ class WindowToken extends WindowContainer<WindowState> { mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, insetsState, new Configuration(config), mDisplayContent.getRotation()); onConfigurationChanged(getParent().getConfiguration()); + notifyFixedRotationTransform(true /* enabled */); } /** @@ -546,6 +550,7 @@ class WindowToken extends WindowContainer<WindowState> { mFixedRotationTransformState = fixedRotationState; fixedRotationState.mAssociatedTokens.add(this); onConfigurationChanged(getParent().getConfiguration()); + notifyFixedRotationTransform(true /* enabled */); } void finishFixedRotationTransform() { @@ -578,9 +583,52 @@ class WindowToken extends WindowContainer<WindowState> { // The state is cleared at the end, because it is used to indicate that other windows can // use seamless rotation when applying rotation to display. for (int i = state.mAssociatedTokens.size() - 1; i >= 0; i--) { - state.mAssociatedTokens.get(i).mFixedRotationTransformState = null; + state.mAssociatedTokens.get(i).cleanUpFixedRotationTransformState(); } + cleanUpFixedRotationTransformState(); + } + + private void cleanUpFixedRotationTransformState() { mFixedRotationTransformState = null; + notifyFixedRotationTransform(false /* enabled */); + } + + /** Notifies application side to enable or disable the rotation adjustment of display info. */ + private void notifyFixedRotationTransform(boolean enabled) { + FixedRotationAdjustments adjustments = null; + // A token may contain windows of the same processes or different processes. The list is + // used to avoid sending the same adjustments to a process multiple times. + ArrayList<WindowProcessController> notifiedProcesses = null; + for (int i = mChildren.size() - 1; i >= 0; i--) { + final WindowState w = mChildren.get(i); + final WindowProcessController app; + if (w.mAttrs.type == TYPE_APPLICATION_STARTING) { + // Use the host activity because starting window is controlled by window manager. + final ActivityRecord r = asActivityRecord(); + if (r == null) { + continue; + } + app = r.app; + } else { + app = mWmService.mAtmService.mProcessMap.getProcess(w.mSession.mPid); + } + if (app == null || !app.hasThread()) { + continue; + } + if (notifiedProcesses == null) { + notifiedProcesses = new ArrayList<>(2); + adjustments = enabled ? createFixedRotationAdjustmentsIfNeeded() : null; + } else if (notifiedProcesses.contains(app)) { + continue; + } + notifiedProcesses.add(app); + try { + mWmService.mAtmService.getLifecycleManager().scheduleTransaction( + app.getThread(), FixedRotationAdjustmentsItem.obtain(token, adjustments)); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to schedule DisplayAdjustmentsItem to " + app, e); + } + } } /** Restores the changes that applies to this container. */ @@ -590,6 +638,7 @@ class WindowToken extends WindowContainer<WindowState> { // The window may be detached or detaching. return; } + notifyFixedRotationTransform(false /* enabled */); final int originalRotation = getWindowConfiguration().getRotation(); onConfigurationChanged(parent.getConfiguration()); onCancelFixedRotationTransform(originalRotation); @@ -603,6 +652,14 @@ class WindowToken extends WindowContainer<WindowState> { void onCancelFixedRotationTransform(int originalDisplayRotation) { } + FixedRotationAdjustments createFixedRotationAdjustmentsIfNeeded() { + if (!isFixedRotationTransforming()) { + return null; + } + return new FixedRotationAdjustments(mFixedRotationTransformState.mDisplayInfo.rotation, + mFixedRotationTransformState.mDisplayInfo.displayCutout); + } + @Override void resolveOverrideConfiguration(Configuration newParentConfig) { super.resolveOverrideConfiguration(newParentConfig); diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS new file mode 100644 index 000000000000..8646a53f3390 --- /dev/null +++ b/services/core/jni/OWNERS @@ -0,0 +1,13 @@ +# Display +per-file com_android_server_lights_LightsService.cpp = michaelwr@google.com, santoscordon@google.com + +# Haptics +per-file com_android_server_VibratorService.cpp = michaelwr@google.com + +# Input +per-file com_android_server_input_InputManagerService.cpp = michaelwr@google.com, svv@google.com + +# Power +per-file com_android_server_HardwarePropertiesManagerService.cpp = michaelwr@google.com, santoscordon@google.com +per-file com_android_server_power_PowerManagerService.* = michaelwr@google.com, santoscordon@google.com + diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java index da716eaed82b..c687184265c1 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java @@ -30,6 +30,7 @@ import android.content.pm.ResolveInfo; import android.os.IBinder; import android.os.ServiceManager; import android.provider.Settings; +import android.provider.Telephony; import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; @@ -84,6 +85,7 @@ public class PersonalAppsSuspensionHelper { result.removeAll(getSystemLauncherPackages()); result.removeAll(getAccessibilityServices()); result.removeAll(getInputMethodPackages()); + result.remove(Telephony.Sms.getDefaultSmsPackage(mContext)); result.remove(getSettingsPackageName()); final String[] unsuspendablePackages = diff --git a/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java index 5d849c114e69..2be3f1e81897 100644 --- a/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java @@ -19,6 +19,7 @@ import static android.app.usage.UsageEvents.Event.MAX_EVENT_TYPE; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; import android.app.usage.UsageEvents; import android.content.res.Configuration; @@ -26,9 +27,12 @@ import android.test.suitebuilder.annotation.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.ArrayUtils; + import org.junit.Test; import org.junit.runner.RunWith; +import java.lang.reflect.Field; import java.util.Locale; @RunWith(AndroidJUnit4.class) @@ -191,4 +195,27 @@ public class IntervalStatsTests { assertTrue(intervalStats.events.size() > NUMBER_OF_EVENTS - NUMBER_OF_EVENTS_PER_PACKAGE); assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES); } + + // All fields in this list are defined in IntervalStats and persisted - please ensure they're + // defined correctly in both usagestatsservice.proto and usagestatsservice_v2.proto + private static final String[] INTERVALSTATS_PERSISTED_FIELDS = {"beginTime", "endTime", + "mStringCache", "majorVersion", "minorVersion", "interactiveTracker", + "nonInteractiveTracker", "keyguardShownTracker", "keyguardHiddenTracker", + "packageStats", "configurations", "activeConfiguration", "events"}; + // All fields in this list are defined in IntervalStats but not persisted + private static final String[] INTERVALSTATS_IGNORED_FIELDS = {"lastTimeSaved", + "packageStatsObfuscated", "CURRENT_MAJOR_VERSION", "CURRENT_MINOR_VERSION", "TAG"}; + + @Test + public void testIntervalStatsFieldsAreKnown() { + final IntervalStats stats = new IntervalStats(); + final Field[] fields = stats.getClass().getDeclaredFields(); + for (Field field : fields) { + if (!(ArrayUtils.contains(INTERVALSTATS_PERSISTED_FIELDS, field.getName()) + || ArrayUtils.contains(INTERVALSTATS_IGNORED_FIELDS, field.getName()))) { + fail("Found an unknown field: " + field.getName() + ". Please correctly update " + + "either INTERVALSTATS_PERSISTED_FIELDS or INTERVALSTATS_IGNORED_FIELDS."); + } + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index fc256b09f2b2..702d9d3142fb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -53,6 +53,7 @@ import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; import com.android.server.pm.PackageManagerService; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -161,6 +162,12 @@ public class ActivityStartInterceptorTest { mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME; } + @After + public void tearDown() { + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + } + @Test public void testSuspendedByAdminPackage() { // GIVEN the package we're about to launch is currently suspended diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 2ea58a028a0a..fdc5c7bf0ce1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -836,7 +836,7 @@ public class WindowOrganizerTests extends WindowTestsBase { spyOn(record); doReturn(true).when(record).checkEnterPictureInPictureState(any(), anyBoolean()); - record.getRootTask().setHasBeenVisible(true); + record.getTask().setHasBeenVisible(true); return record; } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 8c9b77e5cb9a..b59556f0c17a 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -1873,7 +1873,7 @@ public class UsageStatsService extends SystemService implements final DevicePolicyManagerInternal dpmInternal = getDpmInternal(); if (!hasPermissions(callingPackage, Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE) - && (dpmInternal != null && !dpmInternal.isActiveSupervisionApp(callingUid))) { + && (dpmInternal == null || !dpmInternal.isActiveSupervisionApp(callingUid))) { throw new SecurityException("Caller must be the active supervision app or " + "it must have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions"); } @@ -1900,7 +1900,7 @@ public class UsageStatsService extends SystemService implements final DevicePolicyManagerInternal dpmInternal = getDpmInternal(); if (!hasPermissions(callingPackage, Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE) - && (dpmInternal != null && !dpmInternal.isActiveSupervisionApp(callingUid))) { + && (dpmInternal == null || !dpmInternal.isActiveSupervisionApp(callingUid))) { throw new SecurityException("Caller must be the active supervision app or " + "it must have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions"); } diff --git a/telephony/java/com/android/internal/telephony/TelephonyProperties.java b/telephony/java/com/android/internal/telephony/TelephonyProperties.java index ff70f8ba3936..29286e8f429e 100644 --- a/telephony/java/com/android/internal/telephony/TelephonyProperties.java +++ b/telephony/java/com/android/internal/telephony/TelephonyProperties.java @@ -240,5 +240,5 @@ public interface TelephonyProperties * two. * Type: int */ - static final String PROPERTY_MAX_ACTIVE_MODEMS = "ro.telephony.max.active.modems"; + static final String PROPERTY_MAX_ACTIVE_MODEMS = "telephony.active_modems.max_count"; } diff --git a/test-mock/api/lint-baseline.txt b/test-mock/api/lint-baseline.txt index c6ba3f5d8fd8..1411824117e8 100644 --- a/test-mock/api/lint-baseline.txt +++ b/test-mock/api/lint-baseline.txt @@ -21,10 +21,6 @@ MissingNullability: android.test.mock.MockContentProvider#getStreamTypes(android Missing nullability on parameter `url` in method `getStreamTypes` MissingNullability: android.test.mock.MockContentProvider#getStreamTypes(android.net.Uri, String) parameter #1: Missing nullability on parameter `mimeTypeFilter` in method `getStreamTypes` -MissingNullability: android.test.mock.MockContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean) parameter #0: - Missing nullability on parameter `uri` in method `notifyChange` -MissingNullability: android.test.mock.MockContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean) parameter #1: - Missing nullability on parameter `observer` in method `notifyChange` MissingNullability: android.test.mock.MockContext#bindIsolatedService(android.content.Intent, int, String, java.util.concurrent.Executor, android.content.ServiceConnection) parameter #0: Missing nullability on parameter `service` in method `bindIsolatedService` MissingNullability: android.test.mock.MockContext#bindIsolatedService(android.content.Intent, int, String, java.util.concurrent.Executor, android.content.ServiceConnection) parameter #2: @@ -39,6 +35,10 @@ MissingNullability: android.test.mock.MockContext#bindService(android.content.In Missing nullability on parameter `executor` in method `bindService` MissingNullability: android.test.mock.MockContext#bindService(android.content.Intent, int, java.util.concurrent.Executor, android.content.ServiceConnection) parameter #3: Missing nullability on parameter `conn` in method `bindService` +MissingNullability: android.test.mock.MockContext#createWindowContext(int, android.os.Bundle) parameter #1: + Missing nullability on parameter `options` in method `createWindowContext` +MissingNullability: android.test.mock.MockContext#getDisplay(): + Missing nullability on method `getDisplay` return MissingNullability: android.test.mock.MockContext#getMainExecutor(): Missing nullability on method `getMainExecutor` return MissingNullability: android.test.mock.MockContext#sendOrderedBroadcast(android.content.Intent, String, String, android.content.BroadcastReceiver, android.os.Handler, int, String, android.os.Bundle) parameter #0: diff --git a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java index 23098ec067d2..529d03c520ba 100644 --- a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java +++ b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java @@ -547,6 +547,16 @@ public class IpSecServiceParameterizedTest { @Test public void testApplyTransportModeTransform() throws Exception { + verifyApplyTransportModeTransformCommon(false); + } + + @Test + public void testApplyTransportModeTransformReleasedSpi() throws Exception { + verifyApplyTransportModeTransformCommon(true); + } + + public void verifyApplyTransportModeTransformCommon( + boolean closeSpiBeforeApply) throws Exception { IpSecConfig ipSecConfig = new IpSecConfig(); addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); addAuthAndCryptToIpSecConfig(ipSecConfig); @@ -554,6 +564,39 @@ public class IpSecServiceParameterizedTest { IpSecTransformResponse createTransformResp = mIpSecService.createTransform(ipSecConfig, new Binder(), "blessedPackage"); + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + + int resourceId = createTransformResp.resourceId; + mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId); + + verify(mMockNetd) + .ipSecApplyTransportModeTransform( + eq(pfd), + eq(mUid), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI)); + } + + @Test + public void testApplyTransportModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), "blessedPackage"); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + Socket socket = new Socket(); socket.bind(null); ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); @@ -660,6 +703,15 @@ public class IpSecServiceParameterizedTest { @Test public void testApplyTunnelModeTransform() throws Exception { + verifyApplyTunnelModeTransformCommon(false); + } + + @Test + public void testApplyTunnelModeTransformReleasedSpi() throws Exception { + verifyApplyTunnelModeTransformCommon(true); + } + + public void verifyApplyTunnelModeTransformCommon(boolean closeSpiBeforeApply) throws Exception { IpSecConfig ipSecConfig = new IpSecConfig(); ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); @@ -670,6 +722,49 @@ public class IpSecServiceParameterizedTest { IpSecTunnelInterfaceResponse createTunnelResp = createAndValidateTunnel(mSourceAddr, mDestinationAddr, "blessedPackage"); + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + int transformResourceId = createTransformResp.resourceId; + int tunnelResourceId = createTunnelResp.resourceId; + mIpSecService.applyTunnelModeTransform(tunnelResourceId, IpSecManager.DIRECTION_OUT, + transformResourceId, "blessedPackage"); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd) + .ipSecUpdateSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(tunnelResourceId)); + } + + ipSecConfig.setXfrmInterfaceId(tunnelResourceId); + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + + @Test + public void testApplyTunnelModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), "blessedPackage"); + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, "blessedPackage"); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + int transformResourceId = createTransformResp.resourceId; int tunnelResourceId = createTunnelResp.resourceId; mIpSecService.applyTunnelModeTransform(tunnelResourceId, IpSecManager.DIRECTION_OUT, diff --git a/tools/validatekeymaps/OWNERS b/tools/validatekeymaps/OWNERS new file mode 100644 index 000000000000..0313a40f7270 --- /dev/null +++ b/tools/validatekeymaps/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +svv@google.com diff --git a/wifi/java/android/net/wifi/ScanResult.java b/wifi/java/android/net/wifi/ScanResult.java index f5b56225b8e5..aa3a13925894 100644 --- a/wifi/java/android/net/wifi/ScanResult.java +++ b/wifi/java/android/net/wifi/ScanResult.java @@ -524,48 +524,180 @@ public final class ScanResult implements Parcelable { * {@hide} */ public final static int UNSPECIFIED = -1; + /** + * 2.4 GHz band first channel number * @hide */ - public boolean is24GHz() { - return ScanResult.is24GHz(frequency); + public static final int BAND_24_GHZ_FIRST_CH_NUM = 1; + /** + * 2.4 GHz band last channel number + * @hide + */ + public static final int BAND_24_GHZ_LAST_CH_NUM = 14; + /** + * 2.4 GHz band frequency of first channel in MHz + * @hide + */ + public static final int BAND_24_GHZ_START_FREQ_MHZ = 2412; + /** + * 2.4 GHz band frequency of last channel in MHz + * @hide + */ + public static final int BAND_24_GHZ_END_FREQ_MHZ = 2484; + + /** + * 5 GHz band first channel number + * @hide + */ + public static final int BAND_5_GHZ_FIRST_CH_NUM = 32; + /** + * 5 GHz band last channel number + * @hide + */ + public static final int BAND_5_GHZ_LAST_CH_NUM = 173; + /** + * 5 GHz band frequency of first channel in MHz + * @hide + */ + public static final int BAND_5_GHZ_START_FREQ_MHZ = 5160; + /** + * 5 GHz band frequency of last channel in MHz + * @hide + */ + public static final int BAND_5_GHZ_END_FREQ_MHZ = 5865; + + /** + * 6 GHz band first channel number + * @hide + */ + public static final int BAND_6_GHZ_FIRST_CH_NUM = 1; + /** + * 6 GHz band last channel number + * @hide + */ + public static final int BAND_6_GHZ_LAST_CH_NUM = 233; + /** + * 6 GHz band frequency of first channel in MHz + * @hide + */ + public static final int BAND_6_GHZ_START_FREQ_MHZ = 5945; + /** + * 6 GHz band frequency of last channel in MHz + * @hide + */ + public static final int BAND_6_GHZ_END_FREQ_MHZ = 7105; + + /** + * Utility function to check if a frequency within 2.4 GHz band + * @param freqMhz frequency in MHz + * @return true if within 2.4GHz, false otherwise + * + * @hide + */ + public static boolean is24GHz(int freqMhz) { + return freqMhz >= BAND_24_GHZ_START_FREQ_MHZ && freqMhz <= BAND_24_GHZ_END_FREQ_MHZ; } /** + * Utility function to check if a frequency within 5 GHz band + * @param freqMhz frequency in MHz + * @return true if within 5GHz, false otherwise + * * @hide - * TODO: makes real freq boundaries */ - public static boolean is24GHz(int freq) { - return freq > 2400 && freq < 2500; + public static boolean is5GHz(int freqMhz) { + return freqMhz >= BAND_5_GHZ_START_FREQ_MHZ && freqMhz <= BAND_5_GHZ_END_FREQ_MHZ; } /** + * Utility function to check if a frequency within 6 GHz band + * @param freqMhz + * @return true if within 6GHz, false otherwise + * * @hide */ - public boolean is5GHz() { - return ScanResult.is5GHz(frequency); + public static boolean is6GHz(int freqMhz) { + return freqMhz >= BAND_6_GHZ_START_FREQ_MHZ && freqMhz <= BAND_6_GHZ_END_FREQ_MHZ; } /** + * Utility function to convert channel number/band to frequency in MHz + * @param channel number to convert + * @param band of channel to convert + * @return center frequency in Mhz of the channel, {@link UNSPECIFIED} if no match + * * @hide */ - public boolean is6GHz() { - return ScanResult.is6GHz(frequency); + public static int convertChannelToFrequencyMhz(int channel, @WifiScanner.WifiBand int band) { + if (band == WifiScanner.WIFI_BAND_24_GHZ) { + // Special case + if (channel == 14) { + return 2484; + } else if (channel >= BAND_24_GHZ_FIRST_CH_NUM && channel <= BAND_24_GHZ_LAST_CH_NUM) { + return ((channel - BAND_24_GHZ_FIRST_CH_NUM) * 5) + BAND_24_GHZ_START_FREQ_MHZ; + } else { + return UNSPECIFIED; + } + } + if (band == WifiScanner.WIFI_BAND_5_GHZ) { + if (channel >= BAND_5_GHZ_FIRST_CH_NUM && channel <= BAND_5_GHZ_LAST_CH_NUM) { + return ((channel - BAND_5_GHZ_FIRST_CH_NUM) * 5) + BAND_5_GHZ_START_FREQ_MHZ; + } else { + return UNSPECIFIED; + } + } + if (band == WifiScanner.WIFI_BAND_6_GHZ) { + if (channel >= BAND_6_GHZ_FIRST_CH_NUM && channel <= BAND_6_GHZ_LAST_CH_NUM) { + return ((channel - BAND_6_GHZ_FIRST_CH_NUM) * 5) + BAND_6_GHZ_START_FREQ_MHZ; + } else { + return UNSPECIFIED; + } + } + return UNSPECIFIED; } /** + * Utility function to convert frequency in MHz to channel number + * @param freqMhz frequency in MHz + * @return channel number associated with given frequency, {@link UNSPECIFIED} if no match + * * @hide - * TODO: makes real freq boundaries */ - public static boolean is5GHz(int freq) { - return freq > 4900 && freq < 5900; + public static int convertFrequencyMhzToChannel(int freqMhz) { + // Special case + if (freqMhz == 2484) { + return 14; + } else if (is24GHz(freqMhz)) { + return (freqMhz - BAND_24_GHZ_START_FREQ_MHZ) / 5 + BAND_24_GHZ_FIRST_CH_NUM; + } else if (is5GHz(freqMhz)) { + return ((freqMhz - BAND_5_GHZ_START_FREQ_MHZ) / 5) + BAND_5_GHZ_FIRST_CH_NUM; + } else if (is6GHz(freqMhz)) { + return ((freqMhz - BAND_6_GHZ_START_FREQ_MHZ) / 5) + BAND_6_GHZ_FIRST_CH_NUM; + } + + return UNSPECIFIED; + } + + /** + * @hide + */ + public boolean is24GHz() { + return ScanResult.is24GHz(frequency); } /** * @hide */ - public static boolean is6GHz(int freq) { - return freq > 5925 && freq < 7125; + public boolean is5GHz() { + return ScanResult.is5GHz(frequency); + } + + /** + * @hide + */ + public boolean is6GHz() { + return ScanResult.is6GHz(frequency); } /** diff --git a/wifi/tests/src/android/net/wifi/ScanResultTest.java b/wifi/tests/src/android/net/wifi/ScanResultTest.java index 6cb832463bc0..5516f433070f 100644 --- a/wifi/tests/src/android/net/wifi/ScanResultTest.java +++ b/wifi/tests/src/android/net/wifi/ScanResultTest.java @@ -46,6 +46,68 @@ public class ScanResultTest { ScanResult.WIFI_STANDARD_11AC; /** + * Frequency to channel map. This include some frequencies used outside the US. + * Representing it using a vector (instead of map) for simplification. + */ + private static final int[] FREQUENCY_TO_CHANNEL_MAP = { + 2412, WifiScanner.WIFI_BAND_24_GHZ, 1, + 2417, WifiScanner.WIFI_BAND_24_GHZ, 2, + 2422, WifiScanner.WIFI_BAND_24_GHZ, 3, + 2427, WifiScanner.WIFI_BAND_24_GHZ, 4, + 2432, WifiScanner.WIFI_BAND_24_GHZ, 5, + 2437, WifiScanner.WIFI_BAND_24_GHZ, 6, + 2442, WifiScanner.WIFI_BAND_24_GHZ, 7, + 2447, WifiScanner.WIFI_BAND_24_GHZ, 8, + 2452, WifiScanner.WIFI_BAND_24_GHZ, 9, + 2457, WifiScanner.WIFI_BAND_24_GHZ, 10, + 2462, WifiScanner.WIFI_BAND_24_GHZ, 11, + /* 12, 13 are only legitimate outside the US. */ + 2467, WifiScanner.WIFI_BAND_24_GHZ, 12, + 2472, WifiScanner.WIFI_BAND_24_GHZ, 13, + /* 14 is for Japan, DSSS and CCK only. */ + 2484, WifiScanner.WIFI_BAND_24_GHZ, 14, + /* 34 valid in Japan. */ + 5170, WifiScanner.WIFI_BAND_5_GHZ, 34, + 5180, WifiScanner.WIFI_BAND_5_GHZ, 36, + 5190, WifiScanner.WIFI_BAND_5_GHZ, 38, + 5200, WifiScanner.WIFI_BAND_5_GHZ, 40, + 5210, WifiScanner.WIFI_BAND_5_GHZ, 42, + 5220, WifiScanner.WIFI_BAND_5_GHZ, 44, + 5230, WifiScanner.WIFI_BAND_5_GHZ, 46, + 5240, WifiScanner.WIFI_BAND_5_GHZ, 48, + 5260, WifiScanner.WIFI_BAND_5_GHZ, 52, + 5280, WifiScanner.WIFI_BAND_5_GHZ, 56, + 5300, WifiScanner.WIFI_BAND_5_GHZ, 60, + 5320, WifiScanner.WIFI_BAND_5_GHZ, 64, + 5500, WifiScanner.WIFI_BAND_5_GHZ, 100, + 5520, WifiScanner.WIFI_BAND_5_GHZ, 104, + 5540, WifiScanner.WIFI_BAND_5_GHZ, 108, + 5560, WifiScanner.WIFI_BAND_5_GHZ, 112, + 5580, WifiScanner.WIFI_BAND_5_GHZ, 116, + /* 120, 124, 128 valid in Europe/Japan. */ + 5600, WifiScanner.WIFI_BAND_5_GHZ, 120, + 5620, WifiScanner.WIFI_BAND_5_GHZ, 124, + 5640, WifiScanner.WIFI_BAND_5_GHZ, 128, + /* 132+ valid in US. */ + 5660, WifiScanner.WIFI_BAND_5_GHZ, 132, + 5680, WifiScanner.WIFI_BAND_5_GHZ, 136, + 5700, WifiScanner.WIFI_BAND_5_GHZ, 140, + /* 144 is supported by a subset of WiFi chips. */ + 5720, WifiScanner.WIFI_BAND_5_GHZ, 144, + 5745, WifiScanner.WIFI_BAND_5_GHZ, 149, + 5765, WifiScanner.WIFI_BAND_5_GHZ, 153, + 5785, WifiScanner.WIFI_BAND_5_GHZ, 157, + 5805, WifiScanner.WIFI_BAND_5_GHZ, 161, + 5825, WifiScanner.WIFI_BAND_5_GHZ, 165, + 5845, WifiScanner.WIFI_BAND_5_GHZ, 169, + 5865, WifiScanner.WIFI_BAND_5_GHZ, 173, + /* Now some 6GHz channels */ + 5945, WifiScanner.WIFI_BAND_6_GHZ, 1, + 5960, WifiScanner.WIFI_BAND_6_GHZ, 4, + 6100, WifiScanner.WIFI_BAND_6_GHZ, 32 + }; + + /** * Setup before tests. */ @Before @@ -184,6 +246,25 @@ public class ScanResultTest { } /** + * verify frequency to channel conversion for all possible frequencies. + */ + @Test + public void convertFrequencyToChannel() throws Exception { + for (int i = 0; i < FREQUENCY_TO_CHANNEL_MAP.length; i += 3) { + assertEquals(FREQUENCY_TO_CHANNEL_MAP[i + 2], + ScanResult.convertFrequencyMhzToChannel(FREQUENCY_TO_CHANNEL_MAP[i])); + } + } + + /** + * Verify frequency to channel conversion failed for an invalid frequency. + */ + @Test + public void convertFrequencyToChannelWithInvalidFreq() throws Exception { + assertEquals(-1, ScanResult.convertFrequencyMhzToChannel(8000)); + } + + /** * Write the provided {@link ScanResult} to a parcel and deserialize it. */ private static ScanResult parcelReadWrite(ScanResult writeResult) throws Exception { |