diff options
152 files changed, 5600 insertions, 2073 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 091622715578..2ae72ef4e81c 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -456,6 +456,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "com.android.media.flags.bettertogether-aconfig-java-host", + aconfig_declarations: "com.android.media.flags.bettertogether-aconfig", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media TV aconfig_declarations { name: "android.media.tv.flags-aconfig", diff --git a/core/api/current.txt b/core/api/current.txt index 15c054f0b5e8..0210fc3bd113 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26669,12 +26669,16 @@ package android.media.tv { public static final class TvContract.Channels implements android.media.tv.TvContract.BaseTvColumns { method @Nullable public static String getVideoResolution(String); + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; // 0x2 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; // 0x1 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; // 0x0 field public static final String COLUMN_APP_LINK_COLOR = "app_link_color"; field public static final String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri"; field public static final String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri"; field public static final String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri"; field public static final String COLUMN_APP_LINK_TEXT = "app_link_text"; field public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; field public static final String COLUMN_BROWSABLE = "browsable"; field public static final String COLUMN_CHANNEL_LIST_ID = "channel_list_id"; field public static final String COLUMN_DESCRIPTION = "description"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d26662d9039d..95fa903a4243 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14554,6 +14554,7 @@ package android.telephony { field public static final int EVENT_SERVICE_STATE_CHANGED = 1; // 0x1 field public static final int EVENT_SIGNAL_STRENGTHS_CHANGED = 9; // 0x9 field public static final int EVENT_SIGNAL_STRENGTH_CHANGED = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; // 0x29 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SRVCC_STATE_CHANGED = 16; // 0x10 field public static final int EVENT_USER_MOBILE_DATA_STATE_CHANGED = 20; // 0x14 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_VOICE_ACTIVATION_STATE_CHANGED = 18; // 0x12 @@ -14600,6 +14601,10 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onRadioPowerStateChanged(int); } + @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public static interface TelephonyCallback.SimultaneousCellularCallingSupportListener { + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSimultaneousCellularCallingSubscriptionsChanged(@NonNull java.util.Set<java.lang.Integer>); + } + public static interface TelephonyCallback.SrvccStateListener { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSrvccStateChanged(int); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index ebb5ba0e7b0f..1fd49ef43e93 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3574,7 +3574,7 @@ public class ActivityManager { * foreground. This may be running a window that is behind the current * foreground (so paused and with its state saved, not interacting with * the user, but visible to them to some degree); it may also be running - * other services under the system's control that it inconsiders important. + * other services under the system's control that it considers important. */ public static final int IMPORTANCE_VISIBLE = 200; @@ -3646,9 +3646,9 @@ public class ActivityManager { public static final int IMPORTANCE_CANT_SAVE_STATE = 350; /** - * Constant for {@link #importance}: This process process contains - * cached code that is expendable, not actively running any app components - * we care about. + * Constant for {@link #importance}: This process contains cached code + * that is expendable, not actively running any app components we care + * about. */ public static final int IMPORTANCE_CACHED = 400; diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index b303ea64406c..4fc25fd8c699 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -13,3 +13,10 @@ flag { description: "API to get importance of UID that's binding to the caller" bug: "292533010" } + +flag { + namespace: "backstage_power" + name: "app_restrictions_api" + description: "API to track and query restrictions applied to apps" + bug: "320150834" +}
\ No newline at end of file diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index f876eebe64c1..1165f9a697de 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -33,4 +33,11 @@ flag { name: "new_settings_ui" description: "Enables new settings UI for VIC" bug: "315209085" +} + +flag { + namespace: "credential_manager" + name: "selector_ui_improvements_enabled" + description: "Enables Credential Selector UI improvements for VIC" + bug: "319448437" }
\ No newline at end of file diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java index 746278fc296c..e6bfcd728a52 100644 --- a/core/java/android/os/PerformanceHintManager.java +++ b/core/java/android/os/PerformanceHintManager.java @@ -183,13 +183,14 @@ public final class PerformanceHintManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"CPU_LOAD_"}, value = { + @IntDef(prefix = {"CPU_LOAD_", "GPU_LOAD_"}, value = { CPU_LOAD_UP, CPU_LOAD_DOWN, CPU_LOAD_RESET, CPU_LOAD_RESUME, GPU_LOAD_UP, - GPU_LOAD_DOWN + GPU_LOAD_DOWN, + GPU_LOAD_RESET }) public @interface Hint {} diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index adc54f5b5a8c..f2bdbf67e76e 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -325,7 +325,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryDetected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryDetected(partialQuery); + mExecutor.execute(()->mCallback.onQueryDetected(partialQuery)); } }); } @@ -335,7 +335,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryFinished"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryFinished(); + mExecutor.execute(()->mCallback.onQueryFinished()); } }); } @@ -345,7 +345,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryRejected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryRejected(); + mExecutor.execute(()->mCallback.onQueryRejected()); } }); } diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index cf1156db55e5..fb57921b1529 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -1681,6 +1681,10 @@ public class PhoneStateListener { @EmergencyCallbackModeStopReason int reason) { // not support. Can't override. Use TelephonyCallback. } + + public final void onSimultaneousCallingStateChanged(int[] subIds) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 19bcf28d6b83..dc6a035a8176 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -18,6 +18,7 @@ package android.telephony; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; @@ -33,15 +34,19 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IPhoneStateListener; +import com.android.internal.telephony.flags.Flags; import dalvik.system.VMRuntime; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; +import java.util.stream.Collectors; /** * A callback class for monitoring changes in specific telephony states @@ -627,6 +632,18 @@ public class TelephonyCallback { public static final int EVENT_EMERGENCY_CALLBACK_MODE_CHANGED = 40; /** + * Event for listening to changes in simultaneous cellular calling subscriptions. + * + * @see SimultaneousCellularCallingSupportListener + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @SystemApi + public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -669,7 +686,8 @@ public class TelephonyCallback { EVENT_LINK_CAPACITY_ESTIMATE_CHANGED, EVENT_TRIGGER_NOTIFY_ANBR, EVENT_MEDIA_QUALITY_STATUS_CHANGED, - EVENT_EMERGENCY_CALLBACK_MODE_CHANGED + EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, + EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1373,6 +1391,44 @@ public class TelephonyCallback { } /** + * Interface for listening to changes in the simultaneous cellular calling state for active + * cellular subscriptions. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @SystemApi + public interface SimultaneousCellularCallingSupportListener { + /** + * Notify the Listener that the subscriptions available for simultaneous <b>cellular</b> + * calling have changed. + * <p> + * If we have an ongoing <b>cellular</b> call on one subscription in this Set, a + * simultaneous incoming or outgoing <b>cellular</b> call is possible on any of the + * subscriptions in this Set. On a traditional Dual Sim Dual Standby device, simultaneous + * calling is not possible between subscriptions, where on a Dual Sim Dual Active device, + * simultaneous calling may be possible between subscriptions in certain network conditions. + * <p> + * Note: This listener only tracks the capability of the modem to perform simultaneous + * cellular calls and does not track the simultaneous calling state of scenarios based on + * multiple IMS registration over multiple transports (WiFi/Internet calling). + * <p> + * Note: This listener fires for all changes to cellular calling subscriptions independent + * of which subscription it is registered on. + * + * @param simultaneousCallingSubscriptionIds The Set of subscription IDs that support + * simultaneous calling. If there is an ongoing call on a subscription in this Set, then a + * simultaneous incoming or outgoing call is only possible for other subscriptions in this + * Set. If there is an ongoing call on a subscription that is not in this Set, then + * simultaneous calling is not possible at the current time. + * + */ + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + void onSimultaneousCellularCallingSubscriptionsChanged( + @NonNull Set<Integer> simultaneousCallingSubscriptionIds); + } + + /** * Interface for call attributes listener. * * @hide @@ -1976,6 +2032,17 @@ public class TelephonyCallback { allowedNetworkType))); } + public void onSimultaneousCallingStateChanged(int[] subIds) { + SimultaneousCellularCallingSupportListener listener = + (SimultaneousCellularCallingSupportListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + Binder.withCleanCallingIdentity( + () -> mExecutor.execute( + () -> listener.onSimultaneousCellularCallingSubscriptionsChanged( + Arrays.stream(subIds).boxed().collect(Collectors.toSet())))); + } + public void onLinkCapacityEstimateChanged( List<LinkCapacityEstimate> linkCapacityEstimateList) { LinkCapacityEstimateChangedListener listener = diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 886727ea43ef..0de450519646 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -994,6 +994,21 @@ public class TelephonyRegistryManager { } } + /** + * Notify external listeners that the subscriptions supporting simultaneous cellular calling + * have changed. + * @param subIds The new set of subIds supporting simultaneous cellular calling. + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged(Set<Integer> subIds) { + try { + sRegistry.notifySimultaneousCellularCallingSubscriptionsChanged( + subIds.stream().mapToInt(i -> i).toArray()); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + public @NonNull Set<Integer> getEventsFromCallback( @NonNull TelephonyCallback telephonyCallback) { Set<Integer> eventList = new ArraySet<>(); @@ -1135,7 +1150,11 @@ public class TelephonyRegistryManager { eventList.add(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); } - + if (telephonyCallback + instanceof TelephonyCallback.SimultaneousCellularCallingSupportListener) { + eventList.add( + TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); + } return eventList; } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 36b74e39072a..7903050e41d4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -69,12 +69,13 @@ import android.view.SurfaceControl; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.window.AddToSurfaceSyncGroupResult; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; -import android.window.ScreenCapture; -import android.window.WindowContextInfo; import android.window.ITrustedPresentationListener; +import android.window.ScreenCapture; import android.window.TrustedPresentationThresholds; +import android.window.WindowContextInfo; /** * System private interface to the window manager. @@ -1083,4 +1084,8 @@ interface IWindowManager void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); + + boolean registerScreenRecordingCallback(IScreenRecordingCallback callback); + + void unregisterScreenRecordingCallback(IScreenRecordingCallback callback); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 257ecc565c87..c98d1d7ecaea 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -15015,6 +15015,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** @hide */ + @Nullable View getSelfOrParentImportantForA11y() { if (isImportantForAccessibility()) return this; ViewParent parent = getParentForAccessibility(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c66f3c8032fd..34b467008232 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -87,6 +87,7 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_B import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; +import static android.view.accessibility.Flags.fixMergedContentChangeEvent; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -11509,6 +11510,15 @@ public final class ViewRootImpl implements ViewParent, event.setContentChangeTypes(mChangeTypes); if (mAction.isPresent()) event.setAction(mAction.getAsInt()); if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin; + + if (fixMergedContentChangeEvent()) { + if ((mChangeTypes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { + final View importantParent = source.getSelfOrParentImportantForA11y(); + if (importantParent != null) { + source = importantParent; + } + } + } source.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -11543,14 +11553,29 @@ public final class ViewRootImpl implements ViewParent, } if (mSource != null) { - // If there is no common predecessor, then mSource points to - // a removed view, hence in this case always prefer the source. - View predecessor = getCommonPredecessor(mSource, source); - if (predecessor != null) { - predecessor = predecessor.getSelfOrParentImportantForA11y(); - } - mSource = (predecessor != null) ? predecessor : source; - mChangeTypes |= changeType; + if (fixMergedContentChangeEvent()) { + View newSource = getCommonPredecessor(mSource, source); + if (newSource == null) { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + newSource = source; + } + + mChangeTypes |= changeType; + if (mSource != newSource) { + mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; + mSource = newSource; + } + } else { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + View predecessor = getCommonPredecessor(mSource, source); + if (predecessor != null) { + predecessor = predecessor.getSelfOrParentImportantForA11y(); + } + mSource = (predecessor != null) ? predecessor : source; + mChangeTypes |= changeType; + } final int performingAction = mAccessibilityManager.getPerformingAction(); if (performingAction != 0) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index c7355c144c5f..efae57c9946c 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -53,6 +53,13 @@ flag { flag { namespace: "accessibility" + name: "fix_merged_content_change_event" + description: "Fixes event type and source of content change event merged in ViewRootImpl" + bug: "277305460" +} + +flag { + namespace: "accessibility" name: "flash_notification_system_api" description: "Makes flash notification APIs as system APIs for calling from mainline module" bug: "303131332" diff --git a/core/java/android/window/IScreenRecordingCallback.aidl b/core/java/android/window/IScreenRecordingCallback.aidl new file mode 100644 index 000000000000..560ee75aa365 --- /dev/null +++ b/core/java/android/window/IScreenRecordingCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +/** + * @hide + */ +oneway interface IScreenRecordingCallback { + void onScreenRecordingStateChanged(boolean visibleInScreenRecording); +} diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index acc6a749e9b7..7b8cdff8e20b 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -125,6 +125,16 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_SET_DIM_ON_TASK = 16; + /** + * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is + * launched in a mode requiring clear top. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -144,6 +154,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, + OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} @@ -173,12 +184,14 @@ public final class TaskFragmentOperation implements Parcelable { private final boolean mDimOnTask; + private final boolean mMoveToBottomIfClearWhenLaunch; + private TaskFragmentOperation(@OperationType int opType, @Nullable TaskFragmentCreationParams taskFragmentCreationParams, @Nullable IBinder activityToken, @Nullable Intent activityIntent, @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams, - boolean isolatedNav, boolean dimOnTask) { + boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch) { mOpType = opType; mTaskFragmentCreationParams = taskFragmentCreationParams; mActivityToken = activityToken; @@ -188,6 +201,7 @@ public final class TaskFragmentOperation implements Parcelable { mAnimationParams = animationParams; mIsolatedNav = isolatedNav; mDimOnTask = dimOnTask; + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; } private TaskFragmentOperation(Parcel in) { @@ -200,6 +214,7 @@ public final class TaskFragmentOperation implements Parcelable { mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); mIsolatedNav = in.readBoolean(); mDimOnTask = in.readBoolean(); + mMoveToBottomIfClearWhenLaunch = in.readBoolean(); } @Override @@ -213,6 +228,7 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeTypedObject(mAnimationParams, flags); dest.writeBoolean(mIsolatedNav); dest.writeBoolean(mDimOnTask); + dest.writeBoolean(mMoveToBottomIfClearWhenLaunch); } @NonNull @@ -300,6 +316,14 @@ public final class TaskFragmentOperation implements Parcelable { return mDimOnTask; } + /** + * Returns whether the TaskFragment should move to bottom of task when any activity below it + * is launched in clear top mode. + */ + public boolean isMoveToBottomIfClearWhenLaunch() { + return mMoveToBottomIfClearWhenLaunch; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -324,6 +348,7 @@ public final class TaskFragmentOperation implements Parcelable { } sb.append(", isolatedNav=").append(mIsolatedNav); sb.append(", dimOnTask=").append(mDimOnTask); + sb.append(", moveToBottomIfClearWhenLaunch=").append(mMoveToBottomIfClearWhenLaunch); sb.append('}'); return sb.toString(); @@ -332,7 +357,8 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, - mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask); + mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask, + mMoveToBottomIfClearWhenLaunch); } @Override @@ -349,7 +375,8 @@ public final class TaskFragmentOperation implements Parcelable { && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) && Objects.equals(mAnimationParams, other.mAnimationParams) && mIsolatedNav == other.mIsolatedNav - && mDimOnTask == other.mDimOnTask; + && mDimOnTask == other.mDimOnTask + && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch; } @Override @@ -385,6 +412,8 @@ public final class TaskFragmentOperation implements Parcelable { private boolean mDimOnTask; + private boolean mMoveToBottomIfClearWhenLaunch; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -466,13 +495,23 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets whether the TaskFragment should move to bottom of task when any activity below it + * is launched in clear top mode. + */ + @NonNull + public Builder setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) { + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; + return this; + } + + /** * Constructs the {@link TaskFragmentOperation}. */ @NonNull public TaskFragmentOperation build() { return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, - mIsolatedNav, mDimOnTask); + mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch); } } } diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 3c3c8469b3a4..751c1a8dd0ff 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -80,3 +80,11 @@ flag { is_fixed_read_only: true bug: "278757236" } + +flag { + namespace: "window_surfaces" + name: "screen_recording_callbacks" + description: "Enable screen recording callbacks public API" + is_fixed_read_only: true + bug: "304574518" +} diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 03cfd4f2ab91..969f95db002d 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -80,4 +80,5 @@ oneway interface IPhoneStateListener { void onMediaQualityStatusChanged(in MediaQualityStatus mediaQualityStatus); void onCallBackModeStarted(int type); void onCallBackModeStopped(int type, int reason); + void onSimultaneousCallingStateChanged(in int[] subIds); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index aab22421b334..0203ea49f252 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -104,6 +104,7 @@ interface ITelephonyRegistry { void notifyAllowedNetworkTypesChanged(in int phoneId, in int subId, in int reason, in long allowedNetworkType); void notifyLinkCapacityEstimateChanged(in int phoneId, in int subId, in List<LinkCapacityEstimate> linkCapacityEstimateList); + void notifySimultaneousCellularCallingSubscriptionsChanged(in int[] subIds); void addCarrierPrivilegesCallback( int phoneId, ICarrierPrivilegesCallback callback, String pkg, String featureId); diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index 5b95ee79f330..f6fe3dd842da 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -61,13 +61,12 @@ static jboolean android_hardware_OverlayProperties_supportFp16ForHdr(JNIEnv* env static_cast<int32_t>(HAL_PIXEL_FORMAT_RGBA_FP16)) != i.pixelFormats.end() && std::find(i.standards.begin(), i.standards.end(), - static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT2020)) != + static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT709)) != i.standards.end() && std::find(i.transfers.begin(), i.transfers.end(), - static_cast<int32_t>(HAL_DATASPACE_TRANSFER_ST2084)) != - i.transfers.end() && + static_cast<int32_t>(HAL_DATASPACE_TRANSFER_SRGB)) != i.transfers.end() && std::find(i.ranges.begin(), i.ranges.end(), - static_cast<int32_t>(HAL_DATASPACE_RANGE_FULL)) != i.ranges.end()) { + static_cast<int32_t>(HAL_DATASPACE_RANGE_EXTENDED)) != i.ranges.end()) { return true; } } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 28adccd6b0a1..5be29a6d68b8 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -57,7 +57,6 @@ <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item> <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item> <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item> - <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item> @@ -103,7 +102,6 @@ <string translatable="false" name="status_bar_call_strength">call_strength</string> <string translatable="false" name="status_bar_sensors_off">sensors_off</string> <string translatable="false" name="status_bar_screen_record">screen_record</string> - <string translatable="false" name="status_bar_oem_satellite">satellite</string> <!-- Flag indicating whether the surface flinger has limited alpha compositing functionality in hardware. If set, the window diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef12d8f4ac0f..d12ef2b95f06 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3198,7 +3198,6 @@ <java-symbol type="string" name="status_bar_camera" /> <java-symbol type="string" name="status_bar_sensors_off" /> <java-symbol type="string" name="status_bar_screen_record" /> - <java-symbol type="string" name="status_bar_oem_satellite" /> <!-- Locale picker --> <java-symbol type="id" name="locale_search_menu" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 531756ef2302..e18de2e66399 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -30,6 +30,8 @@ filegroup { android_test { name: "FrameworksCoreTests", + // FrameworksCoreTestsRavenwood references the .aapt.srcjar + use_resource_processor: false, srcs: [ "src/**/*.java", diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 917a30061aca..3a778c314606 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -535,6 +535,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/ActivityStarter.java" }, + "-1583619037": { + "message": "Failed to register MediaProjectionWatcherCallback", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/ScreenRecordingCallbackController.java" + }, "-1582845629": { "message": "Starting animation on %s", "level": "VERBOSE", @@ -2983,12 +2989,6 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "466506262": { - "message": "Clear freezing of %s: visible=%b freezing=%b", - "level": "VERBOSE", - "group": "WM_DEBUG_ORIENTATION", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "485170982": { "message": "Not finishing noHistory %s on stop because we're just sleeping", "level": "DEBUG", diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index c5a2f983ae00..f6ba103f6f05 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -65,8 +65,6 @@ public class Paint { private long mNativeShader; private long mNativeColorFilter; - private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric"); - // Use a Holder to allow static initialization of Paint in the boot image. private static class NoImagePreloadHolder { public static final NativeAllocationRegistry sRegistry = @@ -3393,13 +3391,8 @@ public class Paint { return 0.0f; } - if (sIsRobolectric) { - return nGetRunCharacterAdvance(mNativePaint, text, start, end, - contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds); - } else { - return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd, - isRtl, offset, advances, advancesIndex, drawBounds, runInfo); - } + return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd, + isRtl, offset, advances, advancesIndex, drawBounds, runInfo); } /** diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index ddae673e1084..b21bf11088e2 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -23,9 +23,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.compat.CompatChanges; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; +import android.app.ActivityThread; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; @@ -43,15 +41,6 @@ import java.util.Objects; * line-break property</a> for more information. */ public final class LineBreakConfig implements Parcelable { - - /** - * A feature ID for automatic line break word style. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - public static final long WORD_STYLE_AUTO = 280005585L; - /** * No hyphenation preference is specified. * @@ -487,8 +476,15 @@ public final class LineBreakConfig implements Parcelable { * @hide */ public static @LineBreakStyle int getResolvedLineBreakStyle(@Nullable LineBreakConfig config) { - final int defaultStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_STYLE_AUTO : LINE_BREAK_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultStyle = LINE_BREAK_STYLE_AUTO; + } else { + defaultStyle = LINE_BREAK_STYLE_NONE; + } if (config == null) { return defaultStyle; } @@ -515,8 +511,15 @@ public final class LineBreakConfig implements Parcelable { */ public static @LineBreakWordStyle int getResolvedLineBreakWordStyle( @Nullable LineBreakConfig config) { - final int defaultWordStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_WORD_STYLE_AUTO : LINE_BREAK_WORD_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultWordStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultWordStyle = LINE_BREAK_WORD_STYLE_AUTO; + } else { + defaultWordStyle = LINE_BREAK_WORD_STYLE_NONE; + } if (config == null) { return defaultWordStyle; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 12114519d086..bd8ce803c591 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -26,6 +26,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; import android.view.TouchDelegate; import android.view.View; import android.view.ViewTreeObserver; @@ -74,10 +75,6 @@ public class BubbleBarLayerView extends FrameLayout private DismissView mDismissView; private @Nullable Consumer<String> mUnBubbleConversationCallback; - // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. - /** Whether the expanded view is displaying on the left of the screen or not. */ - private boolean mOnLeft = false; - /** Whether a bubble is expanded. */ private boolean mIsExpanded = false; @@ -154,10 +151,10 @@ public class BubbleBarLayerView extends FrameLayout return mIsExpanded; } - // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done. + // TODO(b/313661121) - when dragging is implemented, check user setting first /** Whether the expanded view is positioned on the left or right side of the screen. */ public boolean isOnLeft() { - return mOnLeft; + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; } /** Shows the expanded view of the provided bubble. */ @@ -216,7 +213,7 @@ public class BubbleBarLayerView extends FrameLayout return Unit.INSTANCE; }); - addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); + addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); } if (mEducationViewController.isEducationVisible()) { @@ -311,7 +308,7 @@ public class BubbleBarLayerView extends FrameLayout lp.width = width; lp.height = height; mExpandedView.setLayoutParams(lp); - if (mOnLeft) { + if (isOnLeft()) { mExpandedView.setX(mPositioner.getInsets().left + padding); } else { mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index eebf8aabd89c..b40b73c111d0 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -716,7 +716,6 @@ cc_test { ], shared_libs: [ "libmemunreachable", - "server_configurable_flags", ], srcs: [ "tests/unit/main.cpp", diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index d4af0872e31e..a5a841e07d7a 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -23,6 +23,7 @@ #include <mutex> +#include "Properties.h" #include "utils/Macros.h" namespace android { @@ -60,7 +61,13 @@ public: static void setWideColorDataspace(ADataSpace dataspace); static void setSupportFp16ForHdr(bool supportFp16ForHdr); - static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + static bool isSupportFp16ForHdr() { + if (!Properties::hdr10bitPlus) { + return false; + } + + return get()->mSupportFp16ForHdr; + }; static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index 16de21def0e3..71f7926930fc 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -379,7 +379,7 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { case kAlpha_8_SkColorType: formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM; - formatInfo.format = GL_R8; + formatInfo.format = GL_RED; formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8_UNORM; break; diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index d58c872dbc56..755332ff66fd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -38,6 +38,9 @@ namespace hwui_flags { constexpr bool clip_surfaceviews() { return false; } +constexpr bool hdr_10bit_plus() { + return false; +} } // namespace hwui_flags #endif @@ -105,6 +108,7 @@ bool Properties::isSystemOrPersistent = false; float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; +bool Properties::hdr10bitPlus = false; StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; @@ -177,6 +181,7 @@ bool Properties::load() { clipSurfaceViews = base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); + hdr10bitPlus = hwui_flags::hdr_10bit_plus(); return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index b956facf6f90..ec53070f6cb8 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -336,6 +336,7 @@ public: static float maxHdrHeadroomOn8bit; static bool clipSurfaceViews; + static bool hdr10bitPlus; static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 58d9d8b9def3..286f06a6bad8 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -578,16 +578,6 @@ namespace PaintGlue { return result; } - // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests. - static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric( - JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end, - jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances, - jint advancesIndex, jobject drawBounds) { - return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end, - contextStart, contextEnd, isRtl, offset, - advances, advancesIndex, drawBounds, nullptr); - } - static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[], jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) { minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; @@ -1163,8 +1153,6 @@ static const JNINativeMethod methods[] = { {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F", (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F}, - {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F", - (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric}, {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I}, {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V", (void*)PaintGlue::getFontMetricsIntForText___C}, diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index e0f1f6ef44be..326b6ed77fe0 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -650,9 +650,14 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); break; case ColorMode::Hdr: - mSurfaceColorType = SkColorType::kN32_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeRGB( - GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + if (DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorType = SkColorType::kN32_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } break; case ColorMode::Hdr10: mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; @@ -669,8 +674,13 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { void SkiaPipeline::setTargetSdrHdrRatio(float ratio) { if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { mTargetSdrHdrRatio = ratio; - mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio), - SkNamedGamut::kDisplayP3); + + if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } } else { mTargetSdrHdrRatio = 1.f; } diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index facf30b83b07..2904dfe76f40 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -441,22 +441,32 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, colorMode = ColorMode::Default; } - if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { + // TODO: maybe we want to get rid of the WCG check if overlay properties just works? + const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() || + DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType; + + if (canUseFp16) { if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { colorMode = ColorMode::Default; } else { config = mEglConfigF16; } } + if (EglExtensions.glColorSpace) { attribs[0] = EGL_GL_COLORSPACE_KHR; switch (colorMode) { case ColorMode::Default: attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; break; + case ColorMode::Hdr: + if (canUseFp16) { + attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; + break; + // No fp16 support so fallthrough to HDR10 + } // We don't have an EGL colorspace for extended range P3 that's used for HDR // So override it after configuring the EGL context - case ColorMode::Hdr: case ColorMode::Hdr10: overrideWindowDataSpaceForHdr = true; attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 8ce1b6d5264d..3d927d36a369 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -121,7 +121,7 @@ interface IMediaProjectionManager { @EnforcePermission("MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void addCallback(IMediaProjectionWatcherCallback callback); + MediaProjectionInfo addCallback(IMediaProjectionWatcherCallback callback); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java index ff608565d2e5..c82039297d6e 100644 --- a/media/java/android/media/projection/MediaProjectionInfo.java +++ b/media/java/android/media/projection/MediaProjectionInfo.java @@ -16,6 +16,7 @@ package android.media.projection; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -26,15 +27,18 @@ import java.util.Objects; public final class MediaProjectionInfo implements Parcelable { private final String mPackageName; private final UserHandle mUserHandle; + private final IBinder mLaunchCookie; - public MediaProjectionInfo(String packageName, UserHandle handle) { + public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) { mPackageName = packageName; mUserHandle = handle; + mLaunchCookie = launchCookie; } public MediaProjectionInfo(Parcel in) { mPackageName = in.readString(); mUserHandle = UserHandle.readFromParcel(in); + mLaunchCookie = in.readStrongBinder(); } public String getPackageName() { @@ -45,12 +49,16 @@ public final class MediaProjectionInfo implements Parcelable { return mUserHandle; } + public IBinder getLaunchCookie() { + return mLaunchCookie; + } + @Override public boolean equals(Object o) { - if (o instanceof MediaProjectionInfo) { - final MediaProjectionInfo other = (MediaProjectionInfo) o; + if (o instanceof MediaProjectionInfo other) { return Objects.equals(other.mPackageName, mPackageName) - && Objects.equals(other.mUserHandle, mUserHandle); + && Objects.equals(other.mUserHandle, mUserHandle) + && Objects.equals(other.mLaunchCookie, mLaunchCookie); } return false; } @@ -64,7 +72,8 @@ public final class MediaProjectionInfo implements Parcelable { public String toString() { return "MediaProjectionInfo{mPackageName=" + mPackageName + ", mUserHandle=" - + mUserHandle + "}"; + + mUserHandle + ", mLaunchCookie" + + mLaunchCookie + "}"; } @Override @@ -76,6 +85,7 @@ public final class MediaProjectionInfo implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(mPackageName); UserHandle.writeToParcel(mUserHandle, out); + out.writeStrongBinder(mLaunchCookie); } public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR = diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index db0195056074..7f8f1a3f22ef 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -29,6 +30,7 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.media.tv.flags.Flags; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -2540,9 +2542,9 @@ public final class TvContract { * <p>This is used to indicate the broadcast visibility type defined in the underlying * broadcast standard or country/operator profile, if applicable. For example, * {@code visible_service_flag} and {@code numeric_selection_flag} of - * {@code service_attribute_descriptor} in D-Book, {@code visible_service_flag} and - * {@code selectable_service_flag} of {@code ciplus_service_descriptor} in CI Plus 1.3 - * specification. + * {@code service_attribute_descriptor} in D-Book, the specification for UK-based TV + * products, {@code visible_service_flag} and {@code selectable_service_flag} of + * {@code ciplus_service_descriptor} in the CI Plus 1.3 specification. * * <p>The value should match one of the following: * {@link #BROADCAST_VISIBILITY_TYPE_VISIBLE}, @@ -2553,8 +2555,8 @@ public final class TvContract { * by default. * * <p>Type: INTEGER - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; /** @hide */ @@ -2571,8 +2573,8 @@ public final class TvContract { * visible from users and selectable by users via normal service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; /** @@ -2581,18 +2583,18 @@ public final class TvContract { * the logical channel number. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; /** * The broadcast visibility type for invisible services. Use this type when the service - * is invisible from users and unselectable by users via any of normal service navigation - * mechanisms. + * is invisible from users and not able to be selected by users via any of the normal + * service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; private Channels() {} diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java index 78d7d7631145..2ebb19a7767e 100644 --- a/media/java/android/media/tv/TvTrackInfo.java +++ b/media/java/android/media/tv/TvTrackInfo.java @@ -55,6 +55,27 @@ public final class TvTrackInfo implements Parcelable { */ public static final int TYPE_SUBTITLE = 2; + /** + * The component tag identifies a component carried by a MPEG-2 TS. + * + * This corresponds to the component_tag in the component descriptor in the + * Elementary Stream loop of the stream in the Program Map Table + * (PMT) [EN 300 468], or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_COMPONENT_TAG = "component_tag"; + + /** + * The MPEG Program ID (PID) of the component in the MPEG2-TS in + * which it is carried, or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_PID = "pid"; + private final int mType; private final String mId; private final String mLanguage; diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl new file mode 100644 index 000000000000..34d96b374a35 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdClient.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.view.InputChannel; + +/** + * Interface a client of the ITvAdManager implements, to identify itself and receive + * information about changes to the state of each TV AD service. + * @hide + */ +oneway interface ITvAdClient { + void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq); + void onSessionReleased(int seq); + void onLayoutSurface(int left, int top, int right, int bottom, int seq); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index 92cc923dc9ab..a747e4995869 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,10 +16,25 @@ package android.media.tv.ad; +import android.media.tv.ad.ITvAdClient; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.TvAdServiceInfo; +import android.view.Surface; + /** * Interface to the TV AD service. * @hide */ interface ITvAdManager { + List<TvAdServiceInfo> getTvAdServiceList(int userId); + void createSession( + in ITvAdClient client, in String serviceId, in String type, int seq, int userId); + void releaseSession(in IBinder sessionToken, int userId); void startAdService(in IBinder sessionToken, int userId); + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height, + int userId); + + void registerCallback(in ITvAdManagerCallback callback, int userId); + void unregisterCallback(in ITvAdManagerCallback callback, int userId); } diff --git a/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl new file mode 100644 index 000000000000..f55f67e616db --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +/** + * Interface to receive callbacks from ITvAdManager regardless of sessions. + * @hide + */ +oneway interface ITvAdManagerCallback { + void onAdServiceAdded(in String serviceId); + void onAdServiceRemoved(in String serviceId); + void onAdServiceUpdated(in String serviceId); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdService.aidl b/media/java/android/media/tv/ad/ITvAdService.aidl new file mode 100644 index 000000000000..3bb04097abca --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdService.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSessionCallback; +import android.os.Bundle; +import android.view.InputChannel; + +/** + * Top-level interface to a TV AD component (implemented in a Service). It's used for + * TvAdManagerService to communicate with TvAdService. + * @hide + */ +oneway interface ITvAdService { + void registerCallback(in ITvAdServiceCallback callback); + void unregisterCallback(in ITvAdServiceCallback callback); + void createSession(in InputChannel channel, in ITvAdSessionCallback callback, + in String serviceId, in String type); + void sendAppLinkCommand(in Bundle command); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl new file mode 100644 index 000000000000..a087181e097d --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +/** + * Helper interface for ITvAdService to allow the TvAdService to notify the TvAdManagerService. + * @hide + */ +oneway interface ITvAdServiceCallback { +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index b834f1b9fb92..751257ce4d4e 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,10 +16,15 @@ package android.media.tv.ad; +import android.view.Surface; + /** - * Sub-interface of ITvAdService which is created per session and has its own context. + * Sub-interface of ITvAdService.aidl which is created per session and has its own context. * @hide */ oneway interface ITvAdSession { + void release(); void startAdService(); + void setSurface(in Surface surface); + void dispatchSurfaceChanged(int format, int width, int height); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl new file mode 100644 index 000000000000..f21ef198ab01 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.media.tv.ad.ITvAdSession; + +/** + * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is + * a related event. + * @hide + */ +oneway interface ITvAdSessionCallback { + void onSessionCreated(in ITvAdSession session); + void onLayoutSurface(int left, int top, int right, int bottom); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java new file mode 100644 index 000000000000..4df2783f6511 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.content.Context; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +/** + * Implements the internal ITvAdSession interface. + * @hide + */ +public class ITvAdSessionWrapper + extends ITvAdSession.Stub implements HandlerCaller.Callback { + + private static final String TAG = "ITvAdSessionWrapper"; + + private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 1000; + private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000; + private static final int DO_RELEASE = 1; + private static final int DO_SET_SURFACE = 2; + private static final int DO_DISPATCH_SURFACE_CHANGED = 3; + + private final HandlerCaller mCaller; + private TvAdService.Session mSessionImpl; + private InputChannel mChannel; + private TvAdEventReceiver mReceiver; + + public ITvAdSessionWrapper( + Context context, TvAdService.Session mSessionImpl, InputChannel channel) { + this.mSessionImpl = mSessionImpl; + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mChannel = channel; + if (channel != null) { + mReceiver = new TvAdEventReceiver(channel, context.getMainLooper()); + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + + @Override + public void executeMessage(Message msg) { + if (mSessionImpl == null) { + return; + } + + long startTime = System.nanoTime(); + switch (msg.what) { + case DO_RELEASE: { + mSessionImpl.release(); + mSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + break; + } + case DO_SET_SURFACE: { + mSessionImpl.setSurface((Surface) msg.obj); + break; + } + case DO_DISPATCH_SURFACE_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.dispatchSurfaceChanged( + (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3); + args.recycle(); + break; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + break; + } + } + long durationMs = (System.nanoTime() - startTime) / (1000 * 1000); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) { + Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration=" + + durationMs + "ms)"); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) { + // TODO: handle timeout + } + } + + } + + @Override + public void startAdService() throws RemoteException { + + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public void dispatchSurfaceChanged(int format, int width, int height) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); + } + + private final class TvAdEventReceiver extends InputEventReceiver { + TvAdEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mSessionImpl.dispatchInputEvent(event, this); + if (handled != TvAdManager.Session.DISPATCH_IN_PROGRESS) { + finishInputEvent( + event, handled == TvAdManager.Session.DISPATCH_HANDLED); + } + } + } +} diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 2b52c4b107b6..9c7505197dec 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -17,12 +17,30 @@ package android.media.tv.ad; import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.media.tv.TvInputManager; import android.media.tv.flags.Flags; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.Pools; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; +import android.view.Surface; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; /** * Central system API to the overall client-side TV AD architecture, which arbitrates interaction @@ -37,10 +55,163 @@ public class TvAdManager { private final ITvAdManager mService; private final int mUserId; + // A mapping from the sequence number of a session to its SessionCallbackRecord. + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = + new SparseArray<>(); + + // @GuardedBy("mLock") + private final List<TvAdServiceCallbackRecord> mCallbackRecords = new ArrayList<>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCallbackRecordMap}. + private int mNextSeq; + + private final Object mLock = new Object(); + private final ITvAdClient mClient; + /** @hide */ public TvAdManager(ITvAdManager service, int userId) { mService = service; mUserId = userId; + mClient = new ITvAdClient.Stub() { + @Override + public void onSessionCreated(String serviceId, IBinder token, InputChannel channel, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(token, channel, mService, mUserId, seq, + mSessionCallbackRecordMap); + } else { + mSessionCallbackRecordMap.delete(seq); + } + record.postSessionCreated(session); + } + } + + @Override + public void onSessionReleased(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + mSessionCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq:" + seq); + return; + } + record.mSession.releaseInternal(); + record.postSessionReleased(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postLayoutSurface(left, top, right, bottom); + } + } + + }; + + ITvAdManagerCallback managerCallback = + new ITvAdManagerCallback.Stub() { + @Override + public void onAdServiceAdded(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceAdded(serviceId); + } + } + } + + @Override + public void onAdServiceRemoved(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceRemoved(serviceId); + } + } + } + + @Override + public void onAdServiceUpdated(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceUpdated(serviceId); + } + } + } + }; + try { + if (mService != null) { + mService.registerCallback(managerCallback, mUserId); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the complete list of TV AD service on the system. + * + * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta + * information. + * @hide + */ + @NonNull + public List<TvAdServiceInfo> getTvAdServiceList() { + try { + return mService.getTvAdServiceList(mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a {@link Session} for a given TV AD service. + * + * <p>The number of sessions that can be created at the same time is limited by the capability + * of the given AD service. + * + * @param serviceId The ID of the AD service. + * @param callback A callback used to receive the created session. + * @param handler A {@link Handler} that the session creation will be delivered to. + * @hide + */ + public void createSession( + @NonNull String serviceId, + @NonNull String type, + @NonNull final TvAdManager.SessionCallback callback, + @NonNull Handler handler) { + createSessionInternal(serviceId, type, callback, handler); + } + + private void createSessionInternal(String serviceId, String type, + TvAdManager.SessionCallback callback, Handler handler) { + Preconditions.checkNotNull(serviceId); + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(handler); + TvAdManager.SessionCallbackRecord + record = new TvAdManager.SessionCallbackRecord(callback, handler); + synchronized (mSessionCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, serviceId, type, seq, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -48,14 +219,121 @@ public class TvAdManager { * @hide */ public static final class Session { - private final IBinder mToken; + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; private final ITvAdManager mService; private final int mUserId; + private final int mSeq; + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; - private Session(IBinder token, ITvAdManager service, int userId) { + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private TvInputManager.Session mInputSession; + private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20); + private TvInputEventSender mSender; + private InputChannel mInputChannel; + private IBinder mToken; + + private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId, + int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { mToken = token; + mInputChannel = channel; mService = service; mUserId = userId; + mSeq = seq; + mSessionCallbackRecordMap = sessionCallbackRecordMap; + } + + /** + * Releases this session. + */ + public void release() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + releaseInternal(); + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render AD. + */ + public void setSurface(Surface surface) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Notifies of any structural changes (format or size) of the surface passed in + * {@link #setSurface}. + * + * @param format The new PixelFormat of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void dispatchSurfaceChanged(int format, int width, int height) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void releaseInternal() { + mToken = null; + synchronized (mHandler) { + if (mInputChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mInputChannel.dispose(); + mInputChannel = null; + } + } + synchronized (mSessionCallbackRecordMap) { + mSessionCallbackRecordMap.delete(mSeq); + } } void startAdService() { @@ -69,5 +347,324 @@ public class TvAdManager { throw e.rethrowFromSystemServer(); } } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mEventHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mEventHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mInputChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for session to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token A token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + void onFinishedInputEvent(Object token, boolean handled); + } + + private final class TvInputEventSender extends InputEventSender { + TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mEventToken; + public Session.FinishedInputEventCallback mCallback; + public Handler mEventHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mEventToken = null; + mCallback = null; + mEventHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mEventToken, mHandled); + + synchronized (mEventHandler) { + recyclePendingEventLocked(this); + } + } + } + } + + + /** + * Interface used to receive the created session. + * @hide + */ + public abstract static class SessionCallback { + /** + * This is called after {@link TvAdManager#createSession} has been processed. + * + * @param session A {@link TvAdManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + public void onSessionCreated(@Nullable Session session) { + } + + /** + * This is called when {@link TvAdManager.Session} is released. + * This typically happens when the process hosting the session has crashed or been killed. + * + * @param session the {@link TvAdManager.Session} instance released. + */ + public void onSessionReleased(@NonNull Session session) { + } + + /** + * This is called when {@link TvAdService.Session#layoutSurface} is called to + * change the layout of surface. + * + * @param session A {@link TvAdManager.Session} associated with this callback. + * @param left Left position. + * @param top Top position. + * @param right Right position. + * @param bottom Bottom position. + */ + public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { + } + + } + + /** + * Callback used to monitor status of the TV AD service. + * @hide + */ + public abstract static class TvAdServiceCallback { + /** + * This is called when a TV AD service is added to the system. + * + * <p>Normally it happens when the user installs a new TV AD service package that implements + * {@link TvAdService} interface. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceAdded(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is removed from the system. + * + * <p>Normally it happens when the user uninstalls the previously installed TV AD service + * package. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceRemoved(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is updated on the system. + * + * <p>Normally it happens when a previously installed TV AD service package is re-installed + * or a newer version of the package exists becomes available/unavailable. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceUpdated(@NonNull String serviceId) { + } + + } + + private static final class SessionCallbackRecord { + private final SessionCallback mSessionCallback; + private final Handler mHandler; + private Session mSession; + + SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) { + mSessionCallback = sessionCallback; + mHandler = handler; + } + + void postSessionCreated(final Session session) { + mSession = session; + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionCreated(session); + } + }); + } + + void postSessionReleased() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionReleased(mSession); + } + }); + } + + void postLayoutSurface(final int left, final int top, final int right, + final int bottom) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom); + } + }); + } + } + + private static final class TvAdServiceCallbackRecord { + private final TvAdServiceCallback mCallback; + private final Executor mExecutor; + + TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public TvAdServiceCallback getCallback() { + return mCallback; + } + + public void postAdServiceAdded(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceAdded(serviceId); + } + }); + } + + public void postAdServiceRemoved(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceRemoved(serviceId); + } + }); + } + + public void postAdServiceUpdated(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceUpdated(serviceId); + } + }); + } } } diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 6897a78647c2..699570397e34 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -16,8 +16,37 @@ package android.media.tv.ad; +import android.annotation.CallSuper; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.os.SomeArgs; + +import java.util.ArrayList; +import java.util.List; /** * The TvAdService class represents a TV client-side advertisement service. @@ -36,9 +65,123 @@ public abstract class TvAdService extends Service { public static final String SERVICE_META_DATA = "android.media.tv.ad.service"; /** + * This is the interface name that a service implementing a TV AD service should + * say that it supports -- that is, this is the action it uses for its intent filter. To be + * supported, the service must also require the + * android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other + * applications cannot abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService"; + + private final Handler mServiceHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>(); + + @Override + @Nullable + public final IBinder onBind(@NonNull Intent intent) { + ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() { + @Override + public void registerCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + } + } + + @Override + public void unregisterCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(InputChannel channel, ITvAdSessionCallback cb, + String serviceId, String type) { + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + args.arg3 = serviceId; + args.arg4 = type; + mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args) + .sendToTarget(); + } + + @Override + public void sendAppLinkCommand(Bundle command) { + onAppLinkCommand(command); + } + }; + return tvAdServiceBinder; + } + + /** + * Called when app link command is received. + */ + public void onAppLinkCommand(@NonNull Bundle command) { + } + + + /** + * Returns a concrete implementation of {@link Session}. + * + * <p>May return {@code null} if this TV AD service fails to create a session for some + * reason. + * + * @param serviceId The ID of the TV AD associated with the session. + * @param type The type of the TV AD associated with the session. + */ + @Nullable + public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type); + + /** * Base class for derived classes to implement to provide a TV AD session. */ public abstract static class Session implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + + private final Object mLock = new Object(); + // @GuardedBy("mLock") + private ITvAdSessionCallback mSessionCallback; + // @GuardedBy("mLock") + private final List<Runnable> mPendingActions = new ArrayList<>(); + private final Context mContext; + final Handler mHandler; + private final WindowManager mWindowManager; + private Surface mSurface; + + + /** + * Creates a new Session. + * + * @param context The context of the application + */ + public Session(@NonNull Context context) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mHandler = new Handler(context.getMainLooper()); + } + + /** + * Releases TvAdService session. + */ + public abstract void onRelease(); + + void release() { + onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + synchronized (mLock) { + mSessionCallback = null; + mPendingActions.clear(); + } + } + /** * Starts TvAdService session. */ @@ -48,21 +191,264 @@ public abstract class TvAdService extends Service { void startAdService() { onStartAdService(); } - } - /** - * Implements the internal ITvAdService interface. - */ - public static class ITvAdSessionWrapper extends ITvAdSession.Stub { - private final Session mSessionImpl; + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { + return false; + } - public ITvAdSessionWrapper(Session mSessionImpl) { - this.mSessionImpl = mSessionImpl; + @Override + public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) { + return false; } @Override - public void startAdService() { - mSessionImpl.startAdService(); + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + return false; } + + /** + * Implement this method to handle touch screen motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position + * is relative to the overlay view that sits on top of this surface. + * + * @param left Left position in pixels, relative to the overlay view. + * @param top Top position in pixels, relative to the overlay view. + * @param right Right position in pixels, relative to the overlay view. + * @param bottom Bottom position in pixels, relative to the overlay view. + */ + @CallSuper + public void layoutSurface(final int left, final int top, final int right, + final int bottom) { + if (left > right || top > bottom) { + throw new IllegalArgumentException("Invalid parameter"); + } + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + + ", r=" + right + ", b=" + bottom + ",)"); + } + if (mSessionCallback != null) { + mSessionCallback.onLayoutSurface(left, top, right, bottom); + } + } catch (RemoteException e) { + Log.w(TAG, "error in layoutSurface", e); + } + } + }); + } + + /** + * Called when the application sets the surface. + * + * <p>The TV AD service should render AD UI onto the given surface. When called with + * {@code null}, the AD service should immediately free any references to the currently set + * surface and stop using it. + * + * @param surface The surface to be used for AD UI rendering. Can be {@code null}. + * @return {@code true} if the surface was set successfully, {@code false} otherwise. + */ + public abstract boolean onSetSurface(@Nullable Surface surface); + + /** + * Called after any structural changes (format or size) have been made to the surface passed + * in {@link #onSetSurface}. This method is always called at least once, after + * {@link #onSetSurface} is called with non-null surface. + * + * @param format The new {@link PixelFormat} of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) { + } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.dispatch(this, mDispatcherState, this)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + + // TODO: special handlings of navigation keys and media keys + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } + } + // TODO: handle overlay view + return TvAdManager.Session.DISPATCH_NOT_HANDLED; + } + + + private void initialize(ITvAdSessionCallback callback) { + synchronized (mLock) { + mSessionCallback = callback; + for (Runnable runnable : mPendingActions) { + runnable.run(); + } + mPendingActions.clear(); + } + } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = surface; + // TODO: Handle failure. + } + + /** + * Calls {@link #onSurfaceChanged}. + */ + void dispatchSurfaceChanged(int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width + + ", height=" + height + ")"); + } + onSurfaceChanged(format, width, height); + } + + private void executeOrPostRunnableOnMainThread(Runnable action) { + synchronized (mLock) { + if (mSessionCallback == null) { + // The session is not initialized yet. + mPendingActions.add(action); + } else { + if (mHandler.getLooper().isCurrentThread()) { + action.run(); + } else { + // Posts the runnable if this is not called from the main thread + mHandler.post(action); + } + } + } + } + } + + + @SuppressLint("HandlerLeak") + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_NOTIFY_SESSION_CREATED = 2; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2; + String serviceId = (String) args.arg3; + String type = (String) args.arg4; + args.recycle(); + TvAdService.Session sessionImpl = onCreateSession(serviceId, type); + if (sessionImpl == null) { + try { + // Failed to create a session. + cb.onSessionCreated(null); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + return; + } + ITvAdSession stub = + new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel); + + SomeArgs someArgs = SomeArgs.obtain(); + someArgs.arg1 = sessionImpl; + someArgs.arg2 = stub; + someArgs.arg3 = cb; + mServiceHandler.obtainMessage( + DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget(); + return; + } + case DO_NOTIFY_SESSION_CREATED: { + SomeArgs args = (SomeArgs) msg.obj; + Session sessionImpl = (Session) args.arg1; + ITvAdSession stub = (ITvAdSession) args.arg2; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3; + try { + cb.onSessionCreated(stub); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + if (sessionImpl != null) { + sessionImpl.initialize(cb); + } + args.recycle(); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } } diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java index ed04f1f9058c..45dc89d838d8 100644 --- a/media/java/android/media/tv/ad/TvAdServiceInfo.java +++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Parcel; import android.os.Parcelable; @@ -63,8 +64,7 @@ public final class TvAdServiceInfo implements Parcelable { if (context == null) { throw new IllegalArgumentException("context cannot be null."); } - // TODO: use a constant - Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component); + Intent intent = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); ResolveInfo resolveInfo = context.getPackageManager().resolveService( intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); if (resolveInfo == null) { @@ -80,6 +80,7 @@ public final class TvAdServiceInfo implements Parcelable { mService = resolveInfo; mId = id; + mTypes.addAll(types); } private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) { @@ -147,9 +148,8 @@ public final class TvAdServiceInfo implements Parcelable { ResolveInfo resolveInfo, Context context, List<String> types) { ServiceInfo serviceInfo = resolveInfo.serviceInfo; PackageManager pm = context.getPackageManager(); - // TODO: use constant for the metadata try (XmlResourceParser parser = - serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) { + serviceInfo.loadXmlMetaData(pm, TvAdService.SERVICE_META_DATA)) { if (parser == null) { throw new IllegalStateException( "No " + "android.media.tv.ad.service" @@ -171,7 +171,15 @@ public final class TvAdServiceInfo implements Parcelable { + XML_START_TAG_NAME + " tag for " + serviceInfo.name); } - // TODO: parse attributes + TypedArray sa = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.TvAdService); + CharSequence[] textArr = sa.getTextArray( + com.android.internal.R.styleable.TvAdService_adServiceTypes); + for (CharSequence cs : textArr) { + types.add(cs.toString().toLowerCase()); + } + + sa.recycle(); } catch (IOException | XmlPullParserException e) { throw new IllegalStateException( "Failed reading meta-data for " + serviceInfo.packageName, e); diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 1a3771a9f24c..5e67fe9f697b 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -16,8 +16,20 @@ package android.media.tv.ad; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.util.AttributeSet; import android.util.Log; +import android.util.Xml; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; import android.view.ViewGroup; /** @@ -28,18 +40,166 @@ public class TvAdView extends ViewGroup { private static final String TAG = "TvAdView"; private static final boolean DEBUG = false; - // TODO: create session + private final TvAdManager mTvAdManager; + + private final Handler mHandler = new Handler(); private TvAdManager.Session mSession; + private MySessionCallback mSessionCallback; + + private final AttributeSet mAttrs; + private final int mDefStyleAttr; + private final XmlResourceParser mParser; + + private SurfaceView mSurfaceView; + private Surface mSurface; + + private boolean mSurfaceChanged; + private int mSurfaceFormat; + private int mSurfaceWidth; + private int mSurfaceHeight; + + private boolean mUseRequestedSurfaceLayout; + private int mSurfaceViewLeft; + private int mSurfaceViewRight; + private int mSurfaceViewTop; + private int mSurfaceViewBottom; + + + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + + ", width=" + width + ", height=" + height + ")"); + } + mSurfaceFormat = format; + mSurfaceWidth = width; + mSurfaceHeight = height; + mSurfaceChanged = true; + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + mSurfaceChanged = false; + setSessionSurface(null); + } + }; + + + public TvAdView(@NonNull Context context) { + this(context, null, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + int sourceResId = Resources.getAttributeSetSourceResId(attrs); + if (sourceResId != Resources.ID_NULL) { + Log.d(TAG, "Build local AttributeSet"); + mParser = context.getResources().getXml(sourceResId); + mAttrs = Xml.asAttributeSet(mParser); + } else { + Log.d(TAG, "Use passed in AttributeSet"); + mParser = null; + mAttrs = attrs; + } + mDefStyleAttr = defStyleAttr; + resetSurfaceView(); + mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right + + ", bottom=" + bottom + ",)"); + } + if (mUseRequestedSurfaceLayout) { + mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, + mSurfaceViewBottom); + } else { + mSurfaceView.layout(0, 0, right - left, bottom - top); + } + } - public TvAdView(Context context) { - super(context, /* attrs = */null, /* defStyleAttr = */0); + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); + int width = mSurfaceView.getMeasuredWidth(); + int height = mSurfaceView.getMeasuredHeight(); + int childState = mSurfaceView.getMeasuredState(); + setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), + resolveSizeAndState(height, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { + public void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + mSurfaceView.setVisibility(visibility); + } + + private void resetSurfaceView() { + if (mSurfaceView != null) { + mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); + removeView(mSurfaceView); + } + mSurface = null; + mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { + @Override + protected void updateSurface() { + super.updateSurface(); + }}; + // The surface view's content should be treated as secure all the time. + mSurfaceView.setSecure(true); + mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); + + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.setZOrderMediaOverlay(true); + + addView(mSurfaceView); + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void dispatchSurfaceChanged(int format, int width, int height) { + if (mSession == null) { + return; + } + //mSession.dispatchSurfaceChanged(format, width, height); + } + + /** + * Prepares the AD service of corresponding {@link TvAdService}. + * + * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId(). + */ + public void prepareAdService(@NonNull String serviceId, @NonNull String type) { if (DEBUG) { - Log.d(TAG, - "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)"); + Log.d(TAG, "prepareAdService"); + } + mSessionCallback = new TvAdView.MySessionCallback(serviceId); + if (mTvAdManager != null) { + mTvAdManager.createSession(serviceId, type, mSessionCallback, mHandler); } } @@ -54,4 +214,75 @@ public class TvAdView extends ViewGroup { mSession.startAdService(); } } + + private class MySessionCallback extends TvAdManager.SessionCallback { + final String mServiceId; + + MySessionCallback(String serviceId) { + mServiceId = serviceId; + } + + @Override + public void onSessionCreated(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionCreated()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionCreated - session already created"); + // This callback is obsolete. + if (session != null) { + session.release(); + } + return; + } + mSession = session; + if (session != null) { + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + if (mSurfaceChanged) { + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + } + } else { + // Failed to create + // Todo: forward error to Tv App + mSessionCallback = null; + } + } + + @Override + public void onSessionReleased(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionReleased()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionReleased - session not created"); + return; + } + mSessionCallback = null; + mSession = null; + } + + @Override + public void onLayoutSurface( + TvAdManager.Session session, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" + + right + ", bottom=" + bottom + ",)"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onLayoutSurface - session not created"); + return; + } + mSurfaceViewLeft = left; + mSurfaceViewTop = top; + mSurfaceViewRight = right; + mSurfaceViewBottom = bottom; + mUseRequestedSurfaceLayout = true; + requestLayout(); + } + } } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 77391841c6fe..e3dba03d6093 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -48,6 +48,7 @@ oneway interface ITvInteractiveAppClient { void onRequestCurrentChannelLcn(int seq); void onRequestStreamVolume(int seq); void onRequestTrackInfoList(int seq); + void onRequestSelectedTrackInfo(int seq); void onRequestCurrentTvInputId(int seq); void onRequestTimeShiftMode(int seq); void onRequestAvailableSpeeds(int seq); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 41cbe4ae02d0..4316d053a275 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -102,6 +102,8 @@ interface ITvInteractiveAppManager { int UserId); void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId); void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId); + void sendSelectedTrackInfo(in IBinder sessionToken, in List<TvTrackInfo> tracks, + int userId); void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 052bc3d5adce..ba7cf13a7a1d 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -78,6 +78,7 @@ oneway interface ITvInteractiveAppSession { void notifyBroadcastInfoResponse(in BroadcastInfoResponse response); void notifyAdResponse(in AdResponse response); void notifyAdBufferConsumed(in AdBuffer buffer); + void sendSelectedTrackInfo(in List<TvTrackInfo> tracks); void createMediaView(in IBinder windowToken, in Rect frame); void relayoutMediaView(in Rect frame); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 9e43e79144fd..416b8f12d5ea 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -50,6 +50,7 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestCurrentTvInputId(); void onRequestTimeShiftMode(); void onRequestAvailableSpeeds(); + void onRequestSelectedTrackInfo(); void onRequestStartRecording(in String requestId, in Uri programUri); void onRequestStopRecording(in String recordingId); void onRequestScheduleRecording(in String requestId, in String inputId, in Uri channelUri, diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 253ade809ece..518b08a93f95 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -102,6 +102,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45; private static final int DO_SEND_TIME_SHIFT_MODE = 46; private static final int DO_SEND_AVAILABLE_SPEEDS = 47; + private static final int DO_SEND_SELECTED_TRACK_INFO = 48; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -247,6 +248,10 @@ public class ITvInteractiveAppSessionWrapper args.recycle(); break; } + case DO_SEND_SELECTED_TRACK_INFO: { + mSessionImpl.sendSelectedTrackInfo((List<TvTrackInfo>) msg.obj); + break; + } case DO_NOTIFY_VIDEO_AVAILABLE: { mSessionImpl.notifyVideoAvailable(); break; @@ -526,6 +531,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_SELECTED_TRACK_INFO, tracks)); + } + + @Override public void notifyTracksChanged(List<TvTrackInfo> tracks) { mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_TRACKS_CHANGED, tracks)); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 7cce84a1ee16..bf4379f470d8 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -33,7 +33,6 @@ import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; -import android.media.tv.interactive.TvInteractiveAppService.Session; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -506,6 +505,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestSelectedTrackInfo(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestSelectedTrackInfo(); + } + } + + @Override public void onRequestCurrentTvInputId(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -1209,6 +1220,18 @@ public final class TvInteractiveAppManager { } } + void sendSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendSelectedTrackInfo(mToken, tracks, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + void sendCurrentTvInputId(@Nullable String inputId) { if (mToken == null) { Log.w(TAG, "The session has been already released"); @@ -2108,6 +2131,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestSelectedTrackInfo() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestSelectedTrackInfo(mSession); + } + }); + } + void postRequestCurrentTvInputId() { mHandler.post(new Runnable() { @Override @@ -2378,6 +2410,15 @@ public final class TvInteractiveAppManager { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param session A {@link TvInteractiveAppManager.Session} associated with this callback. + */ + public void onRequestSelectedTrackInfo(Session session) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 241940486a14..5cc86bacf54f 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -932,6 +932,16 @@ public abstract class TvInteractiveAppService extends Service { @NonNull Bundle data) { } + /** + * Called when the TV App sends the selected track info as a response to + * requestSelectedTrackInfo. + * + * @param tracks + * @hide + */ + public void onSelectedTrackInfo(List<TvTrackInfo> tracks) { + } + @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { return false; @@ -1338,6 +1348,30 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests the currently selected {@link TvTrackInfo} from the TV App. + * + * <p> Normally, track info cannot be synchronized until the channel has + * been changed. This is used when the session of the TIAS is newly + * created and the normal synchronization has not happened yet. + * @hide + */ + @CallSuper + public void requestSelectedTrackInfo() { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestSelectedTrackInfo"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestSelectedTrackInfo(); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestSelectedTrackInfo", e); + } + }); + } + + /** * Requests starting of recording * * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to @@ -1781,6 +1815,13 @@ public abstract class TvInteractiveAppService extends Service { onTvMessage(type, data); } + void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")"); + } + onSelectedTrackInfo(tracks); + } + /** * Calls {@link #onAdBufferConsumed}. */ diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index cbaf5e482faa..40a12e4db4cc 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -582,6 +582,20 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Sends the currently selected track info to the TV Interactive App. + * + * @hide + */ + public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "sendSelectedTrackInfo"); + } + if (mSession != null) { + mSession.sendSelectedTrackInfo(tracks); + } + } + + /** * Sends current TV input ID to related TV interactive app. * * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is @@ -1197,6 +1211,16 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param iAppServiceId The ID of the TV interactive app service bound to this view. + * @hide + */ + public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId()} is * called. * @@ -1714,6 +1738,28 @@ public class TvInteractiveAppView extends ViewGroup { } @Override + public void onRequestSelectedTrackInfo(Session session) { + if (DEBUG) { + Log.d(TAG, "onRequestSelectedTrackInfo"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRequestSelectedTrackInfo - session not created"); + return; + } + synchronized (mCallbackLock) { + if (mCallbackExecutor != null) { + mCallbackExecutor.execute(() -> { + synchronized (mCallbackLock) { + if (mCallback != null) { + mCallback.onRequestSelectedTrackInfo(mIAppServiceId); + } + } + }); + } + } + } + + @Override public void onRequestCurrentTvInputId(Session session) { if (DEBUG) { Log.d(TAG, "onRequestCurrentTvInputId"); diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 3fcb8713672f..00b0e57c09ea 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -967,7 +967,7 @@ void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { ScopedLocalRef<jobject> filter(env); { android::Mutex::Autolock autoLock(mLock); - if (env->IsSameObject(filter.get(), nullptr)) { + if (env->IsSameObject(mFilterObj, nullptr)) { ALOGE("FilterClientCallbackImpl::onFilterStatus:" "Filter object has been freed. Ignoring callback."); return; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index b0abf92ffe08..2d442f4c0e6e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1349,6 +1349,26 @@ public class SettingsProvider extends ContentProvider { final int nameCount = names.size(); HashMap<String, String> flagsToValues = new HashMap<>(names.size()); + if (Flags.loadAconfigDefaults()) { + Map<String, Map<String, String>> allDefaults = + settingsState.getAconfigDefaultValues(); + + if (allDefaults != null) { + if (prefix != null) { + String namespace = prefix.substring(0, prefix.length() - 1); + + Map<String, String> namespaceDefaults = allDefaults.get(namespace); + if (namespaceDefaults != null) { + flagsToValues.putAll(namespaceDefaults); + } + } else { + for (Map<String, String> namespaceDefaults : allDefaults.values()) { + flagsToValues.putAll(namespaceDefaults); + } + } + } + } + for (int i = 0; i < nameCount; i++) { String name = names.get(i); Setting setting = settingsState.getSettingLocked(name); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 73c2e22240d3..6f3c88fc8706 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -69,6 +69,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -236,6 +237,10 @@ final class SettingsState { @GuardedBy("mLock") private int mNextHistoricalOpIdx; + @GuardedBy("mLock") + @Nullable + private Map<String, Map<String, String>> mNamespaceDefaults; + public static final int SETTINGS_TYPE_GLOBAL = 0; public static final int SETTINGS_TYPE_SYSTEM = 1; public static final int SETTINGS_TYPE_SECURE = 2; @@ -331,25 +336,21 @@ final class SettingsState { readStateSyncLocked(); if (Flags.loadAconfigDefaults()) { - // Only load aconfig defaults if this is the first boot, the XML - // file doesn't exist yet, or this device is on its first boot after - // an OTA. - boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey) - && (!file.exists() - || mContext.getPackageManager().isDeviceUpgrading()); - if (shouldLoadAconfigValues) { + if (isConfigSettingsKey(mKey)) { loadAconfigDefaultValuesLocked(); } } + } } @GuardedBy("mLock") private void loadAconfigDefaultValuesLocked() { + mNamespaceDefaults = new HashMap<>(); + for (String fileName : sAconfigTextProtoFilesOnDevice) { try (FileInputStream inputStream = new FileInputStream(fileName)) { - byte[] contents = inputStream.readAllBytes(); - loadAconfigDefaultValues(contents); + loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults); } catch (IOException e) { Slog.e(LOG_TAG, "failed to read protobuf", e); } @@ -358,27 +359,21 @@ final class SettingsState { @VisibleForTesting @GuardedBy("mLock") - public void loadAconfigDefaultValues(byte[] fileContents) { + public static void loadAconfigDefaultValues(byte[] fileContents, + @NonNull Map<String, Map<String, String>> defaultMap) { try { - parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); - - if (parsedFlags == null) { - Slog.e(LOG_TAG, "failed to parse aconfig protobuf"); - return; - } - + parsed_flags parsedFlags = + parsed_flags.parseFrom(fileContents); for (parsed_flag flag : parsedFlags.getParsedFlagList()) { - String flagName = flag.getNamespace() + "/" - + flag.getPackage() + "." + flag.getName(); - String value = flag.getState() == flag_state.ENABLED ? "true" : "false"; - - Setting existingSetting = getSettingLocked(flagName); - boolean isDefaultLoaded = existingSetting.getTag() != null - && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG); - if (existingSetting.getValue() == null || isDefaultLoaded) { - insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG, - false, flag.getPackage()); + if (!defaultMap.containsKey(flag.getNamespace())) { + Map<String, String> defaults = new HashMap<>(); + defaultMap.put(flag.getNamespace(), defaults); } + String flagName = flag.getNamespace() + + "/" + flag.getPackage() + "." + flag.getName(); + String flagValue = flag.getState() == flag_state.ENABLED + ? "true" : "false"; + defaultMap.get(flag.getNamespace()).put(flagName, flagValue); } } catch (IOException e) { Slog.e(LOG_TAG, "failed to parse protobuf", e); @@ -443,6 +438,13 @@ final class SettingsState { return names; } + @Nullable + public Map<String, Map<String, String>> getAconfigDefaultValues() { + synchronized (mLock) { + return mNamespaceDefaults; + } + } + // The settings provider must hold its lock when calling here. public Setting getSettingLocked(String name) { if (TextUtils.isEmpty(name)) { diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 24625eaa5e13..e55bbecb67d7 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -30,6 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; public class SettingsStateTest extends AndroidTestCase { public static final String CRAZY_STRING = @@ -93,7 +95,6 @@ public class SettingsStateTest extends AndroidTestCase { SettingsState settingsState = new SettingsState( getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - parsed_flags flags = parsed_flags .newBuilder() .addParsedFlag(parsed_flag @@ -117,18 +118,13 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(2, namespaceDefaults.keySet().size()); - synchronized (lock) { - assertEquals("false", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); - assertEquals("true", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag2").getValue()); + assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1")); + assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2")); } } @@ -150,21 +146,18 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); - synchronized (lock) { - assertEquals(null, - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(null, namespaceDefaults); } } public void testInvalidAconfigProtoDoesNotCrash() { + Map<String, Map<String, String>> defaults = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); - settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes()); + settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults); } public void testIsBinary() { diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index f7b1a26c9df9..7ba889bc8fee 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -36,3 +36,10 @@ flag { description: "Animates the floating menu's transition between curved and jagged edges." bug: "281140482" } + +flag { + name: "create_windowless_window_magnifier" + namespace: "accessibility" + description: "Uses SurfaceControlViewHost to create the magnifier for window magnification." + bug: "280992417" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 91a4d2e01e90..c8e18d7df63a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -38,6 +39,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape @@ -58,17 +60,22 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow @@ -86,6 +93,9 @@ import androidx.compose.ui.window.Popup import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.compose.extensions.allowGestures +import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset +import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.res.R @@ -104,22 +114,59 @@ fun CommunalHub( var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var isDraggingToRemove by remember { mutableStateOf(false) } + val gridState = rememberLazyGridState() + val contentListState = rememberContentListState(communalContent, viewModel) + val reorderingWidgets by viewModel.reorderingWidgets.collectAsState() + val selectedIndex = viewModel.selectedIndex.collectAsState() + val removeButtonEnabled by remember { + derivedStateOf { selectedIndex.value != null || reorderingWidgets } + } + + val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) + val contentOffset = beforeContentPadding(contentPadding).toOffset() Box( modifier = - modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant), + modifier + .fillMaxSize() + .background(LocalAndroidColorScheme.current.outlineVariant) + .pointerInput(gridState, contentOffset, contentListState) { + // If not in edit mode, don't allow selecting items. + if (!viewModel.isEditMode) return@pointerInput + observeTapsWithoutConsuming { offset -> + val adjustedOffset = offset - contentOffset + val index = + gridState.layoutInfo.visibleItemsInfo + .firstItemAtOffset(adjustedOffset) + ?.index + val newIndex = + if (index?.let(contentListState::isItemEditable) == true) { + index + } else { + null + } + viewModel.setSelectedIndex(newIndex) + } + }, ) { CommunalHubLazyGrid( communalContent = communalContent, viewModel = viewModel, - contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize), + contentPadding = contentPadding, + contentOffset = contentOffset, setGridCoordinates = { gridCoordinates = it }, - updateDragPositionForRemove = { + updateDragPositionForRemove = { offset -> isDraggingToRemove = - checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates) + isPointerWithinCoordinates( + offset = gridCoordinates?.let { it.positionInWindow() + offset }, + containerToCheck = removeButtonCoordinates + ) isDraggingToRemove }, onOpenWidgetPicker = onOpenWidgetPicker, + gridState = gridState, + contentListState = contentListState, + selectedIndex = selectedIndex ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -129,6 +176,14 @@ fun CommunalHub( setRemoveButtonCoordinates = { removeButtonCoordinates = it }, onEditDone = onEditDone, onOpenWidgetPicker = onOpenWidgetPicker, + onRemoveClicked = { + selectedIndex.value?.let { index -> + contentListState.onRemove(index) + contentListState.onSaveList() + viewModel.setSelectedIndex(null) + } + }, + removeEnabled = removeButtonEnabled ) } else { IconButton(onClick = viewModel::onOpenWidgetEditor) { @@ -158,16 +213,18 @@ private fun BoxScope.CommunalHubLazyGrid( communalContent: List<CommunalContentModel>, viewModel: BaseCommunalViewModel, contentPadding: PaddingValues, + selectedIndex: State<Int?>, + contentOffset: Offset, + gridState: LazyGridState, + contentListState: ContentListState, setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit, updateDragPositionForRemove: (offset: Offset) -> Boolean, onOpenWidgetPicker: (() -> Unit)? = null, ) { var gridModifier = Modifier.align(Alignment.CenterStart) - val gridState = rememberLazyGridState() var list = communalContent var dragDropState: GridDragDropState? = null if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) { - val contentListState = rememberContentListState(list, viewModel) list = contentListState.list // for drag & drop operations within the communal hub grid dragDropState = @@ -179,7 +236,7 @@ private fun BoxScope.CommunalHubLazyGrid( gridModifier = gridModifier .fillMaxSize() - .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel) + .dragContainer(dragDropState, contentOffset, viewModel) .onGloballyPositioned { setGridCoordinates(it) } // for widgets dropped from other activities val dragAndDropTargetState = @@ -218,8 +275,10 @@ private fun BoxScope.CommunalHubLazyGrid( list[index].size.dp().value, ) if (viewModel.isEditMode && dragDropState != null) { + val selected by remember(index) { derivedStateOf { index == selectedIndex.value } } DraggableItem( dragDropState = dragDropState, + selected = selected, enabled = list[index] is CommunalContentModel.Widget, index = index, size = size @@ -253,11 +312,19 @@ private fun BoxScope.CommunalHubLazyGrid( @Composable private fun Toolbar( isDraggingToRemove: Boolean, + removeEnabled: Boolean, + onRemoveClicked: () -> Unit, setToolbarSize: (toolbarSize: IntSize) -> Unit, setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit, onOpenWidgetPicker: () -> Unit, - onEditDone: () -> Unit, + onEditDone: () -> Unit ) { + val removeButtonAlpha: Float by + animateFloatAsState( + targetValue = if (removeEnabled) 1f else 0.5f, + label = "RemoveButtonAlphaAnimation" + ) + Row( modifier = Modifier.fillMaxWidth() @@ -301,13 +368,18 @@ private fun Toolbar( } } else { OutlinedButton( - // Button is disabled to make it non-clickable - enabled = false, - onClick = {}, - colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary), + enabled = removeEnabled, + onClick = onRemoveClicked, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = colors.primary, + disabledContentColor = colors.primary + ), border = BorderStroke(width = 1.0.dp, color = colors.primary), contentPadding = Dimensions.ButtonPadding, - modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } + modifier = + Modifier.graphicsLayer { alpha = removeButtonAlpha } + .onGloballyPositioned { setRemoveButtonCoordinates(it) } ) { RemoveButtonContent(spacerModifier) } @@ -385,7 +457,7 @@ private fun CommunalContent( ) { when (model) { is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier) - is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size) + is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size) is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, size, modifier) is CommunalContentModel.CtaTileInEditMode -> @@ -396,11 +468,11 @@ private fun CommunalContent( } } -/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */ +/** Creates an empty card used to highlight a particular spot on the grid. */ @Composable -fun WidgetPlaceholderContent(size: SizeF) { +fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) { Card( - modifier = Modifier.size(Dp(size.width), Dp(size.height)), + modifier = modifier.size(Dp(size.width), Dp(size.height)), colors = CardDefaults.cardColors(containerColor = Color.Transparent), border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed), shape = RoundedCornerShape(16.dp) @@ -528,7 +600,7 @@ private fun WidgetContent( contentAlignment = Alignment.Center, ) { AndroidView( - modifier = modifier, + modifier = modifier.allowGestures(allowed = !viewModel.isEditMode), factory = { context -> // The AppWidgetHostView will inherit the interaction handler from the // AppWidgetHost. So set the interaction handler here before creating the view, and @@ -616,8 +688,8 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx { return with(LocalDensity.current) { ContentPaddingInPx( - startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), - topPadding = paddingValues.calculateTopPadding().toPx() + start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), + top = paddingValues.calculateTopPadding().toPx() ) } } @@ -626,18 +698,15 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn * Check whether the pointer position that the item is being dragged at is within the coordinates of * the remove button in the toolbar. Returns true if the item is removable. */ -private fun checkForDraggingToRemove( - offset: Offset, - removeButtonCoordinates: LayoutCoordinates?, - gridCoordinates: LayoutCoordinates?, +private fun isPointerWithinCoordinates( + offset: Offset?, + containerToCheck: LayoutCoordinates? ): Boolean { - if (removeButtonCoordinates == null || gridCoordinates == null) { + if (offset == null || containerToCheck == null) { return false } - val pointer = gridCoordinates.positionInWindow() + offset - val removeButton = removeButtonCoordinates.positionInWindow() - return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width && - pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height + val container = containerToCheck.boundsInWindow() + return container.contains(offset) } private fun CommunalContentSize.dp(): Dp { @@ -648,7 +717,9 @@ private fun CommunalContentSize.dp(): Dp { } } -data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float) +data class ContentPaddingInPx(val start: Float, val top: Float) { + fun toOffset(): Offset = Offset(start, top) +} object Dimensions { val CardWidth = 464.dp diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 979991d7dc2a..45f98b879dd7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel @Composable fun rememberContentListState( communalContent: List<CommunalContentModel>, - viewModel: CommunalEditModeViewModel, + viewModel: BaseCommunalViewModel, ): ContentListState { return remember(communalContent) { ContentListState( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 113822167ca7..a1959532fbb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -17,6 +17,10 @@ package com.android.systemui.communal.ui.compose import android.util.SizeF +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy @@ -32,6 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput @@ -39,6 +44,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex +import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.plus import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import kotlinx.coroutines.CoroutineScope @@ -109,13 +115,10 @@ internal constructor( internal fun onDragStart(offset: Offset, contentOffset: Offset) { state.layoutInfo.visibleItemsInfo - .firstOrNull { item -> - // grid item offset is based off grid content container so we need to deduct - // before content padding from the initial pointer position - contentListState.isItemEditable(item.index) && - (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x && - (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y - } + .filter { item -> contentListState.isItemEditable(item.index) } + // grid item offset is based off grid content container so we need to deduct + // before content padding from the initial pointer position + .firstItemAtOffset(offset - contentOffset) ?.apply { dragStartPointerOffset = offset - this.offset.toOffset() draggingItemIndex = index @@ -148,12 +151,11 @@ internal constructor( val middleOffset = startOffset + (endOffset - startOffset) / 2f val targetItem = - state.layoutInfo.visibleItemsInfo.find { item -> - contentListState.isItemEditable(item.index) && - middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x && - middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y && - draggingItem.index != item.index - } + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .filter { item -> draggingItem.index != item.index } + .firstItemAtOffset(middleOffset) if (targetItem != null) { val scrollToIndex = @@ -208,32 +210,31 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, - beforeContentPadding: ContentPaddingInPx, + contentOffset: Offset, viewModel: BaseCommunalViewModel, ): Modifier { - return pointerInput(dragDropState, beforeContentPadding) { - detectDragGesturesAfterLongPress( - onDrag = { change, offset -> - change.consume() - dragDropState.onDrag(offset = offset) - }, - onDragStart = { offset -> - dragDropState.onDragStart( - offset, - Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding) - ) - viewModel.onReorderWidgetStart() - }, - onDragEnd = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetEnd() - }, - onDragCancel = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetCancel() - } - ) - } + return this.then( + pointerInput(dragDropState, contentOffset) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> + dragDropState.onDragStart(offset, contentOffset) + viewModel.onReorderWidgetStart() + }, + onDragEnd = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetEnd() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetCancel() + } + ) + } + ) } /** Wrap LazyGrid item with additional modifier needed for drag and drop. */ @@ -243,6 +244,7 @@ fun LazyGridItemScope.DraggableItem( dragDropState: GridDragDropState, index: Int, enabled: Boolean, + selected: Boolean, size: SizeF, modifier: Modifier = Modifier, content: @Composable (isDragging: Boolean) -> Unit @@ -250,21 +252,31 @@ fun LazyGridItemScope.DraggableItem( if (!enabled) { return Box(modifier = modifier) { content(false) } } + val dragging = index == dragDropState.draggingItemIndex + val itemAlpha: Float by + animateFloatAsState( + targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f, + label = "DraggableItemAlpha" + ) val draggingModifier = if (dragging) { Modifier.zIndex(1f).graphicsLayer { translationX = dragDropState.draggingItemOffset.x translationY = dragDropState.draggingItemOffset.y - alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f + alpha = itemAlpha } } else { Modifier.animateItemPlacement() } Box(modifier) { - if (dragging) { - WidgetPlaceholderContent(size) + AnimatedVisibility( + visible = (dragging || selected) && !dragDropState.isDraggingToRemove, + enter = fadeIn(), + exit = fadeOut() + ) { + HighlightedItem(size) } Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt new file mode 100644 index 000000000000..132093f034bb --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.toRect + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean { + val boundingBox = IntRect(item.offset, item.size) + return boundingBox.toRect().contains(offset) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt index 252945f1ed6a..b31008e04593 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.dagger +package com.android.systemui.communal.ui.compose.extensions -import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository -import javax.inject.Qualifier +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput -/** Detailed [DeviceBasedSatelliteRepository] logs */ -@Qualifier -@MustBeDocumented -@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) -annotation class OemSatelliteInputLog +/** Sets whether gestures are allowed on children of this element. */ +fun Modifier.allowGestures(allowed: Boolean): Modifier = + if (allowed) { + this + } else { + this.then(pointerInput(Unit) { consumeAllGestures() }) + } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt new file mode 100644 index 000000000000..14074944259b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import kotlinx.coroutines.coroutineScope + +/** + * Observe taps without actually consuming them, so child elements can still respond to them. Long + * presses are excluded. + */ +suspend fun PointerInputScope.observeTapsWithoutConsuming( + pass: PointerEventPass = PointerEventPass.Initial, + onTap: ((Offset) -> Unit)? = null, +) = coroutineScope { + if (onTap == null) return@coroutineScope + awaitEachGesture { + awaitFirstDown(pass = pass) + val tapTimeout = viewConfiguration.longPressTimeoutMillis + val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) } + if (up != null) { + onTap(up.position) + } + } +} + +/** Consume all gestures on the initial pass so that child elements do not receive them. */ +suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { + awaitEachGesture { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 4f7d9444020c..6828041eff5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -21,23 +21,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -46,18 +47,11 @@ import org.junit.runner.RunWith @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyguardTransitionInteractorTest : SysuiTestCase() { - private lateinit var underTest: KeyguardTransitionInteractor - private lateinit var repository: FakeKeyguardTransitionRepository - private val testScope = TestScope() - - @Before - fun setUp() { - repository = FakeKeyguardTransitionRepository() - underTest = KeyguardTransitionInteractorFactory.create( - scope = testScope.backgroundScope, - repository = repository, - ).keyguardTransitionInteractor - } + val kosmos = testKosmos() + + val underTest = kosmos.keyguardTransitionInteractor + val repository = kosmos.fakeKeyguardTransitionRepository + val testScope = kosmos.testScope @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest { @@ -114,49 +108,51 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardStateTests() = testScope.runTest { - val finishedSteps by collectValues(underTest.finishedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun finishedKeyguardStateTests() = + testScope.runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) } - assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) - } - @Test - fun startedKeyguardStateTests() = testScope.runTest { - val startedStates by collectValues(underTest.startedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun startedKeyguardStateTests() = + testScope.runTest { + val startedStates by collectValues(underTest.startedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) } - assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) - } - @Test fun finishedKeyguardTransitionStepTests() = runTest { val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) @@ -178,7 +174,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { // Ignore the default state. assertThat(finishedSteps.subList(1, finishedSteps.size)) - .isEqualTo(listOf(steps[2], steps[5])) + .isEqualTo(listOf(steps[2], steps[5])) } @Test @@ -233,500 +229,1067 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun isInTransitionToState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionToState(GONE)) + fun isInTransitionToAnyState() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, // The repo is seeded with a transition from OFF to LOCKSCREEN. + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToAnyState_finishedStateIsStartedStateAfterCancels() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, + false, + ), + inTransition + ) + + // Start FINISHED in GONE. + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + // We should have been in transition throughout the entire transition, including + // both cancellations, and we should still be in transition despite now + // transitioning to GONE, the state we're also FINISHED in. + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionToState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromState(DOZING)) + fun isInTransitionFromState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromState(DOZING)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromStateWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromStateWhere { - it == DOZING - }) + fun isInTransitionFromStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromStateWhere { it == DOZING }) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionWhere( - fromStatePredicate = { it == DOZING }, - toStatePredicate = { it == GONE }, - )) + fun isInTransitionWhere() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInStateWhere() = testScope.runTest { - val results by collectValues(underTest.isFinishedInStateWhere { it == GONE } ) + fun isInTransitionWhere_withCanceledStep() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, CANCELED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + TransitionStep(GONE, DOZING, 1f, FINISHED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } + + @Test + fun isFinishedInStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInStateWhere { it == GONE }) + + sendSteps( + TransitionStep(AOD, DOZING, 0f, STARTED), + TransitionStep(AOD, DOZING, 0.5f, RUNNING), + TransitionStep(AOD, DOZING, 1f, FINISHED), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0f, RUNNING), + ) - sendSteps( + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) + + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInState() = testScope.runTest { - val results by collectValues(underTest.isFinishedInState(GONE)) + fun isFinishedInState() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest { - val finishedStates by collectValues(underTest.finishedKeyguardState) + fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = + testScope.runTest { + val finishedStates by collectValues(underTest.finishedKeyguardState) - // We default FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN - ), finishedStates) + // We default FINISHED in LOCKSCREEN. + assertEquals(listOf(LOCKSCREEN), finishedStates) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED), - ) + ) - // We're FINISHED in AOD. - assertEquals(listOf( - LOCKSCREEN, - AOD, - ), finishedStates) + // We're FINISHED in AOD. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + ), + finishedStates + ) - // Transition back to LOCKSCREEN. - sendSteps( + // Transition back to LOCKSCREEN. + sendSteps( TransitionStep(AOD, LOCKSCREEN, 0f, STARTED), TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING), TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), - ) + ) - // We're FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We're FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), - ) + ) - // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in - // LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in + // LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED), - ) + ) - // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED), TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING), TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED), - ) - - // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to - // LOCKSCREEN after the cancellation. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - LOCKSCREEN, - ), finishedStates) - } + ) + + // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to + // LOCKSCREEN after the cancellation. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + LOCKSCREEN, + ), + finishedStates + ) + } + + @Test + fun testCurrentState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED), + ) + + // Once CANCELED, we're still currently in LOCKSCREEN... + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED), + ) + + // ...until STARTING back to LOCKSCREEN, at which point the "current" state should be + // the + // one we're transitioning from, despite never FINISHING in that state. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING), + TransitionStep(AOD, LOCKSCREEN, 0.8f, FINISHED), + ) + + // FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + currentStates + ) + } + + @Test + fun testCurrentState_multipleCancellations_backToLastFinishedState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + // Default transition from OFF -> LOCKSCREEN + OFF, + LOCKSCREEN, + // Transitioned to GONE + GONE, + ), + currentStates + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // Current state should not be DOZING until the post-cancelation transition is + // STARTED + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // DOZING -> LS STARTED, DOZING is now the current state. + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + // LS -> GONE STARTED, LS is now the current state. + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + LOCKSCREEN, + // FINISHED in GONE, GONE is now the current state. + GONE, + ), + currentStates + ) + } private suspend fun sendSteps(vararg steps: TransitionStep) { steps.forEach { diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml deleted file mode 100644 index 045c19eb09dc..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml deleted file mode 100644 index 5e012ab7edbd..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillColor="#fff"/> -</vector> - diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml deleted file mode 100644 index d8a9a703260d..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml deleted file mode 100644 index dec9930959a0..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0" - > - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml deleted file mode 100644 index ee4d05c3bda5..000000000000 --- a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<!-- Base layout that provides a single bindable icon_view id image view --> -<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center_vertical" - > - - <ImageView - android:id="@+id/icon_view" - android:layout_height="@dimen/status_bar_bindable_icon_size" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical" - android:padding="@dimen/status_bar_bindable_icon_padding" - android:scaleType="fitCenter" - /> - -</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 9a4520c5fa52..798fc06b44f7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -151,8 +151,6 @@ <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen> <!-- Original dp height of notification icons in the status bar --> <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen> - <dimen name="status_bar_bindable_icon_size">20sp</dimen> - <dimen name="status_bar_bindable_icon_padding">2sp</dimen> <!-- Default horizontal drawable padding for status bar icons. --> <dimen name="status_bar_horizontal_padding">2.5sp</dimen> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index a5a545af641a..033f93b260ab 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -3,6 +3,7 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; +import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -191,11 +192,11 @@ public class KeyguardClockSwitch extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); - mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); - mStatusArea = findViewById(R.id.keyguard_status_area); - + if (!migrateClocksToBlueprint()) { + mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); + mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); + mStatusArea = findViewById(R.id.keyguard_status_area); + } onConfigChanged(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index d372f5a616b1..1758831203d6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -494,7 +494,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV boolean shouldBeCentered, boolean animate) { if (migrateClocksToBlueprint()) { - mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered); + mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } else { mKeyguardClockSwitchController.setSplitShadeCentered( splitShadeEnabled && shouldBeCentered); diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 84708a49f469..4da348e6a92a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.media.controls.ui.MediaHost import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -36,6 +37,15 @@ abstract class BaseCommunalViewModel( val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + /** Whether widgets are currently being re-ordered. */ + open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) + + private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null) + + /** The index of the currently selected item, or null if no item selected. */ + val selectedIndex: StateFlow<Int?> + get() = _selectedIndex + fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) } @@ -105,4 +115,9 @@ abstract class BaseCommunalViewModel( /** Called as the user cancels dragging a widget to reorder. */ open fun onReorderWidgetCancel() {} + + /** Set the index of the currently selected item */ + fun setSelectedIndex(index: Int?) { + _selectedIndex.value = index + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 7faf653cc177..fcad45f950dc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -37,7 +37,10 @@ import javax.inject.Named import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -69,9 +72,15 @@ constructor( // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + // Clear the selected index when the list is updated. + .onEach { setSelectedIndex(null) } + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + + private val _reorderingWidgets = MutableStateFlow(false) + + override val reorderingWidgets: StateFlow<Boolean> + get() = _reorderingWidgets override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -135,14 +144,19 @@ constructor( } override fun onReorderWidgetStart() { + // Clear selection status + setSelectedIndex(null) + _reorderingWidgets.value = true uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) } override fun onReorderWidgetEnd() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) } override fun onReorderWidgetCancel() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index b2d70523c282..6d9994fb2205 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -556,7 +556,7 @@ public class FrameworkServicesModule { @Provides @Singleton static SubscriptionManager provideSubscriptionManager(Context context) { - return context.getSystemService(SubscriptionManager.class); + return context.getSystemService(SubscriptionManager.class).createForAllUserProfiles(); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt new file mode 100644 index 000000000000..afe9151ac7a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.view.View +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class KeyguardSmartspaceRepository @Inject constructor() { + private val _bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(View.GONE) + val bcSmartspaceVisibility: StateFlow<Int> = _bcSmartspaceVisibility.asStateFlow() + + fun setBcSmartspaceVisibility(visibility: Int) { + _bcSmartspaceVisibility.value = visibility + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt new file mode 100644 index 000000000000..67b57456a5c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardSmartspaceRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class KeyguardSmartspaceInteractor +@Inject +constructor(private val keyguardSmartspaceRepository: KeyguardSmartspaceRepository) { + var bcSmartspaceVisibility: StateFlow<Int> = keyguardSmartspaceRepository.bcSmartspaceVisibility + + fun setBcSmartspaceVisibility(visibility: Int) { + keyguardSmartspaceRepository.setBcSmartspaceVisibility(visibility) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index a8223ea83e1f..b43ab5e9110d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -37,17 +37,19 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn /** Encapsulates business-logic related to the keyguard transitions. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardTransitionInteractor @Inject @@ -192,29 +194,121 @@ constructor( val finishedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } - /** The destination state of the last started transition. */ + /** The destination state of the last [TransitionState.STARTED] transition. */ val startedKeyguardState: SharedFlow<KeyguardState> = startedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) - /** The last completed [KeyguardState] transition */ + /** + * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. + * + * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a + * value when a subsequent transition is STARTED. It will *only* emit once we have finally + * FINISHED in a state. This can have unintuitive implications. + * + * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in + * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain + * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition + * finishes (at which point we'll be FINISHED in LOCKSCREEN). + * + * Since there's no real limit to how many consecutive transitions can be canceled, it's even + * possible for the FINISHED state to be the same as the STARTED state while still + * transitioning. + * + * For example: + * 1. We're finished in GONE. + * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still + * FINISHED in GONE. + * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING -> + * LOCKSCREEN transition. We're still FINISHED in GONE. + * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this + * starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also + * STARTED a transition *to* GONE. + * 5. We'll emit KeyguardState.GONE again once the transition finishes. + * + * If you just need to know when we eventually settle into a state, this flow is likely + * sufficient. However, if you're having issues with state *during* transitions started after + * one or more canceled transitions, you probably need to use [currentKeyguardState]. + */ val finishedKeyguardState: SharedFlow<KeyguardState> = finishedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) /** - * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed - * it. + * The [KeyguardState] we're currently in. + * + * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in + * transition, this is the state we're transitioning *from*. + * + * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always + * identical - if a transition FINISHES in a given state, the subsequent state we START a + * transition *from* would always be that same previously FINISHED state. + * + * However, if a transition is CANCELED, the next transition will START from a state we never + * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in + * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never + * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still + * be GONE. + * + * In this example, if there was DOZING-related state that needs to be set up in order to + * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were + * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would + * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state. + * + * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your + * specific use case and how you want to handle cancellations. In general, if you're dealing + * with state/UI present across multiple [KeyguardState]s, you probably want + * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state, + * you likely want [finishedKeyguardState]. + * + * As an example, let's say you want to animate in a message on the lockscreen UI after waking + * up, and that TextView is not involved in animations between states. You'd want to collect + * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen. + * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is + * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible + * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in + * that case. That's likely not what you want. + * + * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during + * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE + * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation. + * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is + * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this + * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace + * during the LS -> GONE transition. + * + * If you need special-case handling for cancellations (such as conditional handling depending + * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep] + * directly. + * + * As a helpful footnote, here's the values of [finishedKeyguardState] and + * [currentKeyguardState] during a sequence with two cancellations: + * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE. + * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. + * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN. + * currentKeyguardState=DOZING; finishedKeyguardState=GONE. + * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE. + * currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE. + * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. */ - val isInTransitionToAnyState = - combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - startedStep.to != finishedState - } + val currentKeyguardState: SharedFlow<KeyguardState> = + repository.transitions + .mapLatest { + if (it.transitionState == TransitionState.FINISHED) { + it.to + } else { + it.from + } + } + .distinctUntilChanged() + .shareIn(scope, SharingStarted.Eagerly, replay = 1) + + /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */ + val isInTransitionToAnyState = isInTransitionWhere({ true }, { true }) /** * The amount of transition into or out of the given [KeyguardState]. @@ -304,13 +398,12 @@ constructor( fromStatePredicate: (KeyguardState) -> Boolean, toStatePredicate: (KeyguardState) -> Boolean, ): Flow<Boolean> { - return combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - fromStatePredicate(startedStep.from) && - toStatePredicate(startedStep.to) && - finishedState != startedStep.to + return repository.transitions + .filter { it.transitionState != TransitionState.CANCELED } + .mapLatest { + it.transitionState != TransitionState.FINISHED && + fromStatePredicate(it.from) && + toStatePredicate(it.to) } .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index bf763b4e1f99..400b8bfff9b0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -30,7 +30,10 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -48,6 +51,7 @@ object KeyguardClockViewBinder { keyguardRootView: ConstraintLayout, viewModel: KeyguardClockViewModel, keyguardClockInteractor: KeyguardClockInteractor, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -61,18 +65,16 @@ object KeyguardClockViewBinder { viewModel.currentClock.collect { currentClock -> cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) viewModel.clock = currentClock - addClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) - viewModel.burnInLayer?.updatePostLayout(keyguardRootView) - applyConstraints(clockSection, keyguardRootView, true) + addClockViews(currentClock, keyguardRootView) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } - // TODO: Weather clock dozing animation - // will trigger both shouldBeCentered and clockSize change - // we should avoid this launch { if (!migrateClocksToBlueprint()) return@launch viewModel.clockSize.collect { - applyConstraints(clockSection, keyguardRootView, true) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } launch { @@ -82,7 +84,7 @@ object KeyguardClockViewBinder { if (it.largeClock.config.hasCustomPositionUpdatedAnimation) { playClockCenteringAnimation(clockSection, keyguardRootView, it) } else { - applyConstraints(clockSection, keyguardRootView, true) + blueprintInteractor.refreshBlueprint() } } } @@ -90,6 +92,29 @@ object KeyguardClockViewBinder { } } } + @VisibleForTesting + fun updateBurnInLayer( + keyguardRootView: ConstraintLayout, + viewModel: KeyguardClockViewModel, + ) { + val burnInLayer = viewModel.burnInLayer + val clockController = viewModel.currentClock.value + clockController?.let { clock -> + when (viewModel.clockSize.value) { + LARGE -> { + clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) } + if (clock.config.useAlternateSmartspaceAODTransition) { + clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } + } + } + SMALL -> { + clock.smallClock.layout.views.forEach { burnInLayer?.addView(it) } + clock.largeClock.layout.views.forEach { burnInLayer?.removeView(it) } + } + } + } + viewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } private fun cleanupClockViews( clockController: ClockController?, @@ -116,7 +141,6 @@ object KeyguardClockViewBinder { fun addClockViews( clockController: ClockController?, rootView: ConstraintLayout, - burnInLayer: Layer? ) { clockController?.let { clock -> clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view @@ -125,17 +149,10 @@ object KeyguardClockViewBinder { } // small clock should either be a single view or container with id // `lockscreen_clock_view` - clock.smallClock.layout.views.forEach { - rootView.addView(it) - burnInLayer?.addView(it) - } + clock.smallClock.layout.views.forEach { rootView.addView(it) } clock.largeClock.layout.views.forEach { rootView.addView(it) } - if (clock.config.useAlternateSmartspaceAODTransition) { - clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } - } } } - fun applyConstraints( clockSection: ClockSection, rootView: ConstraintLayout, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index 81ce8f04d302..10392e3c1450 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -16,15 +16,13 @@ package com.android.systemui.keyguard.ui.binder -import android.transition.TransitionManager import android.view.View import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -35,10 +33,10 @@ import kotlinx.coroutines.launch object KeyguardSmartspaceViewBinder { @JvmStatic fun bind( - smartspaceSection: SmartspaceSection, keyguardRootView: ConstraintLayout, clockViewModel: KeyguardClockViewModel, smartspaceViewModel: KeyguardSmartspaceViewModel, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -46,22 +44,56 @@ object KeyguardSmartspaceViewBinder { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> - if (hasCustomWeatherDataDisplay) { - removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } else { - addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } - clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) - val constraintSet = ConstraintSet().apply { clone(keyguardRootView) } - smartspaceSection.applyConstraints(constraintSet) - TransitionManager.beginDelayedTransition(keyguardRootView) - constraintSet.applyTo(keyguardRootView) + updateDateWeatherToBurnInLayer( + keyguardRootView, + clockViewModel, + smartspaceViewModel + ) + blueprintInteractor.refreshBlueprint() + } + } + + launch { + smartspaceViewModel.bcSmartspaceVisibility.collect { + updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) + blueprintInteractor.refreshBlueprint() } } } } } + private fun updateBCSmartspaceInBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + ) { + // Visibility is controlled by updateTargetVisibility in CardPagerAdapter + val burnInLayer = keyguardRootView.requireViewById<Layer>(R.id.burn_in_layer) + burnInLayer.apply { + val smartspaceView = + keyguardRootView.requireViewById<View>(sharedR.id.bc_smartspace_view) + if (smartspaceView.visibility == View.VISIBLE) { + addView(smartspaceView) + } else { + removeView(smartspaceView) + } + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + + private fun updateDateWeatherToBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + smartspaceViewModel: KeyguardSmartspaceViewModel + ) { + if (clockViewModel.hasCustomWeatherDataDisplay.value) { + removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel) + } else { + addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + private fun addDateWeatherToBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel @@ -76,13 +108,13 @@ object KeyguardSmartspaceViewBinder { constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view) val weatherView = constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view) - addView(weatherView) addView(dateView) + addView(weatherView) } } } - private fun removeDateWeatherToBurnInLayer( + private fun removeDateWeatherFromBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 24d06026dcf7..8472a9f6da6d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection +import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection @@ -30,11 +31,10 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection +import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject @@ -62,8 +62,8 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, - smartspaceSection: SplitShadeSmartspaceSection, - clockSection: SplitShadeClockSection, + clockSection: ClockSection, + smartspaceSection: SmartspaceSection, mediaSection: SplitShadeMediaSection, ) : KeyguardBlueprint { override val id: String = ID diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index d0626d58a4ad..fd530b77707a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -25,6 +25,8 @@ import android.transition.Visibility import android.view.View import android.view.ViewGroup import androidx.constraintlayout.helper.widget.Layer +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.res.R class BaseBlueprintTransition : TransitionSet() { init { @@ -33,7 +35,16 @@ class BaseBlueprintTransition : TransitionSet() { .addTransition(ChangeBounds()) .addTransition(AlphaInVisibility()) excludeTarget(Layer::class.java, /* exclude= */ true) + excludeClockAndSmartspaceViews() } + + private fun excludeClockAndSmartspaceViews() { + excludeTarget(R.id.lockscreen_clock_view, true) + excludeTarget(R.id.lockscreen_clock_view_large, true) + excludeTarget(SmartspaceView::class.java, true) + // TODO(b/319468190): need to exclude views from large weather clock + } + class AlphaOutVisibility : Visibility() { override fun onDisappear( sceneRoot: ViewGroup?, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt new file mode 100644 index 000000000000..67a20e588198 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.View +import androidx.constraintlayout.helper.widget.Layer + +class AodBurnInLayer(context: Context) : Layer(context) { + // For setScale in Layer class, it stores it in mScaleX/Y and directly apply scale to + // referenceViews instead of keeping the value in fields of View class + // when we try to clone ConstraintSet, it will call getScaleX from View class and return 1.0 + // and when we clone and apply, it will reset everything in the layer + // which cause the flicker from AOD to LS + private var _scaleX = 1F + private var _scaleY = 1F + // As described for _scaleX and _scaleY, we have similar issue with translation + private var _translationX = 1F + private var _translationY = 1F + // avoid adding views with same ids + override fun addView(view: View?) { + view?.let { if (it.id !in referencedIds) super.addView(view) } + } + override fun setScaleX(scaleX: Float) { + _scaleX = scaleX + super.setScaleX(scaleX) + } + + override fun getScaleX(): Float { + return _scaleX + } + + override fun setScaleY(scaleY: Float) { + _scaleY = scaleY + super.setScaleY(scaleY) + } + + override fun getScaleY(): Float { + return _scaleY + } + + override fun setTranslationX(dx: Float) { + _translationX = dx + super.setTranslationX(dx) + } + + override fun getTranslationX(): Float { + return _translationX + } + + override fun setTranslationY(dy: Float) { + _translationY = dy + super.setTranslationY(dy) + } + + override fun getTranslationY(): Float { + return _translationY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 1ccc6ccf2cec..3d36eb03a1bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -19,16 +19,13 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View -import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import javax.inject.Inject /** Adds a layer to group elements for translation for burn-in preventation */ @@ -37,10 +34,8 @@ class AodBurnInSection constructor( private val context: Context, private val clockViewModel: KeyguardClockViewModel, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, ) : KeyguardSection() { - lateinit var burnInLayer: Layer - + private lateinit var burnInLayer: AodBurnInLayer override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -48,7 +43,7 @@ constructor( val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container) burnInLayer = - Layer(context).apply { + AodBurnInLayer(context).apply { id = R.id.burn_in_layer addView(nic) if (!migrateClocksToBlueprint()) { @@ -57,11 +52,6 @@ constructor( addView(statusView) } } - if (migrateClocksToBlueprint()) { - // weather and date parts won't be added here, cause their visibility doesn't align - // with others in burnInLayer - addSmartspaceViews(constraintLayout) - } constraintLayout.addView(burnInLayer) } @@ -83,14 +73,4 @@ constructor( override fun removeViews(constraintLayout: ConstraintLayout) { constraintLayout.removeView(R.id.burn_in_layer) } - - private fun addSmartspaceViews(constraintLayout: ConstraintLayout) { - burnInLayer.apply { - if (smartspaceViewModel.isSmartspaceEnabled) { - val smartspaceView = - constraintLayout.requireViewById<View>(sharedR.id.bc_smartspace_view) - addView(smartspaceView) - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index f560b5f068c9..ed7abff555e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -30,9 +30,7 @@ import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker @@ -53,13 +51,13 @@ constructor( private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val notificationIconAreaController: NotificationIconAreaController, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val systemBarUtilsState: SystemBarUtilsState, ) : KeyguardSection() { private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer + private val smartSpaceBarrier = View.generateViewId() override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { @@ -118,7 +116,7 @@ constructor( } constraintSet.apply { if (migrateClocksToBlueprint()) { - connect(nicId, TOP, sharedR.id.bc_smartspace_view, BOTTOM, bottomMargin) + connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin) setGoneMargin(nicId, BOTTOM, bottomMargin) } else { connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index b5f32c8a5608..b344d3b9afea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -23,11 +23,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder @@ -35,8 +38,10 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.res.R +import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils +import dagger.Lazy import javax.inject.Inject internal fun ConstraintSet.setVisibility( @@ -56,6 +61,7 @@ constructor( protected val keyguardClockViewModel: KeyguardClockViewModel, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) {} @@ -68,6 +74,7 @@ constructor( constraintLayout, keyguardClockViewModel, clockInteractor, + blueprintInteractor.get() ) } @@ -88,12 +95,16 @@ constructor( ): ConstraintSet { // Add constraint between rootView and clockContainer applyDefaultConstraints(constraintSet) + getNonTargetClockFace(clock).applyConstraints(constraintSet) getTargetClockFace(clock).applyConstraints(constraintSet) // Add constraint between elements in clock and clock container return constraintSet.apply { - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) + setVisibility(getTargetClockFace(clock).views, VISIBLE) + setVisibility(getNonTargetClockFace(clock).views, INVISIBLE) + if (!keyguardClockViewModel.useLargeClock) { + connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) + } } } @@ -107,9 +118,12 @@ constructor( private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout open fun applyDefaultConstraints(constraints: ConstraintSet) { + val guideline = + if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID + else R.id.split_shade_guideline constraints.apply { connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START) - connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END) + connect(R.id.lockscreen_clock_view_large, END, guideline, END) connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP) var largeClockTopMargin = context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index b0eee0a68b1f..8c5e9b4c6817 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.view.View import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END @@ -27,11 +28,9 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator @@ -54,7 +53,6 @@ constructor( ambientState: AmbientState, controller: NotificationStackScrollLayoutController, notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( @@ -69,6 +67,7 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) { + private val smartSpaceBarrier = View.generateViewId() override fun applyConstraints(constraintSet: ConstraintSet) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -76,16 +75,14 @@ constructor( constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (migrateClocksToBlueprint()) { connect( R.id.nssl_placeholder, TOP, - sharedR.id.bc_smartspace_view, + R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin ) - setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin) } else { connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index eacd466bc473..37842a84c3d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -18,37 +18,41 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import android.view.View.GONE +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.END -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP -import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.res.R +import com.android.systemui.shared.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import dagger.Lazy import javax.inject.Inject open class SmartspaceSection @Inject constructor( + val context: Context, val keyguardClockViewModel: KeyguardClockViewModel, val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - private val context: Context, + val keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor, val smartspaceController: LockscreenSmartspaceController, val keyguardUnlockAnimationController: KeyguardUnlockAnimationController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { private var smartspaceView: View? = null private var weatherView: View? = null private var dateView: View? = null + private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null + override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { return @@ -64,6 +68,20 @@ constructor( } } keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView + smartspaceVisibilityListener = + object : OnGlobalLayoutListener { + var pastVisibility = GONE + override fun onGlobalLayout() { + smartspaceView?.let { + val newVisibility = it.visibility + if (pastVisibility != newVisibility) { + keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility) + pastVisibility = newVisibility + } + } + } + } + smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -71,10 +89,10 @@ constructor( return } KeyguardSmartspaceViewBinder.bind( - this, constraintLayout, keyguardClockViewModel, keyguardSmartspaceViewModel, + blueprintInteractor.get(), ) } @@ -82,65 +100,96 @@ constructor( if (!migrateClocksToBlueprint()) { return } - // Generally, weather should be next to dateView - // smartspace should be below date & weather views constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController - dateView?.let { dateView -> - constrainHeight(dateView.id, WRAP_CONTENT) - constrainWidth(dateView.id, WRAP_CONTENT) - connect( - dateView.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.date_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - } - weatherView?.let { - constrainWidth(it.id, WRAP_CONTENT) - dateView?.let { dateView -> - connect(it.id, TOP, dateView.id, TOP) - connect(it.id, BOTTOM, dateView.id, BOTTOM) - connect(it.id, START, dateView.id, END, 4) - } - } + ) + constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.weather_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.TOP + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.BOTTOM, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.START, + R.id.date_smartspace_view, + ConstraintSet.END, + 4 + ) + // migrate addSmartspaceView from KeyguardClockSwitchController - smartspaceView?.let { - constrainHeight(it.id, WRAP_CONTENT) - connect( - it.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.bc_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - connect( - it.id, - END, - PARENT_ID, - END, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.END, + if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID + else com.android.systemui.res.R.id.split_shade_guideline, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_end ) - } + ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - dateView?.let { dateView -> - smartspaceView?.let { smartspaceView -> - connect(dateView.id, BOTTOM, smartspaceView.id, TOP) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.TOP) + connect( + R.id.date_smartspace_view, + ConstraintSet.BOTTOM, + R.id.bc_smartspace_view, + ConstraintSet.TOP + ) } else { - dateView?.let { dateView -> - clear(dateView.id, BOTTOM) - connect(dateView.id, TOP, R.id.lockscreen_clock_view, BOTTOM) - constrainHeight(dateView.id, WRAP_CONTENT) - smartspaceView?.let { smartspaceView -> - clear(smartspaceView.id, TOP) - connect(smartspaceView.id, TOP, dateView.id, BOTTOM) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM) + connect( + R.id.date_smartspace_view, + ConstraintSet.TOP, + com.android.systemui.res.R.id.lockscreen_clock_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) } + + createBarrier( + com.android.systemui.res.R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.bc_smartspace_view, + R.id.date_smartspace_view, + R.id.weather_smartspace_view, + ) + ) } updateVisibility(constraintSet) } @@ -156,30 +205,28 @@ constructor( } } } + smartspaceView?.viewTreeObserver?.removeOnGlobalLayoutListener(smartspaceVisibilityListener) + smartspaceVisibilityListener = null } private fun updateVisibility(constraintSet: ConstraintSet) { constraintSet.apply { - weatherView?.let { - setVisibility( - it.id, - when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - true -> ConstraintSet.GONE - false -> - when (keyguardSmartspaceViewModel.isWeatherEnabled) { - true -> ConstraintSet.VISIBLE - false -> ConstraintSet.GONE - } - } - ) - } - dateView?.let { - setVisibility( - it.id, - if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE - else ConstraintSet.VISIBLE - ) - } + setVisibility( + R.id.weather_smartspace_view, + when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { + true -> ConstraintSet.GONE + false -> + when (keyguardSmartspaceViewModel.isWeatherEnabled) { + true -> ConstraintSet.VISIBLE + false -> ConstraintSet.GONE + } + } + ) + setVisibility( + R.id.date_smartspace_view, + if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE + else ConstraintSet.VISIBLE + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt deleted file mode 100644 index 19ba1aa4763a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.res.R -import com.android.systemui.statusbar.policy.SplitShadeStateController -import javax.inject.Inject - -class SplitShadeClockSection -@Inject -constructor( - clockInteractor: KeyguardClockInteractor, - keyguardClockViewModel: KeyguardClockViewModel, - context: Context, - splitShadeStateController: SplitShadeStateController, -) : ClockSection(clockInteractor, keyguardClockViewModel, context, splitShadeStateController) { - override fun applyDefaultConstraints(constraints: ConstraintSet) { - super.applyDefaultConstraints(constraints) - val largeClockEndGuideline = - if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else R.id.split_shade_guideline - constraints.apply { - connect( - R.id.lockscreen_clock_view_large, - ConstraintSet.END, - largeClockEndGuideline, - ConstraintSet.END - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index f20ab06bcda9..b12a8a811955 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -20,7 +20,6 @@ import android.content.Context import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -31,11 +30,9 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import javax.inject.Inject /** Aligns media on left side for split shade, below smartspace, date, and weather. */ @@ -44,11 +41,9 @@ class SplitShadeMediaSection constructor( private val context: Context, private val notificationPanelView: NotificationPanelView, - private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, private val keyguardMediaController: KeyguardMediaController ) : KeyguardSection() { private val mediaContainerId = R.id.status_view_media_container - private val smartSpaceBarrier = R.id.smart_space_barrier_bottom override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { @@ -85,18 +80,7 @@ constructor( constraintSet.apply { constrainWidth(mediaContainerId, MATCH_CONSTRAINT) constrainHeight(mediaContainerId, WRAP_CONTENT) - - createBarrier( - smartSpaceBarrier, - Barrier.BOTTOM, - 0, - *intArrayOf( - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - sharedR.id.weather_smartspace_view, - ) - ) - connect(mediaContainerId, TOP, smartSpaceBarrier, BOTTOM) + connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) connect(mediaContainerId, START, PARENT_ID, START) connect(mediaContainerId, END, R.id.split_shade_guideline, END) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt deleted file mode 100644 index 8728adadd8c3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import com.android.systemui.keyguard.KeyguardUnlockAnimationController -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import javax.inject.Inject - -/* - * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called - * when switching to and from splitShade. - */ -class SplitShadeSmartspaceSection -@Inject -constructor( - keyguardClockViewModel: KeyguardClockViewModel, - keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - context: Context, - smartspaceController: LockscreenSmartspaceController, - keyguardUnlockAnimationController: KeyguardUnlockAnimationController, -) : - SmartspaceSection( - keyguardClockViewModel, - keyguardSmartspaceViewModel, - context, - smartspaceController, - keyguardUnlockAnimationController, - ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index a1dd720a82f1..e8c1ab5feccc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -33,6 +34,7 @@ constructor( @Application applicationScope: CoroutineScope, smartspaceController: LockscreenSmartspaceController, keyguardClockViewModel: KeyguardClockViewModel, + smartspaceInteractor: KeyguardSmartspaceInteractor, ) { /** Whether the smartspace section is available in the build. */ val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled() @@ -78,4 +80,7 @@ constructor( ): Boolean { return !clockIncludesCustomWeatherDisplay && isWeatherEnabled } + + /* trigger clock and smartspace constraints change when smartspace appears */ + var bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 5ee38bebc99b..a48fb45861d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -240,13 +240,13 @@ constructor( */ val translationY: Flow<Float> = combine( - isOnLockscreen, + isOnLockscreenWithoutShade, merge( keyguardInteractor.keyguardTranslationY, occludedToLockscreenTransitionViewModel.lockscreenTranslationY, ) - ) { isOnLockscreen, translationY -> - if (isOnLockscreen) { + ) { isOnLockscreenWithoutShade, translationY -> + if (isOnLockscreenWithoutShade) { translationY } else { 0f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 2b90e649a154..e309c32df64e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -265,13 +265,6 @@ abstract class StatusBarPipelineModule { return factory.create("VerboseMobileViewLog", 100) } - @Provides - @SysUISingleton - @OemSatelliteInputLog - fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer { - return factory.create("DeviceBasedSatelliteInputLog", 32) - } - const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON = "FirstMobileSubShowingNetworkTypeIcon" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt index 8400fb08e147..e3c3139f6906 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.icons.shared import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon -import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon import javax.inject.Inject /** @@ -39,12 +38,11 @@ interface BindableIconsRegistry { class BindableIconsRegistryImpl @Inject constructor( - /** Bindables go here */ - oemSatellite: DeviceBasedSatelliteBindableIcon +/** Bindables go here */ ) : BindableIconsRegistry { /** * Adding the injected bindables to this list will get them registered with * StatusBarIconController */ - override val bindableIcons: List<BindableIcon> = listOf(oemSatellite) + override val bindableIcons: List<BindableIcon> = listOf() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 5e6e36dd69a9..de46a5ed99d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -19,17 +19,11 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager -import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.core.LogLevel -import com.android.systemui.log.core.MessageInitializer -import com.android.systemui.log.core.MessagePrinter -import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported @@ -129,7 +123,6 @@ constructor( satelliteManagerOpt: Optional<SatelliteManager>, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, - @OemSatelliteInputLog private val logBuffer: LogBuffer, private val systemClock: SystemClock, ) : DeviceBasedSatelliteRepository { @@ -152,11 +145,6 @@ constructor( ensureMinUptime(systemClock, MIN_UPTIME) satelliteSupport.value = satelliteManager.checkSatelliteSupported() - logBuffer.i( - { str1 = satelliteSupport.value.toString() }, - { "Checked for system support. support=$str1" }, - ) - // We only need to check location availability if this mode is supported if (satelliteSupport.value is Supported) { isSatelliteAllowedForCurrentLocation.subscriptionCount @@ -171,9 +159,6 @@ constructor( * connection might cause more frequent checks. */ while (true) { - logBuffer.i { - "requestIsSatelliteCommunicationAllowedForCurrentLocation" - } checkIsSatelliteAllowed() delay(POLLING_INTERVAL_MS) } @@ -182,8 +167,6 @@ constructor( } } } else { - logBuffer.i { "Satellite manager is null" } - satelliteSupport.value = NotSupported } } @@ -198,21 +181,12 @@ constructor( private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = conflatedCallbackFlow { val cb = SatelliteModemStateCallback { state -> - logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" } trySend(SatelliteConnectionState.fromModemState(state)) } - var registered = false - - try { - val res = - sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) - registered = res == SATELLITE_RESULT_SUCCESS - } catch (e: Exception) { - logBuffer.e("error registering for modem state", e) - } + sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) - awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) } + awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) } } .flowOn(bgDispatcher) @@ -223,21 +197,12 @@ constructor( private fun signalStrengthFlow(sm: SupportedSatelliteManager) = conflatedCallbackFlow { val cb = NtnSignalStrengthCallback { signalStrength -> - logBuffer.i({ int1 = signalStrength.level }) { - "onNtnSignalStrengthChanged: level=$int1" - } trySend(signalStrength.level) } - var registered = false - try { - sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) - registered = true - } catch (e: Exception) { - logBuffer.e("error registering for signal strength", e) - } + sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) - awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) } + awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) } } .flowOn(bgDispatcher) @@ -248,15 +213,11 @@ constructor( bgDispatcher.asExecutor(), object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { override fun onError(e: SatelliteManager.SatelliteException) { - logBuffer.e( - "Found exception when checking availability", - e, - ) + android.util.Log.e(TAG, "Found exception when checking for satellite: ", e) isSatelliteAllowedForCurrentLocation.value = false } override fun onResult(allowed: Boolean) { - logBuffer.i { allowed.toString() } isSatelliteAllowedForCurrentLocation.value = allowed } } @@ -278,12 +239,6 @@ constructor( } override fun onError(error: SatelliteManager.SatelliteException) { - logBuffer.e( - "Exception when checking for satellite support. " + - "Assuming it is not supported for this device.", - error, - ) - // Assume that an error means it's not supported continuation.resume(NotSupported) } @@ -309,19 +264,5 @@ constructor( delay(timeTilMinUptime) } } - - /** A couple of convenience logging methods rather than a whole class */ - private fun LogBuffer.i( - initializer: MessageInitializer = {}, - printer: MessagePrinter, - ) = this.log(TAG, LogLevel.INFO, initializer, printer) - - private fun LogBuffer.e(message: String, exception: Throwable? = null) = - this.log( - tag = TAG, - level = LogLevel.ERROR, - message = message, - exception = exception, - ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt deleted file mode 100644 index f5d0f6b8f07c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui - -import android.content.Context -import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon -import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator -import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder -import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView -import javax.inject.Inject - -@SysUISingleton -class DeviceBasedSatelliteBindableIcon -@Inject -constructor( - context: Context, - viewModel: DeviceBasedSatelliteViewModel, -) : BindableIcon { - override val slot: String = - context.getString(com.android.internal.R.string.status_bar_oem_satellite) - - override val initializer = ModernStatusBarViewCreator { context -> - SingleBindableStatusBarIconView.createView(context).also { view -> - view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) } - } - } - - override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag() -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt deleted file mode 100644 index 59ac5f29b66c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.binder - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.common.ui.binder.IconViewBinder -import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding -import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView -import kotlinx.coroutines.launch - -object DeviceBasedSatelliteIconBinder { - fun bind( - view: SingleBindableStatusBarIconView, - viewModel: DeviceBasedSatelliteViewModel, - ): ModernStatusBarViewBinding { - return SingleBindableStatusBarIconView.withDefaultBinding( - view = view, - shouldBeVisible = { viewModel.icon.value != null } - ) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.icon.collect { newIcon -> - if (newIcon == null) { - view.iconView.setImageDrawable(null) - } else { - IconViewBinder.bind(newIcon, view.iconView) - } - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt deleted file mode 100644 index 6938d667ca81..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.model - -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.res.R -import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState - -/** - * Define the [Icon] that relates to a given satellite connection state + level. Note that for now - * We don't need any data class box, so we can just use a simple mapping function. - */ -object SatelliteIconModel { - fun fromConnectionState( - connectionState: SatelliteConnectionState, - signalStrength: Int, - ): Icon? = - when (connectionState) { - // TODO(b/316635648): check if this should be null - SatelliteConnectionState.Unknown, - SatelliteConnectionState.Off, - SatelliteConnectionState.On -> - Icon.Resource( - res = R.drawable.ic_satellite_not_connected, - contentDescription = null, - ) - SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) - } - - private fun fromSignalStrength( - signalStrength: Int, - ): Icon? = - // TODO(b/316634365): these need content descriptions - when (signalStrength) { - // No signal - 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null) - - // Poor -> Moderate - 1, - 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null) - - // Good -> Great - 3, - 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null) - else -> null - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt deleted file mode 100644 index 0051161eff35..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel - -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor -import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn - -/** - * View-Model for the device-based satellite icon. This icon will only show in the status bar if - * satellite is available AND all other service states are considered OOS. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class DeviceBasedSatelliteViewModel -@Inject -constructor( - interactor: DeviceBasedSatelliteInteractor, - @Application scope: CoroutineScope, -) { - private val shouldShowIcon: StateFlow<Boolean> = - interactor.areAllConnectionsOutOfService - .flatMapLatest { allOos -> - if (!allOos) { - flowOf(false) - } else { - interactor.isSatelliteAllowed - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - val icon: StateFlow<Icon?> = - combine( - shouldShowIcon, - interactor.connectionState, - interactor.signalStrength, - ) { shouldShow, state, signalStrength -> - if (shouldShow) { - SatelliteIconModel.fromConnectionState(state, signalStrength) - } else { - null - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt index 25a2c9dd3caf..3b87bed2e0ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -103,7 +103,7 @@ open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : * * Creates a dot view, and uses [bindingCreator] to get and set the binding. */ - open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot // view. So, this is the required order. this.slot = slot diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt deleted file mode 100644 index c663c37fec98..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.shared.ui.view - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.ImageView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.res.R -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */ -class SingleBindableStatusBarIconView( - context: Context, - attrs: AttributeSet?, -) : ModernStatusBarView(context, attrs) { - - internal lateinit var iconView: ImageView - internal lateinit var dotView: StatusBarIconView - - override fun toString(): String { - return "SingleBindableStatusBarIcon(" + - "slot='$slot', " + - "isCollecting=${binding.isCollecting()}, " + - "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + - "viewString=${super.toString()}" - } - - override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { - super.initView(slot, bindingCreator) - - iconView = requireViewById(R.id.icon_view) - dotView = requireViewById(R.id.status_bar_dot) - } - - companion object { - fun createView( - context: Context, - ): SingleBindableStatusBarIconView { - return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null) - as SingleBindableStatusBarIconView - } - - /** - * Using a given binding [block], create the necessary scaffolding to handle the general - * case of a single status bar icon. This includes eliding into a dot view when there is not - * enough space, and handling tint. - * - * [block] should be a simple [launch] call that handles updating the single icon view with - * its new view. Currently there is no simple way to e.g., extend to handle multiple tints - * for dual-layered icons, and any more complex logic should probably find a way to return - * its own version of [ModernStatusBarViewBinding]. - */ - fun withDefaultBinding( - view: SingleBindableStatusBarIconView, - shouldBeVisible: () -> Boolean, - block: suspend LifecycleOwner.(View) -> Unit - ): SingleBindableStatusBarIconViewBinding { - @StatusBarIconView.VisibleState - val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) - - val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) - val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) - - var isCollecting: Boolean = false - - view.repeatWhenAttached { - // Child binding - block(view) - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - // isVisible controls the visibility state of the outer group, and thus it - // needs - // to run in the CREATED lifecycle so it can continue to watch while - // invisible - // See (b/291031862) for details - launch { - visibilityState.collect { visibilityState -> - // for b/296864006, we can not hide all the child views if - // visibilityState is STATE_HIDDEN. Because hiding all child views - // would cause the - // getWidth() of this view return 0, and that would cause the - // translation - // calculation fails in StatusIconContainer. Therefore, like class - // MobileIconBinder, instead of set the child views visibility to - // View.GONE, - // we set their visibility to View.INVISIBLE to make them invisible - // but - // keep the width. - ModernStatusBarViewVisibilityHelper.setVisibilityState( - visibilityState, - view.iconView, - view.dotView, - ) - } - } - - launch { - iconTint.collect { tint -> - val tintList = ColorStateList.valueOf(tint) - view.iconView.imageTintList = tintList - view.dotView.setDecorColor(tint) - } - } - - launch { - decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) } - } - - try { - awaitCancellation() - } finally { - isCollecting = false - } - } - } - } - - return object : SingleBindableStatusBarIconViewBinding { - override val decorTint: Int - get() = decorTint.value - - override val iconTint: Int - get() = iconTint.value - - override fun getShouldIconBeVisible(): Boolean { - return shouldBeVisible() - } - - override fun onVisibilityStateChanged(state: Int) { - visibilityState.value = state - } - - override fun onIconTintChanged(newTint: Int, contrastTint: Int) { - iconTint.value = newTint - } - - override fun onDecorTintChanged(newTint: Int) { - decorTint.value = newTint - } - - override fun isCollecting(): Boolean { - return isCollecting - } - } - } - } -} - -@VisibleForTesting -interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding { - val iconTint: Int - val decorTint: Int -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index a4d217f1af79..5dd37ae46ee8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -21,13 +21,17 @@ import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.util.mockito.whenever import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mock @@ -48,30 +52,58 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { @Mock private lateinit var smallClockView: View @Mock private lateinit var smallClockFaceLayout: ClockFaceLayout @Mock private lateinit var largeClockFaceLayout: ClockFaceLayout + @Mock private lateinit var clockViewModel: KeyguardClockViewModel + private val clockSize = MutableStateFlow(LARGE) + private val currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null) @Before fun setup() { MockitoAnnotations.initMocks(this) + whenever(clockViewModel.clockSize).thenReturn(clockSize) + whenever(clockViewModel.currentClock).thenReturn(currentClock) + whenever(clockViewModel.burnInLayer).thenReturn(burnInLayer) + } + + @Test + fun addClockViews_WeatherClock() { + setupWeatherClock() + KeyguardClockViewBinder.addClockViews(clock, rootView) + verify(rootView).addView(smallClockView) + verify(rootView).addView(largeClockView) } @Test fun addClockViews_nonWeatherClock() { setupNonWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) + KeyguardClockViewBinder.addClockViews(clock, rootView) verify(rootView).addView(smallClockView) verify(rootView).addView(largeClockView) - verify(burnInLayer).addView(smallClockView) + } + @Test + fun addClockViewsToBurnInLayer_LargeWeatherClock() { + setupWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) + verify(burnInLayer).addView(largeClockView) + } + + @Test + fun addClockViewsToBurnInLayer_LargeNonWeatherClock() { + setupNonWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) verify(burnInLayer, never()).addView(largeClockView) } @Test - fun addClockViews_WeatherClock() { - setupWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) - verify(rootView).addView(smallClockView) - verify(rootView).addView(largeClockView) + fun addClockViewsToBurnInLayer_SmallClock() { + setupNonWeatherClock() + clockSize.value = SMALL + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) verify(burnInLayer).addView(smallClockView) - verify(burnInLayer).addView(largeClockView) + verify(burnInLayer).removeView(largeClockView) } private fun setupWeatherClock() { @@ -99,5 +131,7 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) + whenever(clockViewModel.clock).thenReturn(clock) + currentClock.value = clock } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 070a0ccd1232..57b555989166 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -22,6 +22,7 @@ import android.content.res.Resources import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.res.R @@ -31,6 +32,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -46,6 +49,8 @@ class ClockSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> + private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true) private lateinit var underTest: ClockSection @@ -104,12 +109,15 @@ class ClockSectionTest : SysuiTestCase() { whenever(packageManager.getResourcesForApplication(anyString())).thenReturn(remoteResources) mContext.setMockPackageManager(packageManager) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) + underTest = ClockSection( keyguardClockInteractor, keyguardClockViewModel, mContext, splitShadeStateController, + blueprintInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt index 28da957d31b0..deb3a83fcbee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt @@ -26,6 +26,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R @@ -34,7 +36,8 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.StateFlow +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,7 +53,8 @@ class SmartspaceSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel @Mock private lateinit var lockscreenSmartspaceController: LockscreenSmartspaceController @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController - @Mock private lateinit var hasCustomWeatherDataDisplay: StateFlow<Boolean> + @Mock private lateinit var keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> private val smartspaceView = View(mContext).also { it.id = sharedR.id.bc_smartspace_view } private val weatherView = View(mContext).also { it.id = sharedR.id.weather_smartspace_view } @@ -58,17 +62,22 @@ class SmartspaceSectionTest : SysuiTestCase() { private lateinit var constraintLayout: ConstraintLayout private lateinit var constraintSet: ConstraintSet + private val clockShouldBeCentered = MutableStateFlow(false) + private val hasCustomWeatherDataDisplay = MutableStateFlow(false) + @Before fun setup() { MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) underTest = SmartspaceSection( + mContext, keyguardClockViewModel, keyguardSmartspaceViewModel, - mContext, + keyguardSmartspaceInteractor, lockscreenSmartspaceController, keyguardUnlockAnimationController, + blueprintInteractor ) constraintLayout = ConstraintLayout(mContext) whenever(lockscreenSmartspaceController.buildAndConnectView(any())) @@ -78,6 +87,7 @@ class SmartspaceSectionTest : SysuiTestCase() { whenever(lockscreenSmartspaceController.buildAndConnectDateView(any())).thenReturn(dateView) whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay) .thenReturn(hasCustomWeatherDataDisplay) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) constraintSet = ConstraintSet() } @@ -115,7 +125,7 @@ class SmartspaceSectionTest : SysuiTestCase() { fun testConstraintsWhenNotHasCustomWeatherDataDisplay() { whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true) whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true) - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -129,7 +139,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testConstraintsWhenHasCustomWeatherDataDisplay() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -140,7 +150,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testNormalDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false whenever(keyguardSmartspaceViewModel.isWeatherEnabled).thenReturn(true) underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) @@ -153,7 +163,7 @@ class SmartspaceSectionTest : SysuiTestCase() { } @Test fun testCustomDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt index 45f0a8c62125..44c411fdb1d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt @@ -66,6 +66,11 @@ class FakeMediaProjectionManager { private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test" private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId()) - private val DEFAULT_INFO = MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE) + private val DEFAULT_INFO = + MediaProjectionInfo( + DEFAULT_PACKAGE_NAME, + DEFAULT_USER_HANDLE, + /* launchCookie = */ null + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 9f15b05f8f02..a824bc4f803f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -484,6 +484,46 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun translationYUpdatesOnKeyguard() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreen() + + // The translation values are negative + assertThat(translationY).isLessThan(0f) + } + + @Test + fun translationYDoesNotUpdateWhenShadeIsExpanded() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer but also for + // shade collapsing + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreenWithShadeExpanded() + + assertThat(translationY).isEqualTo(0f) + } + + @Test fun updateBounds_fromKeyguardRoot() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 7d91e8baa92c..02e6fd5a9d6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -35,7 +35,6 @@ import android.telephony.satellite.SatelliteModemStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState @@ -88,7 +87,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { Optional.empty(), dispatcher, testScope.backgroundScope, - FakeLogBuffer.Factory.create(), systemClock, ) @@ -102,22 +100,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test - fun satelliteManagerThrows_doesNotCrash() = - testScope.runTest { - setupDefaultRepo() - - whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any())) - .thenThrow(SatelliteException(13)) - - val conn by collectLastValue(underTest.connectionState) - val strength by collectLastValue(underTest.signalStrength) - - // Flows have not emitted, we haven't crashed - assertThat(conn).isNull() - assertThat(strength).isNull() - } - - @Test fun connectionState_mapsFromSatelliteModemState() = testScope.runTest { setupDefaultRepo() @@ -398,7 +380,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { if (satMan != null) Optional.of(satMan) else Optional.empty(), dispatcher, testScope.backgroundScope, - FakeLogBuffer.Factory.create(), systemClock, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt deleted file mode 100644 index 21c038ad476d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy -import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository -import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor -import com.android.systemui.util.mockito.mock -import com.google.common.truth.Truth.assertThat -import kotlin.test.Test -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.mockito.MockitoAnnotations - -@SmallTest -class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { - private lateinit var underTest: DeviceBasedSatelliteViewModel - private lateinit var interactor: DeviceBasedSatelliteInteractor - - private val repo = FakeDeviceBasedSatelliteRepository() - private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) - - private val testScope = TestScope() - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - interactor = - DeviceBasedSatelliteInteractor( - repo, - mobileIconsInteractor, - testScope.backgroundScope, - ) - - underTest = - DeviceBasedSatelliteViewModel( - interactor, - testScope.backgroundScope, - ) - } - - @Test - fun icon_nullWhenShouldNotShow_satelliteNotAllowed() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is not allowed - repo.isSatelliteAllowedForCurrentLocation.value = false - - // GIVEN all icons are OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false - - // THEN icon is null because we should not be showing it - assertThat(latest).isNull() - } - - @Test - fun icon_nullWhenShouldNotShow_notAllOos() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is allowed - repo.isSatelliteAllowedForCurrentLocation.value = true - - // GIVEN all icons are not OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = true - - // THEN icon is null because we have service - assertThat(latest).isNull() - } - - @Test - fun icon_satelliteIsOff() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is allowed - repo.isSatelliteAllowedForCurrentLocation.value = true - - // GIVEN all icons are OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false - - // THEN icon is null because we have service - assertThat(latest).isInstanceOf(Icon::class.java) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt deleted file mode 100644 index ca9df57e8798..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.shared.ui.view - -import android.graphics.Rect -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.StatusBarIconView -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the - * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding] - * method. - */ -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -class SingleBindableStatusBarIconViewTest : SysuiTestCase() { - private lateinit var binding: SingleBindableStatusBarIconViewBinding - - // Visibility is outsourced to view-models. This simulates it - private var isVisible = true - private var visibilityFn: () -> Boolean = { isVisible } - - @Test - fun initView_hasCorrectSlot() { - val view = createAndInitView() - - assertThat(view.slot).isEqualTo(SLOT_NAME) - } - - @Test - fun getVisibleState_icon_returnsIcon() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON) - } - - @Test - fun getVisibleState_dot_returnsDot() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT) - } - - @Test - fun getVisibleState_hidden_returnsHidden() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN) - } - - @Test - fun onDarkChanged_bindingReceivesIconAndDecorTint() { - val view = createAndInitView() - - view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321) - - assertThat(binding.iconTint).isEqualTo(0x12345678) - assertThat(binding.decorTint).isEqualTo(0x12345678) - } - - @Test - fun setStaticDrawableColor_bindingReceivesIconTint() { - val view = createAndInitView() - - view.setStaticDrawableColor(0x12345678, 0x12344321) - - assertThat(binding.iconTint).isEqualTo(0x12345678) - } - - @Test - fun setDecorColor_bindingReceivesDecorColor() { - val view = createAndInitView() - - view.setDecorColor(0x23456789) - - assertThat(binding.decorTint).isEqualTo(0x23456789) - } - - @Test - fun isIconVisible_usesBinding_true() { - val view = createAndInitView() - - isVisible = true - - assertThat(view.isIconVisible).isEqualTo(true) - } - - @Test - fun isIconVisible_usesBinding_false() { - val view = createAndInitView() - - isVisible = false - - assertThat(view.isIconVisible).isEqualTo(false) - } - - @Test - fun getDrawingRect_takesTranslationIntoAccount() { - val view = createAndInitView() - - view.translationX = 50f - view.translationY = 60f - - val drawingRect = Rect() - view.getDrawingRect(drawingRect) - - assertThat(drawingRect.left).isEqualTo(view.left + 50) - assertThat(drawingRect.right).isEqualTo(view.right + 50) - assertThat(drawingRect.top).isEqualTo(view.top + 60) - assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60) - } - - private fun createAndInitView(): SingleBindableStatusBarIconView { - val view = SingleBindableStatusBarIconView.createView(context) - binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {} - view.initView(SLOT_NAME) { binding } - return view - } - - companion object { - private const val SLOT_NAME = "test_slot" - } -} diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index a0301a920d96..6e906ebe887a 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -34,7 +34,7 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private volatile boolean mShouldProcessRequests = false; - private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(100); + private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500); SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { super(associationId, fd, context); diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index eb6fdd72f2c3..f921b0b94b25 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -418,6 +418,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { LinkCapacityEstimate.INVALID, LinkCapacityEstimate.INVALID))); private List<List<LinkCapacityEstimate>> mLinkCapacityEstimateLists; + private int[] mSimultaneousCellularCallingSubIds = {}; + private int[] mECBMReason; private boolean[] mECBMStarted; private int[] mSCBMReason; @@ -564,7 +566,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { || events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED) - || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); + || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED) + || events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); } private static final int MSG_USER_SWITCHED = 1; @@ -1427,6 +1431,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged( + mSimultaneousCellularCallingSubIds); + } catch (RemoteException ex) { + remove(r.binder); + } + } if (events.contains( TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) { try { @@ -3092,6 +3105,43 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } + /** + * Notify the listeners that simultaneous cellular calling subscriptions have changed + * @param subIds The set of subIds that support simultaneous cellular calling + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged(int[] subIds) { + if (!checkNotifyPermission("notifySimultaneousCellularCallingSubscriptionsChanged()")) { + return; + } + + if (VDBG) { + StringBuilder b = new StringBuilder(); + b.append("notifySimultaneousCellularCallingSubscriptionsChanged: "); + b.append("subIds = {"); + for (int i : subIds) { + b.append(" "); + b.append(i); + } + b.append("}"); + log(b.toString()); + } + + synchronized (mRecords) { + mSimultaneousCellularCallingSubIds = subIds; + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged(subIds); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @Override public void addCarrierPrivilegesCallback( int phoneId, diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index fd17261bda41..c18bacb51671 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -172,6 +172,7 @@ public class Watchdog implements Dumpable { public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] { "android.hardware.audio.core.IModule/", "android.hardware.audio.core.IConfig/", + "android.hardware.audio.effect.IFactory/", "android.hardware.biometrics.face.IFace/", "android.hardware.biometrics.fingerprint.IFingerprint/", "android.hardware.bluetooth.IBluetoothHci/", diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index b03183cb37d5..fa5dbd2543d3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2935,7 +2935,11 @@ public final class ProcessList { return true; } - private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs, + private static boolean unfreezePackageCgroup(int packageUID) { + return freezePackageCgroup(packageUID, false); + } + + private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs, int packageUID) { // Freeze all binder processes under the target UID (whose cgroup is about to be frozen). // Since we're going to kill these, we don't need to unfreze them later. @@ -2943,12 +2947,9 @@ public final class ProcessList { // processes (forks) should not be Binder users. int N = procs.size(); for (int i = 0; i < N; i++) { - final int uid = procs.get(i).first.uid; final int pid = procs.get(i).first.getPid(); int nRetries = 0; - // We only freeze the cgroup of the target package, so we do not need to freeze the - // Binder interfaces of dependant processes in other UIDs. - if (pid > 0 && uid == packageUID) { + if (pid > 0) { try { int rc; do { @@ -2962,12 +2963,19 @@ public final class ProcessList { } // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze - // despite being added to a new child cgroup. The cgroups of package dependant processes are - // not frozen, since it's possible this would freeze processes with no dependency on the - // package being killed here. + // despite being added to a child cgroup created after this call that would otherwise be + // unfrozen. freezePackageCgroup(packageUID, true); } + private static List<Pair<ProcessRecord, Boolean>> getUIDSublist( + List<Pair<ProcessRecord, Boolean>> procs, int startIdx) { + final int uid = procs.get(startIdx).first.uid; + int endIdx = startIdx + 1; + while (endIdx < procs.size() && procs.get(endIdx).first.uid == uid) ++endIdx; + return procs.subList(startIdx, endIdx); + } + @GuardedBy({"mService", "mProcLock"}) boolean killPackageProcessesLSP(String packageName, int appId, int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart, @@ -3063,25 +3071,36 @@ public final class ProcessList { } } - final int packageUID = UserHandle.getUid(userId, appId); - final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID - && appId <= Process.LAST_APPLICATION_UID; - if (doFreeze) { - freezeBinderAndPackageCgroup(procs, packageUID); + final boolean killingUserApp = appId >= Process.FIRST_APPLICATION_UID + && appId <= Process.LAST_APPLICATION_UID; + + if (killingUserApp) { + procs.sort((o1, o2) -> Integer.compare(o1.first.uid, o2.first.uid)); } - int N = procs.size(); - for (int i=0; i<N; i++) { - final Pair<ProcessRecord, Boolean> proc = procs.get(i); - removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, - reasonCode, subReason, reason, !doFreeze /* async */); + int idx = 0; + while (idx < procs.size()) { + final List<Pair<ProcessRecord, Boolean>> uidProcs = getUIDSublist(procs, idx); + final int packageUID = uidProcs.get(0).first.uid; + + // Do not freeze for system apps or for dependencies of the targeted package, but + // make sure to freeze the targeted package for all users if called with USER_ALL. + final boolean doFreeze = killingUserApp && UserHandle.getAppId(packageUID) == appId; + + if (doFreeze) freezeBinderAndPackageCgroup(uidProcs, packageUID); + + for (Pair<ProcessRecord, Boolean> proc : uidProcs) { + removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, + reasonCode, subReason, reason, !doFreeze /* async */); + } + killAppZygotesLocked(packageName, appId, userId, false /* force */); + + if (doFreeze) unfreezePackageCgroup(packageUID); + + idx += uidProcs.size(); } - killAppZygotesLocked(packageName, appId, userId, false /* force */); mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END); - if (doFreeze) { - freezePackageCgroup(packageUID, false); - } - return N > 0; + return procs.size() > 0; } @GuardedBy("mService") diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java index 5189017f5bf0..b084cf3c3b12 100644 --- a/services/core/java/com/android/server/app/GameManagerSettings.java +++ b/services/core/java/com/android/server/app/GameManagerSettings.java @@ -251,6 +251,7 @@ public class GameManagerSettings { + type); } } + str.close(); } catch (XmlPullParserException | java.io.IOException e) { Slog.wtf(TAG, "Error reading game manager settings", e); return false; diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index f80228afa52d..99b45ec79571 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1812,22 +1812,21 @@ public class AudioDeviceBroker { "msg: MSG_L_SET_BT_ACTIVE_DEVICE " + "received with null profile proxy: " + btInfo)).printLog(TAG)); - sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/); - return; - } - @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = - mBtHelper.getCodecWithFallback(btInfo.mDevice, - btInfo.mProfile, btInfo.mIsLeOutput, - "MSG_L_SET_BT_ACTIVE_DEVICE"); - mDeviceInventory.onSetBtActiveDevice(btInfo, codec, - (btInfo.mProfile - != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) - ? mAudioService.getBluetoothContextualVolumeStream() - : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO - || btInfo.mProfile == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient(isBluetoothScoRequested(), - "setBluetoothActiveDevice"); + } else { + @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = + mBtHelper.getCodecWithFallback(btInfo.mDevice, + btInfo.mProfile, btInfo.mIsLeOutput, + "MSG_L_SET_BT_ACTIVE_DEVICE"); + mDeviceInventory.onSetBtActiveDevice(btInfo, codec, + (btInfo.mProfile + != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) + ? mAudioService.getBluetoothContextualVolumeStream() + : AudioSystem.STREAM_DEFAULT); + if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + || btInfo.mProfile == BluetoothProfile.HEARING_AID) { + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), + "setBluetoothActiveDevice"); + } } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index bf20ae3b516d..57b19cda7c12 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -764,7 +764,7 @@ public class AudioDeviceInventory { /** only public for mocking/spying, do not call outside of AudioService */ // @GuardedBy("mDeviceBroker.mSetModeLock") @VisibleForTesting - @GuardedBy("mDeviceBroker.mDeviceStateLock") + //@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") public void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, int streamType) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 1cd267dee2fe..d34661d4d6ac 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -1290,15 +1290,19 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { mService.getHdmiCecNetwork().removeCecSwitches(portId); } - // Turning System Audio Mode off when the AVR is unlugged or standby. - // When the device is not unplugged but reawaken from standby, we check if the System - // Audio Control Feature is enabled or not then decide if turning SAM on/off accordingly. - if (getAvrDeviceInfo() != null && portId == getAvrDeviceInfo().getPortId()) { - HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); - if (!connected) { - setSystemAudioMode(false); - } else { - onNewAvrAdded(getAvrDeviceInfo()); + if (!mService.isEarcEnabled() || !mService.isEarcSupported()) { + HdmiDeviceInfo avr = getAvrDeviceInfo(); + if (avr != null + && portId == avr.getPortId() + && isConnectedToArcPort(avr.getPhysicalAddress())) { + HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); + if (connected) { + if (mArcEstablished) { + enableAudioReturnChannel(true); + } + } else { + enableAudioReturnChannel(false); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index eaf754dc7520..e0e825d9147a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3617,7 +3617,7 @@ public class HdmiControlService extends SystemService { } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) protected boolean isEarcSupported() { synchronized (mLock) { return mEarcSupported; diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java index f3532e5ce7e9..b6c0e5d970a1 100644 --- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java +++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java @@ -53,6 +53,7 @@ final class NewDeviceAction extends HdmiCecFeatureAction { private int mVendorId; private String mDisplayName; private int mTimeoutRetry; + private HdmiDeviceInfo mOldDeviceInfo; /** * Constructor. @@ -73,6 +74,38 @@ final class NewDeviceAction extends HdmiCecFeatureAction { @Override public boolean start() { + mOldDeviceInfo = + localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress); + // If there's deviceInfo with same (logical address, physical address) set + // Then addCecDevice should be delayed until system information process is finished + if (mOldDeviceInfo != null + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) { + Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:[" + + mOldDeviceInfo.toString() + "]"); + } else { + // Add the device ahead with default information to handle <Active Source> + // promptly, rather than waiting till the new device action is finished. + Slog.d(TAG, "Start NewDeviceAction with default deviceInfo"); + HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder() + .setLogicalAddress(mDeviceLogicalAddress) + .setPhysicalAddress(mDevicePhysicalAddress) + .setPortId(tv().getPortId(mDevicePhysicalAddress)) + .setDeviceType(mDeviceType) + .setVendorId(Constants.VENDOR_ID_UNKNOWN) + .build(); + // If a deviceInfo with same logical address but different physical address exists + // We should remove the old deviceInfo first + // This will happen if the interval between unplugging and plugging device is too short + // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly + // plugged device violates HDMI Spec and uses an occupied logical address + if (mOldDeviceInfo != null) { + Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: " + + mDevicePhysicalAddress); + localDevice().mService.getHdmiCecNetwork().removeCecDevice( + localDevice(), mDeviceLogicalAddress); + } + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); + } requestOsdName(true); return true; } @@ -182,14 +215,30 @@ final class NewDeviceAction extends HdmiCecFeatureAction { .setVendorId(mVendorId) .setDisplayName(mDisplayName) .build(); - localDevice().mService.getHdmiCecNetwork().updateCecDevice(deviceInfo); - // Consume CEC messages we already got for this newly found device. - tv().processDelayedMessages(mDeviceLogicalAddress); + // Check if oldDevice is same as newDevice + // If so, don't add newDevice info, preventing ARC or HDMI source re-connection + if (mOldDeviceInfo != null + && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress + && mOldDeviceInfo.getDeviceType() == mDeviceType + && mOldDeviceInfo.getVendorId() == mVendorId + && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) { + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device"); + Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString() + + "]; New:[" + deviceInfo.toString() + "]"); + } else { + Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]"); + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); - if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, - mDeviceLogicalAddress)) { - tv().onNewAvrAdded(deviceInfo); + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, + mDeviceLogicalAddress)) { + tv().onNewAvrAdded(deviceInfo); + } } } 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 f6571d94d554..550aed51c8e2 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -304,7 +304,7 @@ public final class MediaProjectionManagerService extends SystemService } @VisibleForTesting - void addCallback(final IMediaProjectionWatcherCallback callback) { + MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -314,6 +314,7 @@ public final class MediaProjectionManagerService extends SystemService synchronized (mLock) { mCallbackDelegate.add(callback); linkDeathRecipientLocked(callback, deathRecipient); + return mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null; } } @@ -786,11 +787,11 @@ public final class MediaProjectionManagerService extends SystemService @Override //Binder call @EnforcePermission(MANAGE_MEDIA_PROJECTION) - public void addCallback(final IMediaProjectionWatcherCallback callback) { + public MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { addCallback_enforcePermission(); final long token = Binder.clearCallingIdentity(); try { - MediaProjectionManagerService.this.addCallback(callback); + return MediaProjectionManagerService.this.addCallback(callback); } finally { Binder.restoreCallingIdentity(token); } @@ -1244,7 +1245,7 @@ public final class MediaProjectionManagerService extends SystemService } public MediaProjectionInfo getProjectionInfo() { - return new MediaProjectionInfo(packageName, userHandle); + return new MediaProjectionInfo(packageName, userHandle, mLaunchCookie); } boolean requiresForegroundService() { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9ddc362769f6..c9f45e523e3b 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -12103,6 +12103,7 @@ public class NotificationManagerService extends SystemService { return true; } + long token = Binder.clearCallingIdentity(); try { if (mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, uid) == PERMISSION_GRANTED || mPackageManagerInternal.isPlatformSigned(pkg)) { @@ -12129,6 +12130,8 @@ public class NotificationManagerService extends SystemService { } } catch (RemoteException e) { Slog.e(TAG, "Failed to check trusted status of listener", e); + } finally { + Binder.restoreCallingIdentity(token); } return false; } diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 7f0aadce3143..c110fb67b54f 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -75,11 +75,9 @@ public class BackgroundInstallControlService extends SystemService { private static final int MSG_PACKAGE_ADDED = 1; private static final int MSG_PACKAGE_REMOVED = 2; - private final Context mContext; private final BinderService mBinderService; private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; - private final UsageStatsManagerInternal mUsageStatsManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final Handler mHandler; private final File mDiskFile; @@ -99,14 +97,14 @@ public class BackgroundInstallControlService extends SystemService { @VisibleForTesting BackgroundInstallControlService(@NonNull Injector injector) { super(injector.getContext()); - mContext = injector.getContext(); mPackageManager = injector.getPackageManager(); mPackageManagerInternal = injector.getPackageManagerInternal(); mPermissionManager = injector.getPermissionManager(); mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); - mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal(); - mUsageStatsManagerInternal.registerListener( + UsageStatsManagerInternal usageStatsManagerInternal = + injector.getUsageStatsManagerInternal(); + usageStatsManagerInternal.registerListener( (userId, event) -> mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, diff --git a/services/core/java/com/android/server/pm/DataLoaderManagerService.java b/services/core/java/com/android/server/pm/DataLoaderManagerService.java index 888e1c26206c..c25cea65376b 100644 --- a/services/core/java/com/android/server/pm/DataLoaderManagerService.java +++ b/services/core/java/com/android/server/pm/DataLoaderManagerService.java @@ -47,7 +47,6 @@ import java.util.List; public class DataLoaderManagerService extends SystemService { private static final String TAG = "DataLoaderManager"; private final Context mContext; - private final HandlerThread mThread; private final Handler mHandler; private final DataLoaderManagerBinderService mBinderService; private final SparseArray<DataLoaderServiceConnection> mServiceConnections = @@ -57,10 +56,10 @@ public class DataLoaderManagerService extends SystemService { super(context); mContext = context; - mThread = new HandlerThread(TAG); - mThread.start(); + HandlerThread thread = new HandlerThread(TAG); + thread.start(); - mHandler = new Handler(mThread.getLooper()); + mHandler = new Handler(thread.getLooper()); mBinderService = new DataLoaderManagerBinderService(); } diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index e3bbd2d8d17e..f987d4ae8999 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -107,9 +107,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Nullable private final ComponentName mInstantAppResolverSettingsComponent; - @NonNull - private final String mRequiredSupplementalProcessPackage; - @Nullable private final String mServicesExtensionPackageName; @@ -125,7 +122,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @NonNull PackageInstallerService installerService, @NonNull PackageProperty packageProperty, @NonNull ComponentName resolveComponentName, @Nullable ComponentName instantAppResolverSettingsComponent, - @NonNull String requiredSupplementalProcessPackage, @Nullable String servicesExtensionPackageName, @Nullable String sharedSystemSharedLibraryPackageName) { mService = service; @@ -140,7 +136,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { mPackageProperty = packageProperty; mResolveComponentName = resolveComponentName; mInstantAppResolverSettingsComponent = instantAppResolverSettingsComponent; - mRequiredSupplementalProcessPackage = requiredSupplementalProcessPackage; mServicesExtensionPackageName = servicesExtensionPackageName; mSharedSystemSharedLibraryPackageName = sharedSystemSharedLibraryPackageName; } diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 3e5759a88213..b18f2bf21a2b 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -51,6 +51,7 @@ import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ArchivedActivityParcel; +import android.content.pm.ArchivedPackageInfo; import android.content.pm.ArchivedPackageParcel; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; @@ -402,23 +403,30 @@ public class PackageArchiver { installerPackage, /* flags= */ 0, userId); if (installerInfo == null) { // Should never happen because we just fetched the installerInfo. - Slog.e(TAG, "Couldnt find installer " + installerPackage); + Slog.e(TAG, "Couldn't find installer " + installerPackage); return null; } + final int iconSize = mContext.getSystemService( + ActivityManager.class).getLauncherLargeIconSize(); + + var info = new ArchivedPackageInfo(archivedPackage); try { - var packageName = archivedPackage.packageName; - var mainActivities = archivedPackage.archivedActivities; - List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.length); - for (int i = 0, size = mainActivities.length; i < size; ++i) { - var mainActivity = mainActivities[i]; - Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i); + var packageName = info.getPackageName(); + var mainActivities = info.getLauncherActivities(); + List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); + for (int i = 0, size = mainActivities.size(); i < size; ++i) { + var mainActivity = mainActivities.get(i); + Path iconPath = storeDrawable( + packageName, mainActivity.getIcon(), userId, i, iconSize); + Path monochromePath = storeDrawable( + packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize); ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( - mainActivity.title, - mainActivity.originalComponentName, + mainActivity.getLabel().toString(), + mainActivity.getComponentName(), iconPath, - null); + monochromePath); archiveActivityInfos.add(activityInfo); } @@ -452,21 +460,6 @@ public class PackageArchiver { return new ArchiveState(archiveActivityInfos, installerTitle); } - // TODO(b/298452477) Handle monochrome icons. - private static Path storeIconForParcel(String packageName, ArchivedActivityParcel mainActivity, - @UserIdInt int userId, int index) throws IOException { - if (mainActivity.iconBitmap == null) { - return null; - } - File iconsDir = createIconsDir(packageName, userId); - File iconFile = new File(iconsDir, index + ".png"); - try (FileOutputStream out = new FileOutputStream(iconFile)) { - out.write(mainActivity.iconBitmap); - out.flush(); - } - return iconFile.toPath(); - } - @VisibleForTesting Path storeIcon(String packageName, LauncherActivityInfo mainActivity, @UserIdInt int userId, int index, int iconSize) throws IOException { @@ -475,9 +468,18 @@ public class PackageArchiver { // The app doesn't define an icon. No need to store anything. return null; } + return storeDrawable(packageName, mainActivity.getIcon(/* density= */ 0), userId, index, + iconSize); + } + + private static Path storeDrawable(String packageName, @Nullable Drawable iconDrawable, + @UserIdInt int userId, int index, int iconSize) throws IOException { + if (iconDrawable == null) { + return null; + } File iconsDir = createIconsDir(packageName, userId); File iconFile = new File(iconsDir, index + ".png"); - Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize); + Bitmap icon = drawableToBitmap(iconDrawable, iconSize); try (FileOutputStream out = new FileOutputStream(iconFile)) { // Note: Quality is ignored for PNGs. if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 5225529ef001..c5b5a761497d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -4659,8 +4659,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPreferredActivityHelper, mResolveIntentHelper, mDomainVerificationManager, mDomainVerificationConnection, mInstallerService, mPackageProperty, mResolveComponentName, mInstantAppResolverSettingsComponent, - mRequiredSdkSandboxPackage, mServicesExtensionPackageName, - mSharedSystemSharedLibraryPackageName); + mServicesExtensionPackageName, mSharedSystemSharedLibraryPackageName); } @Override diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java index 98533725371f..524252c1469f 100644 --- a/services/core/java/com/android/server/pm/ProtectedPackages.java +++ b/services/core/java/com/android/server/pm/ProtectedPackages.java @@ -57,11 +57,8 @@ public class ProtectedPackages { @GuardedBy("this") private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>(); - private final Context mContext; - public ProtectedPackages(Context context) { - mContext = context; - mDeviceProvisioningPackage = mContext.getResources().getString( + mDeviceProvisioningPackage = context.getResources().getString( R.string.config_deviceProvisioningPackage); } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 7bd6a43969ba..3e3b72caaf2e 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -67,7 +67,6 @@ final class RemovePackageHelper { private final PackageManagerService mPm; private final IncrementalManager mIncrementalManager; private final Installer mInstaller; - private final UserManagerInternal mUserManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final SharedLibrariesImpl mSharedLibraries; private final AppDataHelper mAppDataHelper; @@ -79,7 +78,6 @@ final class RemovePackageHelper { mPm = pm; mIncrementalManager = mPm.mInjector.getIncrementalManager(); mInstaller = mPm.mInjector.getInstaller(); - mUserManagerInternal = mPm.mInjector.getUserManagerInternal(); mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal(); mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl(); mAppDataHelper = appDataHelper; diff --git a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java index 7f6f684e0b68..aa52522cfe46 100644 --- a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java +++ b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java @@ -31,7 +31,6 @@ import java.io.PrintWriter; * The access to it must be guarded with the shortcut manager lock. */ public class ShortcutNonPersistentUser { - private final ShortcutService mService; private final int mUserId; @@ -49,8 +48,7 @@ public class ShortcutNonPersistentUser { */ private final ArraySet<String> mHostPackageSet = new ArraySet<>(); - public ShortcutNonPersistentUser(ShortcutService service, int userId) { - mService = service; + public ShortcutNonPersistentUser(int userId) { mUserId = userId; } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 446c6293aa35..96c205c00ea9 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1378,7 +1378,7 @@ public class ShortcutService extends IShortcutService.Stub { ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) { ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId); if (ret == null) { - ret = new ShortcutNonPersistentUser(this, userId); + ret = new ShortcutNonPersistentUser(userId); mShortcutNonPersistentUsers.put(userId, ret); } return ret; diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java index 4c42c2dd0850..1d414011cff3 100644 --- a/services/core/java/com/android/server/pm/UserDataPreparer.java +++ b/services/core/java/com/android/server/pm/UserDataPreparer.java @@ -141,7 +141,7 @@ class UserDataPreparer { // If internal storage of the system user fails to prepare on first boot, then // things are *really* broken, so we might as well reboot to recovery right away. try { - Log.wtf(TAG, "prepareUserData failed for user " + userId, e); + Log.e(TAG, "prepareUserData failed for user " + userId, e); if (isNewUser && userId == UserHandle.USER_SYSTEM && volumeUuid == null) { RecoverySystem.rebootPromptAndWipeUserData(mContext, "failed to prepare internal storage for system user"); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index ee40e18b7d9f..40f226435194 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -77,7 +77,6 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.TriFunction; import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider; import com.android.server.pm.pkg.AndroidPackage; @@ -114,9 +113,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { /** Internal connection to the package manager */ private final PackageManagerInternal mPackageManagerInt; - /** Internal connection to the user manager */ - private final UserManagerInternal mUserManagerInt; - /** Map of OneTimePermissionUserManagers keyed by userId */ @GuardedBy("mLock") @NonNull @@ -148,7 +144,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { mContext = context; mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class); - mUserManagerInt = LocalServices.getService(UserManagerInternal.class); mAppOpsManager = context.getSystemService(AppOpsManager.class); mAttributionSourceRegistry = new AttributionSourceRegistry(context); @@ -1098,12 +1093,10 @@ public class PermissionManagerService extends IPermissionManager.Stub { private static final AtomicInteger sAttributionChainIds = new AtomicInteger(0); private final @NonNull Context mContext; - private final @NonNull AppOpsManager mAppOpsManager; private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal; PermissionCheckerService(@NonNull Context context) { mContext = context; - mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mPermissionManagerServiceInternal = LocalServices.getService(PermissionManagerServiceInternal.class); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 3afba39ad4af..6a5736269e51 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -279,7 +279,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt @NonNull private final int[] mGlobalGids; - private final HandlerThread mHandlerThread; private final Handler mHandler; private final Context mContext; private final MetricsLogger mMetricsLogger = new MetricsLogger(); @@ -432,10 +431,10 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } } - mHandlerThread = new ServiceThread(TAG, + HandlerThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); Watchdog.getInstance().addThread(mHandler); SystemConfig systemConfig = SystemConfig.getInstance(); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index e2269535d931..a172de0bb0ff 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -4413,8 +4413,8 @@ public final class PowerManagerService extends SystemService private boolean setPowerModeInternal(int mode, boolean enabled) { // Maybe filter the event. - if (mBatterySaverStateMachine == null || (mode == Mode.LAUNCH && enabled - && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled())) { + if (mode == Mode.LAUNCH && enabled && mBatterySaverStateMachine != null + && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled()) { return false; } return mNativeWrapper.nativeSetPowerMode(mode, enabled); diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 0467d0cd351d..7ab075e2f3a7 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -38,7 +38,16 @@ import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; +import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManager; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.ITvAdService; +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSession; +import android.media.tv.ad.ITvAdSessionCallback; +import android.media.tv.ad.TvAdService; +import android.media.tv.ad.TvAdServiceInfo; +import android.media.tv.flags.Flags; import android.media.tv.interactive.AppLinkInfo; import android.media.tv.interactive.ITvInteractiveAppClient; import android.media.tv.interactive.ITvInteractiveAppManager; @@ -110,6 +119,8 @@ public class TvInteractiveAppManagerService extends SystemService { @GuardedBy("mLock") private boolean mGetServiceListCalled = false; @GuardedBy("mLock") + private boolean mGetAdServiceListCalled = false; + @GuardedBy("mLock") private boolean mGetAppLinkInfoListCalled = false; private final UserManager mUserManager; @@ -256,6 +267,141 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void buildTvAdServiceListLocked(int userId, String[] updatedPackages) { + if (!Flags.enableAdServiceFw()) { + return; + } + UserState userState = getOrCreateUserStateLocked(userId); + userState.mPackageSet.clear(); + + if (DEBUG) { + Slogf.d(TAG, "buildTvAdServiceListLocked"); + } + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = pm.queryIntentServicesAsUser( + new Intent(TvAdService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + userId); + List<TvAdServiceInfo> serviceList = new ArrayList<>(); + + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_AD_SERVICE.equals(si.permission)) { + Slog.w(TAG, "Skipping TV AD service " + si.name + + ": it does not require the permission " + + android.Manifest.permission.BIND_TV_AD_SERVICE); + continue; + } + + ComponentName component = new ComponentName(si.packageName, si.name); + try { + TvAdServiceInfo info = new TvAdServiceInfo(mContext, component); + serviceList.add(info); + } catch (Exception e) { + Slogf.e(TAG, "failed to load TV AD service " + si.name, e); + continue; + } + userState.mPackageSet.add(si.packageName); + } + + // sort the service list by service id + Collections.sort(serviceList, Comparator.comparing(TvAdServiceInfo::getId)); + Map<String, TvAdServiceState> adServiceMap = new HashMap<>(); + for (TvAdServiceInfo info : serviceList) { + String serviceId = info.getId(); + if (DEBUG) { + Slogf.d(TAG, "add " + serviceId); + } + TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId); + if (adServiceState == null) { + adServiceState = new TvAdServiceState(); + } + adServiceState.mInfo = info; + adServiceState.mUid = getAdServiceUid(info); + adServiceState.mComponentName = info.getComponent(); + adServiceMap.put(serviceId, adServiceState); + } + + for (String serviceId : adServiceMap.keySet()) { + if (!userState.mAdServiceMap.containsKey(serviceId)) { + notifyAdServiceAddedLocked(userState, serviceId); + } else if (updatedPackages != null) { + // Notify the package updates + ComponentName component = adServiceMap.get(serviceId).mInfo.getComponent(); + for (String updatedPackage : updatedPackages) { + if (component.getPackageName().equals(updatedPackage)) { + updateAdServiceConnectionLocked(component, userId); + notifyAdServiceUpdatedLocked(userState, serviceId); + break; + } + } + } + } + + for (String serviceId : userState.mAdServiceMap.keySet()) { + if (!adServiceMap.containsKey(serviceId)) { + TvAdServiceInfo info = userState.mAdServiceMap.get(serviceId).mInfo; + AdServiceState serviceState = userState.mAdServiceStateMap.get(info.getComponent()); + if (serviceState != null) { + abortPendingCreateAdSessionRequestsLocked(serviceState, serviceId, userId); + } + notifyAdServiceRemovedLocked(userState, serviceId); + } + } + + userState.mIAppMap.clear(); + userState.mAdServiceMap = adServiceMap; + } + + @GuardedBy("mLock") + private void notifyAdServiceAddedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceAddedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceAdded(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceRemovedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceRemovedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceRemoved(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report removed AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceUpdatedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceUpdatedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceUpdated(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report updated AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") private void notifyInteractiveAppServiceAddedLocked(UserState userState, String iAppServiceId) { if (DEBUG) { Slog.d(TAG, "notifyInteractiveAppServiceAddedLocked(iAppServiceId=" @@ -340,6 +486,16 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private int getAdServiceUid(TvAdServiceInfo info) { + try { + return getContext().getPackageManager().getApplicationInfo( + info.getServiceInfo().packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + Slogf.w(TAG, "Unable to get UID for " + info, e); + return Process.INVALID_UID; + } + } + @Override public void onStart() { if (DEBUG) { @@ -357,6 +513,7 @@ public class TvInteractiveAppManagerService extends SystemService { synchronized (mLock) { buildTvInteractiveAppServiceListLocked(mCurrentUserId, null); buildAppLinkInfoLocked(mCurrentUserId); + buildTvAdServiceListLocked(mCurrentUserId, null); } } } @@ -372,6 +529,14 @@ public class TvInteractiveAppManagerService extends SystemService { } } } + private void buildTvAdServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvAdServiceListLocked(userId, packages); + } + } + } @Override public void onPackageUpdateFinished(String packageName, int uid) { @@ -379,6 +544,7 @@ public class TvInteractiveAppManagerService extends SystemService { // This callback is invoked when the TV interactive App service is reinstalled. // In this case, isReplacing() always returns true. buildTvInteractiveAppServiceList(new String[] { packageName }); + buildTvAdServiceList(new String[] { packageName }); } @Override @@ -390,6 +556,7 @@ public class TvInteractiveAppManagerService extends SystemService { // available. if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -403,6 +570,7 @@ public class TvInteractiveAppManagerService extends SystemService { } if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -418,6 +586,7 @@ public class TvInteractiveAppManagerService extends SystemService { return; } buildTvInteractiveAppServiceList(null); + buildTvAdServiceList(null); } @Override @@ -476,6 +645,7 @@ public class TvInteractiveAppManagerService extends SystemService { mCurrentUserId = userId; buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } } @@ -562,6 +732,7 @@ public class TvInteractiveAppManagerService extends SystemService { mRunningProfiles.add(userId); buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } @GuardedBy("mLock") @@ -619,7 +790,19 @@ public class TvInteractiveAppManagerService extends SystemService { Slog.e(TAG, "error in onSessionReleased", e); } } - removeSessionStateLocked(state.mSessionToken, state.mUserId); + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); + } + + @GuardedBy("mLock") + private void clearAdSessionAndNotifyClientLocked(AdSessionState state) { + if (state.mClient != null) { + try { + state.mClient.onSessionReleased(state.mSeq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onSessionReleased", e); + } + } + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); } private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId, @@ -655,6 +838,44 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked( + IBinder sessionToken, int callingUid, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + return getAdSessionStateLocked(sessionToken, callingUid, userState); + } + + @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked(IBinder sessionToken, int callingUid, + UserState userState) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState == null) { + throw new SessionNotFoundException("Session state not found for token " + sessionToken); + } + // Only the application that requested this session or the system can access it. + if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) { + throw new SecurityException("Illegal access to the session with token " + sessionToken + + " from uid " + callingUid); + } + return sessionState; + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + return getAdSessionLocked(getAdSessionStateLocked(sessionToken, callingUid, userId)); + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked(AdSessionState sessionState) { + ITvAdSession session = sessionState.mSession; + if (session == null) { + throw new IllegalStateException("Session not yet created for token " + + sessionState.mSessionToken); + } + return session; + } + + @GuardedBy("mLock") private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) { UserState userState = getOrCreateUserStateLocked(userId); return getSessionStateLocked(sessionToken, callingUid, userState); @@ -691,10 +912,200 @@ public class TvInteractiveAppManagerService extends SystemService { return session; } private final class TvAdBinderService extends ITvAdManager.Stub { + + @Override + public List<TvAdServiceInfo> getTvAdServiceList(int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "getTvAdServiceList"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!mGetAdServiceListCalled) { + buildTvAdServiceListLocked(userId, null); + mGetAdServiceListCalled = true; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + List<TvAdServiceInfo> adServiceList = new ArrayList<>(); + for (TvAdServiceState state : userState.mAdServiceMap.values()) { + adServiceList.add(state.mInfo); + } + return adServiceList; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void createSession(final ITvAdClient client, final String serviceId, String type, + int seq, int userId) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, + userId, "createSession"); + final long identity = Binder.clearCallingIdentity(); + + try { + synchronized (mLock) { + if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) { + // Only current user and its running profiles can create sessions. + // Let the client get onConnectionFailed callback for this case. + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + TvAdServiceState adState = userState.mAdMap.get(serviceId); + if (adState == null) { + Slogf.w(TAG, "Failed to find state for serviceId=" + serviceId); + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + AdServiceState serviceState = + userState.mAdServiceStateMap.get(adState.mComponentName); + if (serviceState == null) { + int tasUid = PackageManager.getApplicationInfoAsUserCached( + adState.mComponentName.getPackageName(), 0, resolvedUserId).uid; + serviceState = new AdServiceState( + adState.mComponentName, serviceId, resolvedUserId); + userState.mAdServiceStateMap.put(adState.mComponentName, serviceState); + } + // Send a null token immediately while reconnecting. + if (serviceState.mReconnecting) { + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + + // Create a new session token and a session state. + IBinder sessionToken = new Binder(); + AdSessionState sessionState = new AdSessionState(sessionToken, serviceId, type, + adState.mComponentName, client, seq, callingUid, + callingPid, resolvedUserId); + + // Add them to the global session state map of the current user. + userState.mAdSessionStateMap.put(sessionToken, sessionState); + + // Also, add them to the session state map of the current service. + serviceState.mSessionTokens.add(sessionToken); + + if (serviceState.mService != null) { + if (!createAdSessionInternalLocked(serviceState.mService, sessionToken, + resolvedUserId)) { + removeAdSessionStateLocked(sessionToken, resolvedUserId); + } + } else { + updateAdServiceConnectionLocked(adState.mComponentName, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void releaseSession(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")"); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "releaseSession"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setSurface(IBinder sessionToken, Surface surface, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setSurface"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).setSurface(surface); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in setSurface", e); + } + } + } finally { + if (surface != null) { + // surface is not used in TvInteractiveAppManagerService. + surface.release(); + } + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width, + int height, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "dispatchSurfaceChanged"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + AdSessionState sessionState = getAdSessionStateLocked( + sessionToken, callingUid, resolvedUserId); + getAdSessionLocked(sessionState).dispatchSurfaceChanged(format, width, + height); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in dispatchSurfaceChanged", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public void startAdService(IBinder sessionToken, int userId) { } + @Override + public void registerCallback(final ITvAdManagerCallback callback, int userId) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "registerCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final UserState userState = getOrCreateUserStateLocked(resolvedUserId); + if (!userState.mAdCallbacks.register(callback)) { + Slog.e(TAG, "client process has already died"); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void unregisterCallback(ITvAdManagerCallback callback, int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "unregisterCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + userState.mAdCallbacks.unregister(callback); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { @@ -927,7 +1338,7 @@ public class TvInteractiveAppManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + releaseAdSessionLocked(sessionToken, callingUid, resolvedUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -1471,6 +1882,32 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendSelectedTrackInfo(IBinder sessionToken, List<TvTrackInfo> tracks, + int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendSelectedTrackInfo(tracks=%s)", tracks.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendSelectedTrackInfo"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendSelectedTrackInfo(tracks); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendSelectedTrackInfo", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) { if (DEBUG) { Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId); @@ -2134,6 +2571,17 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void sendAdSessionTokenToClientLocked( + ITvAdClient client, String serviceId, IBinder sessionToken, + InputChannel channel, int seq) { + try { + client.onSessionCreated(serviceId, sessionToken, channel, seq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onSessionCreated", e); + } + } + + @GuardedBy("mLock") private boolean createSessionInternalLocked( ITvInteractiveAppService service, IBinder sessionToken, int userId) { UserState userState = getOrCreateUserStateLocked(userId); @@ -2163,6 +2611,58 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private boolean createAdSessionInternalLocked( + ITvAdService service, IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (DEBUG) { + Slogf.d(TAG, "createAdSessionInternalLocked(iAppServiceId=" + + sessionState.mAdServiceId + ")"); + } + InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString()); + + // Set up a callback to send the session token. + ITvAdSessionCallback callback = new AdSessionCallback(sessionState, channels); + + boolean created = true; + // Create a session. When failed, send a null token immediately. + try { + service.createSession( + channels[1], callback, sessionState.mAdServiceId, sessionState.mType); + } catch (RemoteException e) { + Slogf.e(TAG, "error in createSession", e); + sendAdSessionTokenToClientLocked(sessionState.mClient, sessionState.mAdServiceId, null, + null, sessionState.mSeq); + created = false; + } + channels[1].dispose(); + return created; + } + + @GuardedBy("mLock") + @Nullable + private AdSessionState releaseAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + AdSessionState sessionState = null; + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, userId); + UserState userState = getOrCreateUserStateLocked(userId); + if (sessionState.mSession != null) { + sessionState.mSession.asBinder().unlinkToDeath(sessionState, 0); + sessionState.mSession.release(); + } + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in releaseSession", e); + } finally { + if (sessionState != null) { + sessionState.mSession = null; + } + } + removeAdSessionStateLocked(sessionToken, userId); + return sessionState; + } + + @GuardedBy("mLock") @Nullable private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) { SessionState sessionState = null; @@ -2215,6 +2715,36 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void removeAdSessionStateLocked(IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + + // Remove the session state from the global session state map of the current user. + AdSessionState sessionState = userState.mAdSessionStateMap.remove(sessionToken); + + if (sessionState == null) { + Slogf.e(TAG, "sessionState null, no more remove session action!"); + return; + } + + // Also remove the session token from the session token list of the current client and + // service. + ClientState clientState = userState.mClientStateMap.get(sessionState.mClient.asBinder()); + if (clientState != null) { + clientState.mSessionTokens.remove(sessionToken); + if (clientState.isEmpty()) { + userState.mClientStateMap.remove(sessionState.mClient.asBinder()); + sessionState.mClient.asBinder().unlinkToDeath(clientState, 0); + } + } + + AdServiceState serviceState = userState.mAdServiceStateMap.get(sessionState.mComponent); + if (serviceState != null) { + serviceState.mSessionTokens.remove(sessionToken); + } + updateAdServiceConnectionLocked(sessionState.mComponent, userId); + } + + @GuardedBy("mLock") private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState, String iAppServiceId, int userId) { // Let clients know the create session requests are failed. @@ -2237,6 +2767,28 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void abortPendingCreateAdSessionRequestsLocked(AdServiceState serviceState, + String serviceId, int userId) { + // Let clients know the create session requests are failed. + UserState userState = getOrCreateUserStateLocked(userId); + List<AdSessionState> sessionsToAbort = new ArrayList<>(); + for (IBinder sessionToken : serviceState.mSessionTokens) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState.mSession == null + && (serviceState == null + || sessionState.mAdServiceId.equals(serviceId))) { + sessionsToAbort.add(sessionState); + } + } + for (AdSessionState sessionState : sessionsToAbort) { + removeAdSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId); + sendAdSessionTokenToClientLocked(sessionState.mClient, + sessionState.mAdServiceId, null, null, sessionState.mSeq); + } + updateAdServiceConnectionLocked(serviceState.mComponent, userId); + } + + @GuardedBy("mLock") private void updateServiceConnectionLocked(ComponentName component, int userId) { UserState userState = getOrCreateUserStateLocked(userId); ServiceState serviceState = userState.mServiceStateMap.get(component); @@ -2284,10 +2836,64 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @GuardedBy("mLock") + private void updateAdServiceConnectionLocked(ComponentName component, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(component); + if (serviceState == null) { + return; + } + if (serviceState.mReconnecting) { + if (!serviceState.mSessionTokens.isEmpty()) { + // wait until all the sessions are removed. + return; + } + serviceState.mReconnecting = false; + } + + boolean shouldBind = (!serviceState.mSessionTokens.isEmpty()) + || (!serviceState.mPendingAppLinkCommand.isEmpty()); + + if (serviceState.mService == null && shouldBind) { + // This means that the service is not yet connected but its state indicates that we + // have pending requests. Then, connect the service. + if (serviceState.mBound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + return; + } + if (DEBUG) { + Slogf.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); + } + + Intent i = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); + serviceState.mBound = mContext.bindServiceAsUser( + i, serviceState.mConnection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + } else if (serviceState.mService != null && !shouldBind) { + // This means that the service is already connected but its state indicates that we have + // nothing to do with it. Then, disconnect the service. + if (DEBUG) { + Slogf.d(TAG, "unbindService(service=" + component + ")"); + } + mContext.unbindService(serviceState.mConnection); + userState.mAdServiceStateMap.remove(component); + } + } + private static final class UserState { private final int mUserId; + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdMap = new HashMap<>(); + // A mapping from the name of a TV Interactive App service to its state. + private final Map<ComponentName, AdServiceState> mAdServiceStateMap = new HashMap<>(); + // A mapping from the token of a TV Interactive App session to its state. + private final Map<IBinder, AdSessionState> mAdSessionStateMap = new HashMap<>(); // A mapping from the TV Interactive App ID to its TvInteractiveAppState. private Map<String, TvInteractiveAppState> mIAppMap = new HashMap<>(); + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdServiceMap = new HashMap<>(); // A mapping from the token of a client to its state. private final Map<IBinder, ClientState> mClientStateMap = new HashMap<>(); // A mapping from the name of a TV Interactive App service to its state. @@ -2299,6 +2905,8 @@ public class TvInteractiveAppManagerService extends SystemService { private final Set<String> mPackageSet = new HashSet<>(); // A list of all app link infos. private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>(); + private final RemoteCallbackList<ITvAdManagerCallback> mAdCallbacks = + new RemoteCallbackList<>(); // A list of callbacks. private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks = @@ -2317,7 +2925,16 @@ public class TvInteractiveAppManagerService extends SystemService { private int mIAppNumber; } + private static final class TvAdServiceState { + private String mAdServiceId; + private ComponentName mComponentName; + private TvAdServiceInfo mInfo; + private int mUid; + private int mAdNumber; + } + private final class SessionState implements IBinder.DeathRecipient { + // TODO: rename SessionState and reorganize classes / methods of this file private final IBinder mSessionToken; private ITvInteractiveAppSession mSession; private final String mIAppServiceId; @@ -2359,6 +2976,49 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionState implements IBinder.DeathRecipient { + private final IBinder mSessionToken; + private ITvAdSession mSession; + private final String mAdServiceId; + + private final String mType; + private final ITvAdClient mClient; + private final int mSeq; + private final ComponentName mComponent; + + // The UID of the application that created the session. + // The application is usually the TV app. + private final int mCallingUid; + + // The PID of the application that created the session. + // The application is usually the TV app. + private final int mCallingPid; + + private final int mUserId; + + private AdSessionState(IBinder sessionToken, String serviceId, String type, + ComponentName componentName, ITvAdClient client, int seq, + int callingUid, int callingPid, int userId) { + mSessionToken = sessionToken; + mAdServiceId = serviceId; + mType = type; + mComponent = componentName; + mClient = client; + mSeq = seq; + mCallingUid = callingUid; + mCallingPid = callingPid; + mUserId = userId; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mSession = null; + clearAdSessionAndNotifyClientLocked(this); + } + } + } + private final class ClientState implements IBinder.DeathRecipient { private final List<IBinder> mSessionTokens = new ArrayList<>(); @@ -2429,6 +3089,29 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceState { + private final List<IBinder> mSessionTokens = new ArrayList<>(); + private final ServiceConnection mConnection; + private final ComponentName mComponent; + private final String mAdServiceId; + private final List<Bundle> mPendingAppLinkCommand = new ArrayList<>(); + + private ITvAdService mService; + private AdServiceCallback mCallback; + private boolean mBound; + private boolean mReconnecting; + + private AdServiceState(ComponentName component, String tasId, int userId) { + mComponent = component; + mConnection = new AdServiceConnection(component, userId); + mAdServiceId = tasId; + } + + private void addPendingAppLinkCommand(Bundle command) { + mPendingAppLinkCommand.add(command); + } + } + private final class InteractiveAppServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -2542,6 +3225,98 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceConnection implements ServiceConnection { + private final ComponentName mComponent; + private final int mUserId; + + private AdServiceConnection(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (DEBUG) { + Slogf.d(TAG, "onServiceConnected(component=" + component + ")"); + } + synchronized (mLock) { + UserState userState = getUserStateLocked(mUserId); + if (userState == null) { + // The user was removed while connecting. + mContext.unbindService(this); + return; + } + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + serviceState.mService = ITvAdService.Stub.asInterface(service); + + // Register a callback, if we need to. + if (serviceState.mCallback == null) { + serviceState.mCallback = new AdServiceCallback(mComponent, mUserId); + try { + serviceState.mService.registerCallback(serviceState.mCallback); + } catch (RemoteException e) { + Slog.e(TAG, "error in registerCallback", e); + } + } + + if (!serviceState.mPendingAppLinkCommand.isEmpty()) { + for (Iterator<Bundle> it = serviceState.mPendingAppLinkCommand.iterator(); + it.hasNext(); ) { + Bundle command = it.next(); + final long identity = Binder.clearCallingIdentity(); + try { + serviceState.mService.sendAppLinkCommand(command); + it.remove(); + } catch (RemoteException e) { + Slogf.e(TAG, "error in sendAppLinkCommand(" + command + + ") when onServiceConnected", e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.mSessionTokens) { + if (!createAdSessionInternalLocked( + serviceState.mService, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeAdSessionStateLocked(sessionToken, mUserId); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (DEBUG) { + Slogf.d(TAG, "onServiceDisconnected(component=" + component + ")"); + } + if (!mComponent.equals(component)) { + throw new IllegalArgumentException("Mismatched ComponentName: " + + mComponent + " (expected), " + component + " (actual)."); + } + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(mUserId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + if (serviceState != null) { + serviceState.mReconnecting = true; + serviceState.mBound = false; + serviceState.mService = null; + serviceState.mCallback = null; + + abortPendingCreateAdSessionRequestsLocked(serviceState, null, mUserId); + } + } + } + } + + private final class ServiceCallback extends ITvInteractiveAppServiceCallback.Stub { private final ComponentName mComponent; private final int mUserId; @@ -2567,6 +3342,17 @@ public class TvInteractiveAppManagerService extends SystemService { } } + + private final class AdServiceCallback extends ITvAdServiceCallback.Stub { + private final ComponentName mComponent; + private final int mUserId; + + AdServiceCallback(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + } + private final class SessionCallback extends ITvInteractiveAppSessionCallback.Stub { private final SessionState mSessionState; private final InputChannel[] mInputChannels; @@ -2798,6 +3584,23 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestSelectedTrackInfo() { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestSelectedTrackInfo"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestSelectedTrackInfo(mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestSelectedTrackInfo", e); + } + } + } + + @Override public void onRequestCurrentTvInputId() { synchronized (mLock) { if (DEBUG) { @@ -3110,6 +3913,85 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionCallback extends ITvAdSessionCallback.Stub { + private final AdSessionState mSessionState; + private final InputChannel[] mInputChannels; + + AdSessionCallback(AdSessionState sessionState, InputChannel[] channels) { + mSessionState = sessionState; + mInputChannels = channels; + } + + @Override + public void onSessionCreated(ITvAdSession session) { + if (DEBUG) { + Slogf.d(TAG, "onSessionCreated(adServiceId=" + + mSessionState.mAdServiceId + ")"); + } + synchronized (mLock) { + mSessionState.mSession = session; + if (session != null && addAdSessionTokenToClientStateLocked(session)) { + sendAdSessionTokenToClientLocked( + mSessionState.mClient, + mSessionState.mAdServiceId, + mSessionState.mSessionToken, + mInputChannels[0], + mSessionState.mSeq); + } else { + removeAdSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId); + sendAdSessionTokenToClientLocked(mSessionState.mClient, + mSessionState.mAdServiceId, null, null, mSessionState.mSeq); + } + mInputChannels[0].dispose(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + + ", right=" + right + ", bottom=" + bottom + ",)"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onLayoutSurface(left, top, right, bottom, + mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onLayoutSurface", e); + } + } + } + + @GuardedBy("mLock") + private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) { + try { + session.asBinder().linkToDeath(mSessionState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "session process has already died", e); + return false; + } + + IBinder clientToken = mSessionState.mClient.asBinder(); + UserState userState = getOrCreateUserStateLocked(mSessionState.mUserId); + ClientState clientState = userState.mClientStateMap.get(clientToken); + if (clientState == null) { + clientState = new ClientState(clientToken, mSessionState.mUserId); + try { + clientToken.linkToDeath(clientState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "client process has already died", e); + return false; + } + userState.mClientStateMap.put(clientToken, clientState); + } + clientState.mSessionTokens.add(mSessionState.mSessionToken); + return true; + } + } + private static class SessionNotFoundException extends IllegalArgumentException { SessionNotFoundException(String name) { super(name); diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java new file mode 100644 index 000000000000..5f488b769885 --- /dev/null +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -0,0 +1,284 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR; + +import android.media.projection.IMediaProjectionManager; +import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.ContentRecordingSession; +import android.window.IScreenRecordingCallback; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; + +public class ScreenRecordingCallbackController { + + private final class Callback implements IBinder.DeathRecipient { + + IScreenRecordingCallback mCallback; + int mUid; + + Callback(IScreenRecordingCallback callback, int uid) { + this.mCallback = callback; + this.mUid = uid; + } + + public void binderDied() { + unregister(mCallback); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>(); + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>(); + + private final WindowManagerService mWms; + + @GuardedBy("WindowManagerService.mGlobalLock") + private WindowContainer<WindowContainer> mRecordedWC; + + private boolean mWatcherCallbackRegistered = false; + + private final class MediaProjectionWatcherCallback extends + IMediaProjectionWatcherCallback.Stub { + @Override + public void onStart(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStart(mediaProjectionInfo); + } + + @Override + public void onStop(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStop(); + } + + @Override + public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, + ContentRecordingSession contentRecordingSession) { + } + } + + ScreenRecordingCallbackController(WindowManagerService wms) { + mWms = wms; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo) { + if (mediaProjectionInfo.getLaunchCookie() == null) { + mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); + } else { + mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie + == mediaProjectionInfo.getLaunchCookie()).getTask(); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void ensureMediaProjectionWatcherCallbackRegistered() { + if (mWatcherCallbackRegistered) { + return; + } + + IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + IMediaProjectionManager mediaProjectionManager = + IMediaProjectionManager.Stub.asInterface(binder); + + long identityToken = Binder.clearCallingIdentity(); + MediaProjectionInfo mediaProjectionInfo = null; + try { + mediaProjectionInfo = mediaProjectionManager.addCallback( + new MediaProjectionWatcherCallback()); + mWatcherCallbackRegistered = true; + } catch (RemoteException e) { + ProtoLog.e(WM_ERROR, "Failed to register MediaProjectionWatcherCallback"); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (mediaProjectionInfo != null) { + setRecordedWindowContainer(mediaProjectionInfo); + } + } + + boolean register(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + ensureMediaProjectionWatcherCallbackRegistered(); + + IBinder binder = callback.asBinder(); + int uid = Binder.getCallingUid(); + + if (mCallbacks.containsKey(binder)) { + return mLastInvokedStateByUid.get(uid); + } + + Callback callbackInfo = new Callback(callback, uid); + try { + binder.linkToDeath(callbackInfo, 0); + } catch (RemoteException e) { + return false; + } + + boolean uidInRecording = uidHasRecordedActivity(callbackInfo.mUid); + mLastInvokedStateByUid.put(callbackInfo.mUid, uidInRecording); + mCallbacks.put(binder, callbackInfo); + return uidInRecording; + } + } + + void unregister(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + IBinder binder = callback.asBinder(); + Callback callbackInfo = mCallbacks.remove(binder); + binder.unlinkToDeath(callbackInfo, 0); + + boolean uidHasCallback = false; + for (Callback cb : mCallbacks.values()) { + if (cb.mUid == callbackInfo.mUid) { + uidHasCallback = true; + break; + } + } + if (!uidHasCallback) { + mLastInvokedStateByUid.remove(callbackInfo.mUid); + } + } + } + + private void onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo) { + synchronized (mWms.mGlobalLock) { + setRecordedWindowContainer(mediaProjectionInfo); + dispatchCallbacks(getRecordedUids(), true /* visibleInScreenRecording*/); + } + } + + private void onScreenRecordingStop() { + synchronized (mWms.mGlobalLock) { + dispatchCallbacks(getRecordedUids(), false /*visibleInScreenRecording*/); + mRecordedWC = null; + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + void onProcessActivityVisibilityChanged(int uid, boolean processVisible) { + // If recording isn't active or there's no registered callback for the uid, there's nothing + // to do on this visibility change. + if (mRecordedWC == null || !mLastInvokedStateByUid.containsKey(uid)) { + return; + } + + // If the callbacks are already in the correct state, avoid making duplicate callbacks for + // the same state. This can happen when: + // * a process becomes visible but its UID already has a recorded activity from another + // process. + // * a process becomes invisible but its UID already doesn't have any recorded activities. + if (processVisible == mLastInvokedStateByUid.get(uid)) { + return; + } + + // If the process visibility change doesn't change the visibility of the UID, avoid making + // duplicate callbacks for the same state. This can happen when: + // * a process becomes visible but the newly visible activity isn't in the recorded window + // container. + // * a process becomes invisible but there are still activities being recorded for the UID. + boolean uidInRecording = uidHasRecordedActivity(uid); + if ((processVisible && !uidInRecording) || (!processVisible && uidInRecording)) { + return; + } + + dispatchCallbacks(Set.of(uid), processVisible); + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private boolean uidHasRecordedActivity(int uid) { + if (mRecordedWC == null) { + return false; + } + boolean[] hasRecordedActivity = {false}; + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) { + hasRecordedActivity[0] = true; + return true; + } + return false; + }, true /*traverseTopToBottom*/); + return hasRecordedActivity[0]; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private Set<Integer> getRecordedUids() { + Set<Integer> result = new ArraySet<>(); + if (mRecordedWC == null) { + return result; + } + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey( + activityRecord.getUid())) { + result.add(activityRecord.getUid()); + } + }, true /*traverseTopToBottom*/); + return result; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) { + if (uids.isEmpty()) { + return; + } + + for (Integer uid : uids) { + mLastInvokedStateByUid.put(uid, visibleInScreenRecording); + } + + for (Callback callback : mCallbacks.values()) { + if (!uids.contains(callback.mUid)) { + continue; + } + try { + callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording); + } catch (RemoteException e) { + // Client has died. Cleanup is handled via DeathRecipient. + } + } + } + + void dump(PrintWriter pw) { + pw.format("ScreenRecordingCallbackController:\n"); + pw.format(" Registered callbacks:\n"); + for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) { + pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid); + } + pw.format(" Last invoked states:\n"); + for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) { + pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(), + entry.getValue()); + } + } +} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a7a6bf2ed2a1..314d720692f7 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -208,6 +208,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; @@ -1702,6 +1703,8 @@ class Task extends TaskFragment { final ActivityRecord r = findActivityInHistory(newR.mActivityComponent, newR.mUserId); if (r == null) return null; + moveTaskFragmentsToBottomIfNeeded(r, finishCount); + final PooledPredicate f = PooledLambda.obtainPredicate( (ActivityRecord ar, ActivityRecord boundaryActivity) -> finishActivityAbove(ar, boundaryActivity, finishCount), @@ -1722,6 +1725,50 @@ class Task extends TaskFragment { return r; } + /** + * Moves {@link TaskFragment}s to the bottom if the flag + * {@link TaskFragment#isMoveToBottomIfClearWhenLaunch} is {@code true}. + */ + @VisibleForTesting + void moveTaskFragmentsToBottomIfNeeded(@NonNull ActivityRecord r, @NonNull int[] finishCount) { + final int activityIndex = mChildren.indexOf(r); + if (activityIndex < 0) { + return; + } + + List<TaskFragment> taskFragmentsToMove = null; + + // Find the TaskFragments that need to be moved + for (int i = mChildren.size() - 1; i > activityIndex; i--) { + final TaskFragment taskFragment = mChildren.get(i).asTaskFragment(); + if (taskFragment != null && taskFragment.isMoveToBottomIfClearWhenLaunch()) { + if (taskFragmentsToMove == null) { + taskFragmentsToMove = new ArrayList<>(); + } + taskFragmentsToMove.add(taskFragment); + } + } + if (taskFragmentsToMove == null) { + return; + } + + // Move the TaskFragments to the bottom of the Task. Their relative orders are preserved. + final int size = taskFragmentsToMove.size(); + for (int i = 0; i < size; i++) { + final TaskFragment taskFragment = taskFragmentsToMove.get(i); + + // The visibility of the TaskFragment may change. Collect it in the transition so that + // transition animation can be properly played. + mTransitionController.collect(taskFragment); + + positionChildAt(POSITION_BOTTOM, taskFragment, false /* includeParents */); + } + + // Treat it as if the TaskFragments are finished so that a transition animation can be + // played to send the TaskFragments back and bring the activity to front. + finishCount[0] += size; + } + private static boolean finishActivityAbove(ActivityRecord r, ActivityRecord boundaryActivity, @NonNull int[] finishCount) { // Stop operation once we reach the boundary activity. diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index f56759f9481c..7d418eae6548 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -363,6 +363,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mIsolatedNav; + /** + * Whether the TaskFragment should move to bottom of task when any activity below it is + * launched in clear top mode. + */ + private boolean mMoveToBottomIfClearWhenLaunch; + /** When set, will force the task to report as invisible. */ static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1; static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1; @@ -3045,6 +3051,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { mEmbeddedDimArea = embeddedDimArea; } + void setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) { + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; + } + + boolean isMoveToBottomIfClearWhenLaunch() { + return mMoveToBottomIfClearWhenLaunch; + } + @VisibleForTesting boolean isDimmingOnParentTask() { return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b38dc008f4e5..9179acf2caed 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,6 +303,7 @@ import android.view.displayhash.VerifiedDisplayHash; import android.view.inputmethod.ImeTracker; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ITrustedPresentationListener; @@ -1104,6 +1105,8 @@ public class WindowManagerService extends IWindowManager.Stub void onAppFreezeTimeout(); } + private final ScreenRecordingCallbackController mScreenRecordingCallbackController; + public static WindowManagerService main(final Context context, final InputManagerService im, final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm) { @@ -1345,6 +1348,7 @@ public class WindowManagerService extends IWindowManager.Stub mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); mAccessibilityController = new AccessibilityController(this); + mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this); mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> { synchronized (mGlobalLock) { DisplayContent dc = mRoot.getDisplayContent(displayId); @@ -7188,6 +7192,7 @@ public class WindowManagerService extends IWindowManager.Stub mSystemPerformanceHinter.dump(pw, ""); mTrustedPresentationListenerController.dump(pw); mSensitiveContentPackages.dump(pw); + mScreenRecordingCallbackController.dump(pw); } } @@ -9889,4 +9894,18 @@ public class WindowManagerService extends IWindowManager.Stub int id) { mTrustedPresentationListenerController.unregisterListener(listener, id); } + + @Override + public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) { + return mScreenRecordingCallbackController.register(callback); + } + + @Override + public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) { + mScreenRecordingCallbackController.unregister(callback); + } + + void onProcessActivityVisibilityChanged(int uid, boolean visible) { + mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible); + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0da0bb45eb9b..205ed977f316 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -36,6 +36,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH; import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; @@ -1514,6 +1515,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub : EMBEDDED_DIM_AREA_TASK_FRAGMENT); break; } + case OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: { + taskFragment.setMoveToBottomIfClearWhenLaunch( + operation.isMoveToBottomIfClearWhenLaunch()); + break; + } } return effects; } @@ -1566,6 +1572,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return false; } + if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH) + && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { + final Throwable exception = new SecurityException( + "Only a system organizer can perform " + + "OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH." + ); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); return secondaryFragmentToken == null || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType, diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index b8fa5e5b2786..6d2e8cc29506 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -1271,8 +1271,10 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; if (!wasAnyVisible && anyVisible) { mAtm.mVisibleActivityProcessTracker.onAnyActivityVisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, true /*visible*/); } else if (wasAnyVisible && !anyVisible) { mAtm.mVisibleActivityProcessTracker.onAllActivitiesInvisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, false /*visible*/); } else if (wasAnyVisible && !wasResumed && hasResumedActivity()) { mAtm.mVisibleActivityProcessTracker.onActivityResumedWhileVisible(this); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index b36080023ef2..961fdfb14bf3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1822,6 +1822,35 @@ public class TaskTests extends WindowTestsBase { verify(fragment2).assignLayer(t, 2); } + @Test + public void testMoveTaskFragmentsToBottomIfNeeded() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord unembeddedActivity = task.getTopMostActivity(); + + final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment fragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment fragment3 = createTaskFragmentWithEmbeddedActivity(task, organizer); + doReturn(true).when(fragment1).isMoveToBottomIfClearWhenLaunch(); + doReturn(false).when(fragment2).isMoveToBottomIfClearWhenLaunch(); + doReturn(true).when(fragment3).isMoveToBottomIfClearWhenLaunch(); + + assertEquals(unembeddedActivity, task.mChildren.get(0)); + assertEquals(fragment1, task.mChildren.get(1)); + assertEquals(fragment2, task.mChildren.get(2)); + assertEquals(fragment3, task.mChildren.get(3)); + + final int[] finishCount = {0}; + task.moveTaskFragmentsToBottomIfNeeded(unembeddedActivity, finishCount); + + // fragment1 and fragment3 should be moved to the bottom of the task + assertEquals(fragment1, task.mChildren.get(0)); + assertEquals(fragment3, task.mChildren.get(1)); + assertEquals(unembeddedActivity, task.mChildren.get(2)); + assertEquals(fragment2, task.mChildren.get(3)); + assertEquals(2, finishCount[0]); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java index 4c37f7d3184c..b84ff2977b34 100644 --- a/telephony/java/android/telephony/ims/ImsService.java +++ b/telephony/java/android/telephony/ims/ImsService.java @@ -16,6 +16,7 @@ package android.telephony.ims; +import android.annotation.FlaggedApi; import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +47,7 @@ import android.util.SparseBooleanArray; import com.android.ims.internal.IImsFeatureStatusCallback; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.flags.Flags; import com.android.internal.telephony.util.TelephonyUtils; import java.lang.annotation.Retention; @@ -152,12 +154,36 @@ public class ImsService extends Service { public static final long CAPABILITY_TERMINAL_BASED_CALL_WAITING = 1 << 2; /** + * This ImsService supports the capability to manage calls on multiple subscriptions at the same + * time. + * <p> + * When set, this ImsService supports managing calls on multiple subscriptions at the same time + * for all WLAN network configurations. Telephony will allow new outgoing/incoming IMS calls to + * be set up on other subscriptions while there is an ongoing call. The ImsService must also + * support managing calls on WWAN + WWAN configurations whenever the modem also reports + * simultaneous calling availability, which can be listened to using the + * {@link android.telephony.TelephonyCallback.SimultaneousCellularCallingSupportListener} API. + * Telephony will only allow additional ongoing/incoming IMS calls on another subscription to be + * set up on WWAN + WWAN configurations when the modem reports that simultaneous cellular + * calling is allowed at the current time on both subscriptions where there are ongoing calls. + * <p> + * When unset (default), this ImsService can not support calls on multiple subscriptions at the + * same time for any WLAN or WWAN configurations, so pending outgoing call placed on another + * cellular subscription while there is an ongoing call will be cancelled by Telephony. + * Similarly, any incoming call notification on another cellular subscription while there is an + * ongoing call will be rejected. + * @hide TODO: move this to system API when we have a backing implementation + CTS testing + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public static final long CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING = 1 << 3; + + /** * Used for internal correctness checks of capabilities set by the ImsService implementation and * tracks the index of the largest defined flag in the capabilities long. * @hide */ public static final long CAPABILITY_MAX_INDEX = - Long.numberOfTrailingZeros(CAPABILITY_TERMINAL_BASED_CALL_WAITING); + Long.numberOfTrailingZeros(CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING); /** * @hide |