summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/ApplicationStartInfo.java25
-rw-r--r--core/java/android/os/FileUtils.java4
-rw-r--r--core/java/android/window/ImeOnBackInvokedDispatcher.java11
-rw-r--r--core/java/android/window/WindowOnBackInvokedDispatcher.java1
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig10
-rw-r--r--core/java/com/android/internal/widget/ActionBarOverlayLayout.java59
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java6
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt10
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt6
-rw-r--r--packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java50
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java19
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java12
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java24
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java2
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java8
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java20
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesChecker.java108
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java76
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java58
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java116
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java48
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java50
-rw-r--r--services/core/java/com/android/server/media/MediaSessionRecord.java50
-rw-r--r--services/core/java/com/android/server/media/MediaSessionService.java15
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java40
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java12
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java2
-rw-r--r--services/core/java/com/android/server/wm/DeferredDisplayUpdater.java10
-rw-r--r--services/core/java/com/android/server/wm/EventLogTags.logtags4
-rw-r--r--services/core/java/com/android/server/wm/LetterboxUiController.java241
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java3
-rw-r--r--services/core/java/com/android/server/wm/Transition.java13
-rw-r--r--services/core/java/com/android/server/wm/TransparentPolicy.java353
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java1
-rw-r--r--services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java39
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteManager.java67
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteSessionStats.aidl19
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteSessionStats.java224
-rw-r--r--telephony/java/com/android/internal/telephony/ITelephony.aidl10
-rw-r--r--tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt2
48 files changed, 1385 insertions, 511 deletions
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 3715c6e633dc..f77c50a271be 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -416,11 +416,34 @@ public final class ApplicationStartInfo implements Parcelable {
/**
* @see #getStartIntent
+ *
+ * <p class="note"> Note: This method will clone the provided intent and ensure that the cloned
+ * intent doesn't contain any large objects like bitmaps in its extras by stripping it in the
+ * least aggressive acceptable way for the individual intent.</p>
+ *
* @hide
*/
public void setIntent(Intent startIntent) {
if (startIntent != null) {
- mStartIntent = startIntent.maybeStripForHistory();
+ if (startIntent.canStripForHistory()) {
+ // If maybeStripForHistory will return a lightened version, do that.
+ mStartIntent = startIntent.maybeStripForHistory();
+ } else if (startIntent.getExtras() != null) {
+ // If maybeStripForHistory would not return a lightened version and extras is
+ // non-null then extras contains un-parcelled data. Use cloneFilter to strip data
+ // more aggressively.
+ mStartIntent = startIntent.cloneFilter();
+ } else {
+ // Finally, if maybeStripForHistory would not return a lightened version and extras
+ // is null then do a regular clone so we don't leak the intent.
+ mStartIntent = new Intent(startIntent);
+ }
+
+ // If the newly cloned intent has an original intent, clear that as we don't need it and
+ // can't guarantee it doesn't need to be stripped as well.
+ if (mStartIntent.getOriginalIntent() != null) {
+ mStartIntent.setOriginalIntent(null);
+ }
}
}
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index 61b52c67dfe2..e6b1c07846f9 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -580,6 +580,8 @@ public final class FileUtils {
", copied:" + progress +
", read:" + (count - countToRead) +
", in pipe: " + countInPipe);
+ Os.close(pipes[0]);
+ Os.close(pipes[1]);
throw new ErrnoException("splice, pipe --> fdOut", EIO);
} else {
progress += t;
@@ -607,6 +609,8 @@ public final class FileUtils {
listener.onProgress(progressSnapshot);
});
}
+ Os.close(pipes[0]);
+ Os.close(pipes[1]);
return progress;
}
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 2a12507679f5..ce1f9869b690 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -55,6 +55,9 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
static final int RESULT_CODE_UNREGISTER = 1;
@NonNull
private final ResultReceiver mResultReceiver;
+ // The handler to run callbacks on. This should be on the same thread
+ // the ViewRootImpl holding IME's WindowOnBackInvokedDispatcher is created on.
+ private Handler mHandler;
public ImeOnBackInvokedDispatcher(Handler handler) {
mResultReceiver = new ResultReceiver(handler) {
@@ -68,6 +71,10 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
};
}
+ void setHandler(@NonNull Handler handler) {
+ mHandler = handler;
+ }
+
/**
* Override this method to return the {@link WindowOnBackInvokedDispatcher} of the window
* that should receive the forwarded callback.
@@ -326,7 +333,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
@Override
public void onBackInvoked() {
- mCallback.onBackInvoked();
+ mHandler.post(mCallback::onBackInvoked);
}
@Override
@@ -336,7 +343,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
private void maybeRunOnAnimationCallback(Consumer<OnBackAnimationCallback> block) {
if (mCallback instanceof OnBackAnimationCallback) {
- block.accept((OnBackAnimationCallback) mCallback);
+ mHandler.post(() -> block.accept((OnBackAnimationCallback) mCallback));
}
}
}
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 9f6933c5429d..4c993c2544ce 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -550,6 +550,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
public void setImeOnBackInvokedDispatcher(
@NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
mImeDispatcher = imeDispatcher;
+ mImeDispatcher.setHandler(mHandler);
}
/** Returns true if a non-null {@link ImeOnBackInvokedDispatcher} has been set. **/
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index d6f65f8c9d8b..b71468247e37 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -19,6 +19,16 @@ flag {
}
flag {
+ name: "blast_sync_notification_shade_on_display_switch"
+ namespace: "windowing_frontend"
+ description: "Make the buffer content of notification shade synchronize with display switch"
+ bug: "337154331"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "edge_to_edge_by_default"
namespace: "windowing_frontend"
description: "Make app go edge-to-edge by default when targeting SDK 35 or greater"
diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
index 707f1094a8bc..68328252abaf 100644
--- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
+++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -52,6 +52,7 @@ import com.android.internal.view.menu.MenuPresenter;
*/
public class ActionBarOverlayLayout extends ViewGroup implements DecorContentParent {
private static final String TAG = "ActionBarOverlayLayout";
+ private static final Rect EMPTY_RECT = new Rect();
private int mActionBarHeight;
//private WindowDecorActionBar mActionBar;
@@ -294,55 +295,53 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
}
private boolean applyInsets(View view, Rect insets, boolean toPadding,
- boolean left, boolean top, boolean bottom, boolean right) {
+ boolean left, boolean top, boolean right, boolean bottom) {
boolean changed;
if (toPadding) {
- changed = setMargin(view, left, top, bottom, right, 0, 0, 0, 0);
- changed |= setPadding(view, left, top, bottom, right,
- insets.left, insets.top, insets.right, insets.bottom);
+ changed = setMargin(view, EMPTY_RECT, left, top, right, bottom);
+ changed |= setPadding(view, insets, left, top, right, bottom);
} else {
- changed = setPadding(view, left, top, bottom, right, 0, 0, 0, 0);
- changed |= setMargin(view, left, top, bottom, right,
- insets.left, insets.top, insets.right, insets.bottom);
+ changed = setPadding(view, EMPTY_RECT, left, top, right, bottom);
+ changed |= setMargin(view, insets, left, top, right, bottom);
}
return changed;
}
- private boolean setPadding(View view, boolean left, boolean top, boolean bottom, boolean right,
- int l, int t, int r, int b) {
- if ((left && view.getPaddingLeft() != l)
- || (top && view.getPaddingTop() != t)
- || (right && view.getPaddingRight() != r)
- || (bottom && view.getPaddingBottom() != b)) {
+ private boolean setPadding(View view, Rect insets,
+ boolean left, boolean top, boolean right, boolean bottom) {
+ if ((left && view.getPaddingLeft() != insets.left)
+ || (top && view.getPaddingTop() != insets.top)
+ || (right && view.getPaddingRight() != insets.right)
+ || (bottom && view.getPaddingBottom() != insets.bottom)) {
view.setPadding(
- left ? l : view.getPaddingLeft(),
- top ? t : view.getPaddingTop(),
- right ? r : view.getPaddingRight(),
- bottom ? b : view.getPaddingBottom());
+ left ? insets.left : view.getPaddingLeft(),
+ top ? insets.top : view.getPaddingTop(),
+ right ? insets.right : view.getPaddingRight(),
+ bottom ? insets.bottom : view.getPaddingBottom());
return true;
}
return false;
}
- private boolean setMargin(View view, boolean left, boolean top, boolean bottom, boolean right,
- int l, int t, int r, int b) {
- LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ private boolean setMargin(View view, Rect insets,
+ boolean left, boolean top, boolean right, boolean bottom) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
boolean changed = false;
- if (left && lp.leftMargin != l) {
+ if (left && lp.leftMargin != insets.left) {
changed = true;
- lp.leftMargin = l;
+ lp.leftMargin = insets.left;
}
- if (top && lp.topMargin != t) {
+ if (top && lp.topMargin != insets.top) {
changed = true;
- lp.topMargin = t;
+ lp.topMargin = insets.top;
}
- if (right && lp.rightMargin != r) {
+ if (right && lp.rightMargin != insets.right) {
changed = true;
- lp.rightMargin = r;
+ lp.rightMargin = insets.right;
}
- if (bottom && lp.bottomMargin != b) {
+ if (bottom && lp.bottomMargin != insets.bottom) {
changed = true;
- lp.bottomMargin = b;
+ lp.bottomMargin = insets.bottom;
}
return changed;
}
@@ -370,7 +369,7 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
// The top and bottom action bars are always within the content area.
boolean changed = applyInsets(mActionBarTop, mSystemInsets,
- mActionBarExtendsIntoSystemInsets, true, true, false, true);
+ mActionBarExtendsIntoSystemInsets, true, true, true, false);
if (mActionBarBottom != null) {
changed |= applyInsets(mActionBarBottom, mSystemInsets,
mActionBarExtendsIntoSystemInsets, true, false, true, true);
@@ -522,7 +521,7 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
);
}
}
- applyInsets(mContent, mContentInsets, false /* toPadding */, true, true, true, true);
+ setMargin(mContent, mContentInsets, true, true, true, true);
if (!mLastInnerInsets.equals(mInnerInsets)) {
// If the inner insets have changed, we need to dispatch this down to
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index e8f01c273550..d92d24d9e22d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -16,6 +16,9 @@
package androidx.window.extensions.embedding;
+import static android.content.pm.ActivityInfo.CONFIG_DENSITY;
+import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -40,7 +43,6 @@ import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
import android.content.Context;
-import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
@@ -959,7 +961,7 @@ class DividerPresenter implements View.OnTouchListener {
@VisibleForTesting
static class Properties {
private static final int CONFIGURATION_MASK_FOR_DIVIDER =
- ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+ CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION;
@NonNull
private final Configuration mConfiguration;
@NonNull
@@ -1228,6 +1230,12 @@ class DividerPresenter implements View.OnTouchListener {
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
PixelFormat.TRANSLUCENT);
lp.setTitle(WINDOW_NAME);
+
+ // Ensure that the divider layout is always LTR regardless of the locale, because we
+ // already considered the locale when determining the split layout direction and the
+ // computed divider line position always starts from the left. This only affects the
+ // horizontal layout and does not have any effect on the top-to-bottom layout.
+ mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
mViewHost.setView(mDividerLayout, lp);
mViewHost.relayout(lp);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index b52b0d8dee74..5ee6f6bb0e1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -200,9 +200,6 @@ public class PipTransition extends PipTransitionController {
animator.cancel();
}
mExitTransition = mTransitions.startTransition(type, out, this);
- if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) {
- mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */);
- }
}
@Override
@@ -659,6 +656,9 @@ public class PipTransition extends PipTransitionController {
startTransaction.remove(mPipOrganizer.mPipOverlay);
mPipOrganizer.clearContentOverlay();
}
+ if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) {
+ mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */);
+ }
if (pipChange == null) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: No window of exiting PIP is found. Can't play expand animation", TAG);
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index c477f30a1d2f..08846f0fed96 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -45,8 +45,8 @@ import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreateCustomCredentialRequest
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CredentialOption
import androidx.credentials.PasswordCredential
-import androidx.credentials.PriorityHints
import androidx.credentials.PublicKeyCredential
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.RemoteEntry
@@ -177,9 +177,9 @@ class GetFlowUtils {
"androidx.credentials.BUNDLE_KEY_TYPE_PRIORITY_VALUE",
when (option.type) {
PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
- PriorityHints.PRIORITY_PASSWORD_OR_SIMILAR
+ CredentialOption.PRIORITY_PASSWORD_OR_SIMILAR
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> 100
- else -> PriorityHints.PRIORITY_DEFAULT
+ else -> CredentialOption.PRIORITY_DEFAULT
}
)
typePriorityMap[option.type] = priority
@@ -349,8 +349,8 @@ class CreateFlowUtils {
}
is CreateCustomCredentialRequest -> {
// TODO: directly use the display info once made public
- val displayInfo = CreateCredentialRequest.DisplayInfo
- .parseFromCredentialDataBundle(createCredentialRequest.credentialData)
+ val displayInfo = CreateCredentialRequest.DisplayInfo.createFrom(
+ createCredentialRequest.credentialData)
?: return null
RequestDisplayInfo(
title = displayInfo.userId.toString(),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 19f5a99f46fa..314cc0547b89 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -19,7 +19,7 @@ package com.android.credentialmanager.getflow
import android.credentials.flags.Flags.selectorUiImprovementsEnabled
import android.credentials.flags.Flags.credmanBiometricApiEnabled
import android.graphics.drawable.Drawable
-import androidx.credentials.PriorityHints
+import androidx.credentials.CredentialOption
import com.android.credentialmanager.R
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.get.ProviderInfo
@@ -322,10 +322,10 @@ internal class CredentialEntryInfoComparatorByTypeThenTimestamp(
// First rank by priorities of each credential type.
if (p0.rawCredentialType != p1.rawCredentialType) {
val p0Priority = typePriorityMap.getOrDefault(
- p0.rawCredentialType, PriorityHints.PRIORITY_DEFAULT
+ p0.rawCredentialType, CredentialOption.PRIORITY_DEFAULT
)
val p1Priority = typePriorityMap.getOrDefault(
- p1.rawCredentialType, PriorityHints.PRIORITY_DEFAULT
+ p1.rawCredentialType, CredentialOption.PRIORITY_DEFAULT
)
if (p0Priority < p1Priority) {
return -1
diff --git a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java
index ea3dbd925792..0e71a1b127e7 100644
--- a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java
+++ b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java
@@ -38,8 +38,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
- * Progres bar preference with a usage summary and a total summary.
- * This preference shows number in usage summary with enlarged font size.
+ * Progress bar preference with a usage summary and a total summary.
+ *
+ * <p>This preference shows number in usage summary with enlarged font size.
*/
public class UsageProgressBarPreference extends Preference {
@@ -48,18 +49,18 @@ public class UsageProgressBarPreference extends Preference {
private CharSequence mUsageSummary;
private CharSequence mTotalSummary;
private CharSequence mBottomSummary;
+ private CharSequence mBottomSummaryContentDescription;
private ImageView mCustomImageView;
private int mPercent = -1;
/**
* Perform inflation from XML and apply a class-specific base style.
*
- * @param context The {@link Context} this is associated with, through which it can
- * access the current theme, resources, {@link SharedPreferences}, etc.
- * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param context The {@link Context} this is associated with, through which it can access the
+ * current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
* @param defStyle An attribute in the current theme that contains a reference to a style
- * resource that supplies default values for the view. Can be 0 to not
- * look for defaults.
+ * resource that supplies default values for the view. Can be 0 to not look for defaults.
*/
public UsageProgressBarPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
@@ -69,9 +70,9 @@ public class UsageProgressBarPreference extends Preference {
/**
* Perform inflation from XML and apply a class-specific base style.
*
- * @param context The {@link Context} this is associated with, through which it can
- * access the current theme, resources, {@link SharedPreferences}, etc.
- * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param context The {@link Context} this is associated with, through which it can access the
+ * current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
*/
public UsageProgressBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -114,9 +115,17 @@ public class UsageProgressBarPreference extends Preference {
notifyChanged();
}
+ /** Set content description for the bottom summary. */
+ public void setBottomSummaryContentDescription(CharSequence contentDescription) {
+ if (!TextUtils.equals(mBottomSummaryContentDescription, contentDescription)) {
+ mBottomSummaryContentDescription = contentDescription;
+ notifyChanged();
+ }
+ }
+
/** Set percentage of the progress bar. */
public void setPercent(long usage, long total) {
- if (usage > total) {
+ if (usage > total) {
return;
}
if (total == 0L) {
@@ -146,14 +155,13 @@ public class UsageProgressBarPreference extends Preference {
/**
* Binds the created View to the data for this preference.
*
- * <p>This is a good place to grab references to custom Views in the layout and set
- * properties on them.
+ * <p>This is a good place to grab references to custom Views in the layout and set properties
+ * on them.
*
* <p>Make sure to call through to the superclass's implementation.
*
* @param holder The ViewHolder that provides references to the views to fill in. These views
- * will be recycled, so you should not hold a reference to them after this method
- * returns.
+ * will be recycled, so you should not hold a reference to them after this method returns.
*/
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
@@ -177,6 +185,9 @@ public class UsageProgressBarPreference extends Preference {
bottomSummary.setVisibility(View.VISIBLE);
bottomSummary.setMovementMethod(LinkMovementMethod.getInstance());
bottomSummary.setText(mBottomSummary);
+ if (!TextUtils.isEmpty(mBottomSummaryContentDescription)) {
+ bottomSummary.setContentDescription(mBottomSummaryContentDescription);
+ }
}
final ProgressBar progressBar = (ProgressBar) holder.findViewById(android.R.id.progress);
@@ -205,9 +216,12 @@ public class UsageProgressBarPreference extends Preference {
final Matcher matcher = mNumberPattern.matcher(summary);
if (matcher.find()) {
- final SpannableString spannableSummary = new SpannableString(summary);
- spannableSummary.setSpan(new AbsoluteSizeSpan(64, true /* dip */), matcher.start(),
- matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ final SpannableString spannableSummary = new SpannableString(summary);
+ spannableSummary.setSpan(
+ new AbsoluteSizeSpan(64, true /* dip */),
+ matcher.start(),
+ matcher.end(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableSummary;
}
return summary;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index cdb87404b016..063807abeb0a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -17,6 +17,8 @@ package com.android.settingslib.media;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
@@ -37,21 +39,14 @@ public class BluetoothMediaDevice extends MediaDevice {
private static final String TAG = "BluetoothMediaDevice";
- private CachedBluetoothDevice mCachedDevice;
+ private final CachedBluetoothDevice mCachedDevice;
private final AudioManager mAudioManager;
BluetoothMediaDevice(
- Context context,
- CachedBluetoothDevice device,
- MediaRoute2Info info) {
- this(context, device, info, null);
- }
-
- BluetoothMediaDevice(
- Context context,
- CachedBluetoothDevice device,
- MediaRoute2Info info,
- RouteListingPreference.Item item) {
+ @NonNull Context context,
+ @NonNull CachedBluetoothDevice device,
+ @Nullable MediaRoute2Info info,
+ @Nullable RouteListingPreference.Item item) {
super(context, info, item);
mCachedDevice = device;
mAudioManager = context.getSystemService(AudioManager.class);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java
index 338fb872650c..a87daf90a84f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ComplexMediaDevice.java
@@ -16,6 +16,8 @@
package com.android.settingslib.media;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.MediaRoute2Info;
@@ -32,9 +34,9 @@ public class ComplexMediaDevice extends MediaDevice {
private final String mSummary = "";
ComplexMediaDevice(
- Context context,
- MediaRoute2Info info,
- RouteListingPreference.Item item) {
+ @NonNull Context context,
+ @NonNull MediaRoute2Info info,
+ @Nullable RouteListingPreference.Item item) {
super(context, info, item);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
index 1347dd131f69..21873ef3aeab 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
@@ -26,6 +26,8 @@ import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET;
import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED;
import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.MediaRoute2Info;
@@ -43,17 +45,13 @@ public class InfoMediaDevice extends MediaDevice {
private static final String TAG = "InfoMediaDevice";
InfoMediaDevice(
- Context context,
- MediaRoute2Info info,
- RouteListingPreference.Item item) {
+ @NonNull Context context,
+ @NonNull MediaRoute2Info info,
+ @Nullable RouteListingPreference.Item item) {
super(context, info, item);
initDeviceRecord();
}
- InfoMediaDevice(Context context, MediaRoute2Info info) {
- this(context, info, null);
- }
-
@Override
public String getName() {
return mRouteInfo.getName().toString();
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index cfa825bbb1c4..72a60fbc9fea 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -570,7 +570,7 @@ public class LocalMediaManager implements BluetoothCallback {
final CachedBluetoothDevice cachedDevice =
cachedDeviceManager.findDevice(device);
if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) {
- return new BluetoothMediaDevice(mContext, cachedDevice, null);
+ return new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null);
}
}
return null;
@@ -617,7 +617,7 @@ public class LocalMediaManager implements BluetoothCallback {
mDisconnectedMediaDevices.clear();
for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) {
final MediaDevice mediaDevice =
- new BluetoothMediaDevice(mContext, cachedDevice, null);
+ new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null);
if (!mMediaDevices.contains(mediaDevice)) {
cachedDevice.registerCallback(mDeviceAttributeChangeCallback);
mDisconnectedMediaDevices.add(mediaDevice);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 0c4cf769ca90..ce1f29766bed 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -49,6 +49,8 @@ import static android.media.RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -123,9 +125,9 @@ public abstract class MediaDevice implements Comparable<MediaDevice> {
protected final RouteListingPreference.Item mItem;
MediaDevice(
- Context context,
- MediaRoute2Info info,
- RouteListingPreference.Item item) {
+ @NonNull Context context,
+ @Nullable MediaRoute2Info info,
+ @Nullable RouteListingPreference.Item item) {
mContext = context;
mRouteInfo = info;
mItem = item;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index ba9180db0887..9eaf8d3838d8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -29,6 +29,8 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
@@ -40,7 +42,6 @@ import android.media.RouteListingPreference;
import android.os.SystemProperties;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
@@ -100,17 +101,6 @@ public class PhoneMediaDevice extends MediaDevice {
R.string.media_transfer_external_device_name);
break;
case TYPE_HDMI_ARC:
- if (isTv) {
- String deviceName = getHdmiOutDeviceName(context);
- if (deviceName != null) {
- name = deviceName;
- } else {
- name = context.getString(R.string.tv_media_transfer_arc_fallback_title);
- }
- } else {
- name = context.getString(R.string.media_transfer_external_device_name);
- }
- break;
case TYPE_HDMI_EARC:
if (isTv) {
String deviceName = getHdmiOutDeviceName(context);
@@ -130,14 +120,10 @@ public class PhoneMediaDevice extends MediaDevice {
return name.toString();
}
- PhoneMediaDevice(Context context, MediaRoute2Info info) {
- this(context, info, null);
- }
-
PhoneMediaDevice(
- Context context,
- MediaRoute2Info info,
- RouteListingPreference.Item item) {
+ @NonNull Context context,
+ @NonNull MediaRoute2Info info,
+ @Nullable RouteListingPreference.Item item) {
super(context, info, item);
mDeviceIconUtil = new DeviceIconUtil(mContext);
initDeviceRecord();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java
index 0665308fdbfb..6647a278a6bd 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java
@@ -65,7 +65,7 @@ public class InfoMediaDeviceTest {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
- mInfoMediaDevice = new InfoMediaDevice(mContext, mRouteInfo);
+ mInfoMediaDevice = new InfoMediaDevice(mContext, mRouteInfo, /* item */ null);
}
@Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index ce07fe9fdf0a..c9b35a0ae833 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -559,7 +559,7 @@ public class InfoMediaManagerTest {
routingSessionInfos.add(info);
final MediaRoute2Info route2Info = mock(MediaRoute2Info.class);
- final MediaDevice device = new InfoMediaDevice(mContext, route2Info);
+ final MediaDevice device = new InfoMediaDevice(mContext, route2Info, /* item */ null);
final List<String> list = new ArrayList<>();
list.add(TEST_ID);
@@ -580,7 +580,7 @@ public class InfoMediaManagerTest {
routingSessionInfos.add(info);
final MediaRoute2Info route2Info = mock(MediaRoute2Info.class);
- final MediaDevice device = new InfoMediaDevice(mContext, route2Info);
+ final MediaDevice device = new InfoMediaDevice(mContext, route2Info, /* item */ null);
final List<String> list = new ArrayList<>();
list.add("fake_id");
@@ -602,7 +602,7 @@ public class InfoMediaManagerTest {
routingSessionInfos.add(info);
final MediaRoute2Info route2Info = mock(MediaRoute2Info.class);
- final MediaDevice device = new InfoMediaDevice(mContext, route2Info);
+ final MediaDevice device = new InfoMediaDevice(mContext, route2Info, /* item */ null);
final List<String> list = new ArrayList<>();
list.add(TEST_ID);
@@ -623,7 +623,7 @@ public class InfoMediaManagerTest {
routingSessionInfos.add(info);
final MediaRoute2Info route2Info = mock(MediaRoute2Info.class);
- final MediaDevice device = new InfoMediaDevice(mContext, route2Info);
+ final MediaDevice device = new InfoMediaDevice(mContext, route2Info, /* item */ null);
final List<String> list = new ArrayList<>();
list.add("fake_id");
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 12541bb51cc8..a30d6a787971 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -135,8 +135,8 @@ public class LocalMediaManagerTest {
.when(mInfoMediaManager)
.getRoutingSessionsForPackage();
- mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1));
- mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2);
+ mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1, /* item */ null));
+ mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2, /* item */ null);
mLocalMediaManager =
new LocalMediaManager(
mContext, mLocalBluetoothManager, mInfoMediaManager, TEST_PACKAGE_NAME);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 098ab162c225..3d16d6f1cd56 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -171,17 +171,17 @@ public class MediaDeviceTest {
mBluetoothMediaDevice1 =
new BluetoothMediaDevice(
- mContext, mCachedDevice1, mBluetoothRouteInfo1);
+ mContext, mCachedDevice1, mBluetoothRouteInfo1, /* item */ null);
mBluetoothMediaDevice2 =
new BluetoothMediaDevice(
- mContext, mCachedDevice2, mBluetoothRouteInfo2);
+ mContext, mCachedDevice2, mBluetoothRouteInfo2, /* item */ null);
mBluetoothMediaDevice3 =
new BluetoothMediaDevice(
- mContext, mCachedDevice3, mBluetoothRouteInfo3);
- mInfoMediaDevice1 = new InfoMediaDevice(mContext, mRouteInfo1);
- mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2);
- mInfoMediaDevice3 = new InfoMediaDevice(mContext, mRouteInfo3);
- mPhoneMediaDevice = new PhoneMediaDevice(mContext, mPhoneRouteInfo);
+ mContext, mCachedDevice3, mBluetoothRouteInfo3, /* item */ null);
+ mInfoMediaDevice1 = new InfoMediaDevice(mContext, mRouteInfo1, /* item */ null);
+ mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2, /* item */ null);
+ mInfoMediaDevice3 = new InfoMediaDevice(mContext, mRouteInfo3, /* item */ null);
+ mPhoneMediaDevice = new PhoneMediaDevice(mContext, mPhoneRouteInfo, /* item */ null);
}
@Test
@@ -316,7 +316,7 @@ public class MediaDeviceTest {
when(phoneRouteInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
final PhoneMediaDevice phoneMediaDevice =
- new PhoneMediaDevice(mContext, phoneRouteInfo);
+ new PhoneMediaDevice(mContext, phoneRouteInfo, /* item */ null);
mMediaDevices.add(mBluetoothMediaDevice1);
mMediaDevices.add(phoneMediaDevice);
@@ -332,7 +332,7 @@ public class MediaDeviceTest {
when(phoneRouteInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
final PhoneMediaDevice phoneMediaDevice =
- new PhoneMediaDevice(mContext, phoneRouteInfo);
+ new PhoneMediaDevice(mContext, phoneRouteInfo, /* item */ null);
mMediaDevices.add(mInfoMediaDevice1);
mMediaDevices.add(phoneMediaDevice);
@@ -483,7 +483,7 @@ public class MediaDeviceTest {
public void getFeatures_noRouteInfo_returnEmptyList() {
mBluetoothMediaDevice1 =
new BluetoothMediaDevice(
- mContext, mCachedDevice1, /* MediaRoute2Info */ null);
+ mContext, mCachedDevice1, /* MediaRoute2Info */ null, /* item */ null);
assertThat(mBluetoothMediaDevice1.getFeatures().size()).isEqualTo(0);
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index b4377eae4d1f..c0c8b755108c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -122,6 +122,8 @@ public class QuickStepContract {
public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY = 1L << 31;
// Physical keyboard shortcuts helper is showing
public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32;
+ // Touchpad gestures are disabled
+ public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33;
// Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and
// SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants
@@ -170,6 +172,7 @@ public class QuickStepContract {
SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE,
SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY,
SYSUI_STATE_SHORTCUT_HELPER_SHOWING,
+ SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED,
})
public @interface SystemUiStateFlags {}
@@ -271,6 +274,9 @@ public class QuickStepContract {
if ((flags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0) {
str.add("shortcut_helper_showing");
}
+ if ((flags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) != 0) {
+ str.add("touchpad_gestures_disabled");
+ }
return str.toString();
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesChecker.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesChecker.java
new file mode 100644
index 000000000000..2d1cd03aea4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesChecker.java
@@ -0,0 +1,108 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.dagger.SysUISingleton;
+
+import javax.inject.Inject;
+
+/**
+ * HearingDevicesChecker provides utility methods to determine the presence and status of
+ * connected hearing aid devices.
+ *
+ * <p>It also filters out devices that are exclusively managed by other applications to avoid
+ * interfering with their operation.
+ */
+@SysUISingleton
+public class HearingDevicesChecker {
+
+ private final Context mContext;
+ private final LocalBluetoothManager mLocalBluetoothManager;
+
+ @Inject
+ public HearingDevicesChecker(
+ Context context,
+ @Nullable LocalBluetoothManager localBluetoothManager) {
+ mContext = context;
+ mLocalBluetoothManager = localBluetoothManager;
+ }
+
+ /**
+ * Checks if any hearing device is already paired.
+ *
+ * <p>It includes {@link BluetoothDevice.BOND_BONDING} and {@link BluetoothDevice.BOND_BONDED}).
+ *
+ * <p>A bonded device means it has been paired, but may not connected now.
+ *
+ * @return {@code true} if any bonded hearing device is found, {@code false} otherwise.
+ */
+ @WorkerThread
+ public boolean isAnyPairedHearingDevice() {
+ if (mLocalBluetoothManager == null) {
+ return false;
+ }
+ if (!mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
+ return false;
+ }
+
+ return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream()
+ .anyMatch(device -> device.isHearingAidDevice()
+ && device.getBondState() != BluetoothDevice.BOND_NONE
+ && !isExclusivelyManagedBluetoothDevice(device));
+ }
+
+ /**
+ * Checks if there are any active hearing device.
+ *
+ * <p>An active device means it is currently connected and streaming media.
+ *
+ * @return {@code true} if any active hearing device is found, {@code false} otherwise.
+ */
+ @WorkerThread
+ public boolean isAnyActiveHearingDevice() {
+ if (mLocalBluetoothManager == null) {
+ return false;
+ }
+ if (!mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
+ return false;
+ }
+
+ return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream()
+ .anyMatch(device -> BluetoothUtils.isActiveMediaDevice(device)
+ && BluetoothUtils.isAvailableHearingDevice(device)
+ && !isExclusivelyManagedBluetoothDevice(device));
+ }
+
+ private boolean isExclusivelyManagedBluetoothDevice(
+ @NonNull CachedBluetoothDevice cachedDevice) {
+ if (com.android.settingslib.flags.Flags.enableHideExclusivelyManagedBluetoothDevice()) {
+ return BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ cachedDevice.getDevice());
+ }
+ return false;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
index 14e5f3422a27..bc4cb45582ff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
@@ -16,19 +16,24 @@
package com.android.systemui.accessibility.hearingaid;
-import android.bluetooth.BluetoothDevice;
import android.util.Log;
-import androidx.annotation.Nullable;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.android.internal.jank.InteractionJankMonitor;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
import javax.inject.Inject;
/**
@@ -43,16 +48,22 @@ public class HearingDevicesDialogManager {
private SystemUIDialog mDialog;
private final DialogTransitionAnimator mDialogTransitionAnimator;
private final HearingDevicesDialogDelegate.Factory mDialogFactory;
- private final LocalBluetoothManager mLocalBluetoothManager;
+ private final HearingDevicesChecker mDevicesChecker;
+ private final Executor mBackgroundExecutor;
+ private final Executor mMainExecutor;
@Inject
public HearingDevicesDialogManager(
DialogTransitionAnimator dialogTransitionAnimator,
HearingDevicesDialogDelegate.Factory dialogFactory,
- @Nullable LocalBluetoothManager localBluetoothManager) {
+ HearingDevicesChecker devicesChecker,
+ @Background Executor backgroundExecutor,
+ @Main Executor mainExecutor) {
mDialogTransitionAnimator = dialogTransitionAnimator;
mDialogFactory = dialogFactory;
- mLocalBluetoothManager = localBluetoothManager;
+ mDevicesChecker = devicesChecker;
+ mBackgroundExecutor = backgroundExecutor;
+ mMainExecutor = mainExecutor;
}
/**
@@ -68,36 +79,41 @@ public class HearingDevicesDialogManager {
destroyDialog();
}
- mDialog = mDialogFactory.create(!isAnyBondedHearingDevice()).createDialog();
+ final ListenableFuture<Boolean> pairedHearingDeviceCheckTask =
+ CallbackToFutureAdapter.getFuture(completer -> {
+ mBackgroundExecutor.execute(
+ () -> {
+ completer.set(mDevicesChecker.isAnyPairedHearingDevice());
+ });
+ // This value is used only for debug purposes: it will be used in toString()
+ // of returned future or error cases.
+ return "isAnyPairedHearingDevice check";
+ });
+ pairedHearingDeviceCheckTask.addListener(() -> {
+ try {
+ mDialog = mDialogFactory.create(!pairedHearingDeviceCheckTask.get()).createDialog();
+
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(
+ new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(mDialog,
+ controller, /* animateBackgroundBoundsChange= */ true);
+ return;
+ }
+ }
+ mDialog.show();
- if (expandable != null) {
- DialogTransitionAnimator.Controller controller = expandable.dialogTransitionController(
- new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG));
- if (controller != null) {
- mDialogTransitionAnimator.show(mDialog,
- controller, /* animateBackgroundBoundsChange= */ true);
- return;
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(TAG, "Exception occurs while running pairedHearingDeviceCheckTask", e);
}
- }
- mDialog.show();
+ }, mMainExecutor);
}
private void destroyDialog() {
mDialog.dismiss();
mDialog = null;
}
-
- private boolean isAnyBondedHearingDevice() {
- if (mLocalBluetoothManager == null) {
- return false;
- }
- if (!mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
- return false;
- }
-
- return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream()
- .anyMatch(device -> device.isHearingAidDevice()
- && device.getBondState() != BluetoothDevice.BOND_NONE);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
index 183c1a4a7ce7..b96e83d43e32 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
@@ -19,34 +19,53 @@ package com.android.systemui.qs.tiles;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
+import android.os.UserManager;
import android.provider.Settings;
+import android.service.quicksettings.Tile;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Flags;
+import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker;
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSTile.State;
+import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.policy.BluetoothController;
import javax.inject.Inject;
/** Quick settings tile: Hearing Devices **/
-public class HearingDevicesTile extends QSTileImpl<State> {
-
+public class HearingDevicesTile extends QSTileImpl<BooleanState> {
+ //TODO(b/338520598): Transform the current implementation into new QS architecture
+ // and use Kotlin except Tile class.
public static final String TILE_SPEC = "hearing_devices";
private final HearingDevicesDialogManager mDialogManager;
+ private final HearingDevicesChecker mDevicesChecker;
+ private final BluetoothController mBluetoothController;
+
+ private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
+ @Override
+ public void onBluetoothStateChange(boolean enabled) {
+ refreshState();
+ }
+
+ @Override
+ public void onBluetoothDevicesChanged() {
+ refreshState();
+ }
+ };
@Inject
public HearingDevicesTile(
@@ -59,16 +78,20 @@ public class HearingDevicesTile extends QSTileImpl<State> {
StatusBarStateController statusBarStateController,
ActivityStarter activityStarter,
QSLogger qsLogger,
- HearingDevicesDialogManager hearingDevicesDialogManager
- ) {
+ HearingDevicesDialogManager hearingDevicesDialogManager,
+ HearingDevicesChecker hearingDevicesChecker,
+ BluetoothController bluetoothController) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mDialogManager = hearingDevicesDialogManager;
+ mDevicesChecker = hearingDevicesChecker;
+ mBluetoothController = bluetoothController;
+ mBluetoothController.observe(getLifecycle(), mCallback);
}
@Override
- public State newTileState() {
- return new State();
+ public BooleanState newTileState() {
+ return new BooleanState();
}
@Override
@@ -77,9 +100,28 @@ public class HearingDevicesTile extends QSTileImpl<State> {
}
@Override
- protected void handleUpdateState(State state, Object arg) {
+ protected void handleUpdateState(BooleanState state, Object arg) {
+ checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH);
+
state.label = mContext.getString(R.string.quick_settings_hearing_devices_label);
state.icon = ResourceIcon.get(R.drawable.qs_hearing_devices_icon);
+ state.forceExpandIcon = true;
+
+ boolean isBonded = mDevicesChecker.isAnyPairedHearingDevice();
+ boolean isActive = mDevicesChecker.isAnyActiveHearingDevice();
+
+ if (isActive) {
+ state.state = Tile.STATE_ACTIVE;
+ state.secondaryLabel = mContext.getString(
+ R.string.quick_settings_hearing_devices_connected);
+ } else if (isBonded) {
+ state.state = Tile.STATE_INACTIVE;
+ state.secondaryLabel = mContext.getString(
+ R.string.quick_settings_hearing_devices_disconnected);
+ } else {
+ state.state = Tile.STATE_INACTIVE;
+ state.secondaryLabel = "";
+ }
}
@Nullable
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java
new file mode 100644
index 000000000000..51f6cdb2cb89
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesCheckerTest extends SysuiTestCase {
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
+ @Mock
+ private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock
+ private LocalBluetoothAdapter mLocalBluetoothAdapter;
+ @Mock
+ private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
+ @Mock
+ private CachedBluetoothDevice mCachedDevice;
+ @Mock
+ private BluetoothDevice mDevice;
+ private HearingDevicesChecker mDevicesChecker;
+
+ @Before
+ public void setUp() {
+ when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
+ when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
+ mCachedBluetoothDeviceManager);
+ when(mCachedBluetoothDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ null);
+
+ mDevicesChecker = new HearingDevicesChecker(mContext, mLocalBluetoothManager);
+ }
+
+ @Test
+ public void isAnyPairedHearingDevice_bluetoothDisable_returnFalse() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
+
+ assertThat(mDevicesChecker.isAnyPairedHearingDevice()).isFalse();
+ }
+
+ @Test
+ public void isAnyActiveHearingDevice_bluetoothDisable_returnFalse() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
+
+ assertThat(mDevicesChecker.isAnyActiveHearingDevice()).isFalse();
+ }
+
+ @Test
+ public void isAnyPairedHearingDevice_hearingAidBonded_returnTrue() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mCachedDevice.isHearingAidDevice()).thenReturn(true);
+ when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ mCachedDevices.add(mCachedDevice);
+
+ assertThat(mDevicesChecker.isAnyPairedHearingDevice()).isTrue();
+ }
+
+ @Test
+ public void isAnyActiveHearingDevice_hearingAidActiveAndConnected_returnTrue() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mCachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
+ when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mDevice.isConnected()).thenReturn(true);
+ when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+ mCachedDevices.add(mCachedDevice);
+
+ assertThat(mDevicesChecker.isAnyActiveHearingDevice()).isTrue();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
index e9c742d63d81..cb9c26c7a4b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
@@ -21,20 +21,17 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.bluetooth.BluetoothDevice;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
-import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.animation.Expandable;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Rule;
@@ -44,9 +41,6 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.ArrayList;
-import java.util.List;
-
/** Tests for {@link HearingDevicesDialogManager}. */
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -56,7 +50,8 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
- private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
+ private final FakeExecutor mMainExecutor = new FakeExecutor(new FakeSystemClock());
+ private final FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
private Expandable mExpandable;
@Mock
@@ -68,13 +63,7 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
@Mock
private SystemUIDialog mDialog;
@Mock
- private LocalBluetoothManager mLocalBluetoothManager;
- @Mock
- private LocalBluetoothAdapter mLocalBluetoothAdapter;
- @Mock
- private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
- @Mock
- private CachedBluetoothDevice mCachedDevice;
+ private HearingDevicesChecker mDevicesChecker;
private HearingDevicesDialogManager mManager;
@@ -82,36 +71,35 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
public void setUp() {
when(mDialogFactory.create(anyBoolean())).thenReturn(mDialogDelegate);
when(mDialogDelegate.createDialog()).thenReturn(mDialog);
- when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
- when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
- mCachedBluetoothDeviceManager);
- when(mCachedBluetoothDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
mManager = new HearingDevicesDialogManager(
mDialogTransitionAnimator,
mDialogFactory,
- mLocalBluetoothManager
+ mDevicesChecker,
+ mBackgroundExecutor,
+ mMainExecutor
);
}
@Test
- public void showDialog_bluetoothDisable_showPairNewDeviceTrue() {
- when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
+ public void showDialog_existHearingDevice_showPairNewDeviceFalse() {
+ when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(true);
mManager.showDialog(mExpandable);
+ mBackgroundExecutor.runAllReady();
+ mMainExecutor.runAllReady();
- verify(mDialogFactory).create(eq(true));
+ verify(mDialogFactory).create(eq(/* showPairNewDevice= */ false));
}
@Test
- public void showDialog_containsHearingAid_showPairNewDeviceFalse() {
- when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
- when(mCachedDevice.isHearingAidDevice()).thenReturn(true);
- when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
- mCachedDevices.add(mCachedDevice);
+ public void showDialog_noHearingDevice_showPairNewDeviceTrue() {
+ when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(false);
mManager.showDialog(mExpandable);
+ mBackgroundExecutor.runAllReady();
+ mMainExecutor.runAllReady();
- verify(mDialogFactory).create(eq(false));
+ verify(mDialogFactory).create(eq(/* showPairNewDevice= */ true));
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
index 59ee0b843043..76c8cf081262 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
@@ -28,6 +28,7 @@ import android.os.Handler;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
+import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
import android.view.View;
@@ -37,14 +38,18 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker;
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
import com.android.systemui.animation.Expandable;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.policy.BluetoothController;
import org.junit.After;
import org.junit.Before;
@@ -78,7 +83,11 @@ public class HearingDevicesTileTest extends SysuiTestCase {
@Mock
private QSLogger mQSLogger;
@Mock
+ private HearingDevicesChecker mDevicesChecker;
+ @Mock
HearingDevicesDialogManager mHearingDevicesDialogManager;
+ @Mock
+ BluetoothController mBluetoothController;
private TestableLooper mTestableLooper;
private HearingDevicesTile mTile;
@@ -98,7 +107,9 @@ public class HearingDevicesTileTest extends SysuiTestCase {
mStatusBarStateController,
mActivityStarter,
mQSLogger,
- mHearingDevicesDialogManager);
+ mHearingDevicesDialogManager,
+ mDevicesChecker,
+ mBluetoothController);
mTile.initialize();
mTestableLooper.processAllMessages();
@@ -142,4 +153,41 @@ public class HearingDevicesTileTest extends SysuiTestCase {
verify(mHearingDevicesDialogManager).showDialog(expandable);
}
+
+ @Test
+ public void handleUpdateState_activeHearingDevice_stateActiveConnectedLabel() {
+ when(mDevicesChecker.isAnyActiveHearingDevice()).thenReturn(true);
+ when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(true);
+
+ BooleanState activeState = new BooleanState();
+ mTile.handleUpdateState(activeState, null);
+
+ assertThat(activeState.state).isEqualTo(Tile.STATE_ACTIVE);
+ assertThat(activeState.secondaryLabel.toString()).isEqualTo(
+ mContext.getString(R.string.quick_settings_hearing_devices_connected));
+ }
+
+ @Test
+ public void handleUpdateState_bondedInactiveHearingDevice_stateInactiveDisconnectedLabel() {
+ when(mDevicesChecker.isAnyActiveHearingDevice()).thenReturn(false);
+ when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(true);
+
+ BooleanState disconnectedState = new BooleanState();
+ mTile.handleUpdateState(disconnectedState, null);
+
+ assertThat(disconnectedState.state).isEqualTo(Tile.STATE_INACTIVE);
+ assertThat(disconnectedState.secondaryLabel.toString()).isEqualTo(
+ mContext.getString(R.string.quick_settings_hearing_devices_disconnected));
+ }
+
+ @Test
+ public void handleUpdateState_noHearingDevice_stateInactive() {
+ when(mDevicesChecker.isAnyActiveHearingDevice()).thenReturn(false);
+ when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(false);
+
+ BooleanState inactiveState = new BooleanState();
+ mTile.handleUpdateState(inactiveState, null);
+
+ assertThat(inactiveState.state).isEqualTo(Tile.STATE_INACTIVE);
+ }
}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 69f07d5c5f7b..fc75bf47ed96 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -81,6 +81,8 @@ import android.util.Log;
import android.util.Slog;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.uri.UriGrantsManagerInternal;
@@ -229,6 +231,14 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
private int mPolicies;
+ private final Runnable mUserEngagementTimeoutExpirationRunnable =
+ () -> {
+ synchronized (mLock) {
+ updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true);
+ }
+ };
+
+ @GuardedBy("mLock")
private @UserEngagementState int mUserEngagementState = USER_DISENGAGED;
@IntDef({USER_PERMANENTLY_ENGAGED, USER_TEMPORARY_ENGAGED, USER_DISENGAGED})
@@ -238,26 +248,26 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
/**
* Indicates that the session is active and in one of the user engaged states.
*
- * @see #updateUserEngagedStateIfNeededLocked(boolean) ()
+ * @see #updateUserEngagedStateIfNeededLocked(boolean)
*/
private static final int USER_PERMANENTLY_ENGAGED = 0;
/**
* Indicates that the session is active and in {@link PlaybackState#STATE_PAUSED} state.
*
- * @see #updateUserEngagedStateIfNeededLocked(boolean) ()
+ * @see #updateUserEngagedStateIfNeededLocked(boolean)
*/
private static final int USER_TEMPORARY_ENGAGED = 1;
/**
* Indicates that the session is either not active or in one of the user disengaged states
*
- * @see #updateUserEngagedStateIfNeededLocked(boolean) ()
+ * @see #updateUserEngagedStateIfNeededLocked(boolean)
*/
private static final int USER_DISENGAGED = 2;
/**
- * Indicates the duration of the temporary engaged states.
+ * Indicates the duration of the temporary engaged states, in milliseconds.
*
* <p>Some {@link MediaSession} states like {@link PlaybackState#STATE_PAUSED} are temporarily
* engaged, meaning the corresponding session is only considered in an engaged state for the
@@ -270,7 +280,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
* user-engaged state is not considered user-engaged when transitioning from a non-user engaged
* state {@link PlaybackState#STATE_STOPPED}.
*/
- private static final int TEMP_USER_ENGAGED_TIMEOUT = 600000;
+ private static final int TEMP_USER_ENGAGED_TIMEOUT_MS = 600000;
public MediaSessionRecord(
int ownerPid,
@@ -609,8 +619,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
@Override
public void expireTempEngaged() {
- mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout);
- updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true);
+ mHandler.post(mUserEngagementTimeoutExpirationRunnable);
}
/**
@@ -1086,11 +1095,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
}
};
- private final Runnable mHandleTempEngagedSessionTimeout =
- () -> {
- updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true);
- };
-
@RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
private static boolean componentNameExists(
@NonNull ComponentName componentName, @NonNull Context context, int userId) {
@@ -1107,10 +1111,14 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
return !resolveInfos.isEmpty();
}
+ @GuardedBy("mLock")
private void updateUserEngagedStateIfNeededLocked(boolean isTimeoutExpired) {
+ if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+ return;
+ }
int oldUserEngagedState = mUserEngagementState;
int newUserEngagedState;
- if (!isActive() || mPlaybackState == null) {
+ if (!isActive() || mPlaybackState == null || mDestroyed) {
newUserEngagedState = USER_DISENGAGED;
} else if (isActive() && mPlaybackState.isActive()) {
newUserEngagedState = USER_PERMANENTLY_ENGAGED;
@@ -1126,18 +1134,22 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
return;
}
+ mUserEngagementState = newUserEngagedState;
if (newUserEngagedState == USER_TEMPORARY_ENGAGED) {
- mHandler.postDelayed(mHandleTempEngagedSessionTimeout, TEMP_USER_ENGAGED_TIMEOUT);
- } else if (oldUserEngagedState == USER_TEMPORARY_ENGAGED) {
- mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout);
+ mHandler.postDelayed(
+ mUserEngagementTimeoutExpirationRunnable, TEMP_USER_ENGAGED_TIMEOUT_MS);
+ } else {
+ mHandler.removeCallbacks(mUserEngagementTimeoutExpirationRunnable);
}
boolean wasUserEngaged = oldUserEngagedState != USER_DISENGAGED;
boolean isNowUserEngaged = newUserEngagedState != USER_DISENGAGED;
- mUserEngagementState = newUserEngagedState;
if (wasUserEngaged != isNowUserEngaged) {
- mService.onSessionUserEngagementStateChange(
- /* mediaSessionRecord= */ this, /* isUserEngaged= */ isNowUserEngaged);
+ mHandler.post(
+ () ->
+ mService.onSessionUserEngagementStateChange(
+ /* mediaSessionRecord= */ this,
+ /* isUserEngaged= */ isNowUserEngaged));
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 399866728770..f02a3fff12f5 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -715,6 +715,12 @@ public class MediaSessionService extends SystemService implements Monitor {
ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) {
final long token = Binder.clearCallingIdentity();
try {
+ Log.i(
+ TAG,
+ TextUtils.formatSimple(
+ "startFgsDelegate: pkg=%s uid=%d",
+ foregroundServiceDelegationOptions.mClientPackageName,
+ foregroundServiceDelegationOptions.mClientUid));
mActivityManagerInternal.startForegroundServiceDelegate(
foregroundServiceDelegationOptions, /* connection= */ null);
} finally {
@@ -756,6 +762,12 @@ public class MediaSessionService extends SystemService implements Monitor {
ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) {
final long token = Binder.clearCallingIdentity();
try {
+ Log.i(
+ TAG,
+ TextUtils.formatSimple(
+ "stopFgsDelegate: pkg=%s uid=%d",
+ foregroundServiceDelegationOptions.mClientPackageName,
+ foregroundServiceDelegationOptions.mClientUid));
mActivityManagerInternal.stopForegroundServiceDelegate(
foregroundServiceDelegationOptions);
} finally {
@@ -2679,6 +2691,9 @@ public class MediaSessionService extends SystemService implements Monitor {
@Override
public void expireTempEngagedSessions() {
+ if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+ return;
+ }
synchronized (mLock) {
for (Set<MediaSessionRecordImpl> uidSessions :
mUserEngagedSessionsForFgs.values()) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2d2a88a866ba..fec1af47d6e6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -803,6 +803,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
final LetterboxUiController mLetterboxUiController;
/**
+ * The policy for transparent activities
+ */
+ final TransparentPolicy mTransparentPolicy;
+
+ /**
* The scale to fit at least one side of the activity to its parent. If the activity uses
* 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5.
*/
@@ -1698,7 +1703,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
if (isState(RESUMED)) {
newParent.setResumedActivity(this, "onParentChanged");
}
- mLetterboxUiController.updateInheritedLetterbox();
+ mTransparentPolicy.start();
}
if (rootTask != null && rootTask.topRunningActivity() == this) {
@@ -2136,6 +2141,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
// Don't move below setOrientation(info.screenOrientation) since it triggers
// getOverrideOrientation that requires having mLetterboxUiController
// initialised.
+ mTransparentPolicy = new TransparentPolicy(this, mWmService.mLetterboxConfiguration);
mLetterboxUiController = new LetterboxUiController(mWmService, this);
mCameraCompatControlEnabled = mWmService.mContext.getResources()
.getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
@@ -8080,13 +8086,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
@Configuration.Orientation
int getRequestedConfigurationOrientation(boolean forDisplay,
@ActivityInfo.ScreenOrientation int requestedOrientation) {
- if (mLetterboxUiController.hasInheritedOrientation()) {
+ if (mTransparentPolicy.hasInheritedOrientation()) {
final RootDisplayArea root = getRootDisplayArea();
if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) {
return reverseConfigurationOrientation(
- mLetterboxUiController.getInheritedOrientation());
+ mTransparentPolicy.getInheritedOrientation());
} else {
- return mLetterboxUiController.getInheritedOrientation();
+ return mTransparentPolicy.getInheritedOrientation();
}
}
if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) {
@@ -8302,8 +8308,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
@Nullable
CompatDisplayInsets getCompatDisplayInsets() {
- if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
- return mLetterboxUiController.getInheritedCompatDisplayInsets();
+ if (mTransparentPolicy.isRunning()) {
+ return mTransparentPolicy.getInheritedCompatDisplayInsets();
}
return mCompatDisplayInsets;
}
@@ -8466,7 +8472,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
mSizeCompatBounds = null;
mCompatDisplayInsets = null;
- mLetterboxUiController.clearInheritedCompatDisplayInsets();
+ mTransparentPolicy.clearInheritedCompatDisplayInsets();
}
@VisibleForTesting
@@ -8784,8 +8790,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
return APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
}
// TODO(b/256564921): Investigate if we need new metrics for translucent activities
- if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
- return mLetterboxUiController.getInheritedAppCompatState();
+ if (mTransparentPolicy.isRunning()) {
+ return mTransparentPolicy.getInheritedAppCompatState();
}
if (mInSizeCompatModeForBounds) {
return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
@@ -8938,7 +8944,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
// We check if the current activity is transparent. In that case we need to
// recomputeConfiguration of the first opaque activity beneath, to allow a
// proper computation of the new bounds.
- if (!mLetterboxUiController.applyOnOpaqueActivityBelow(
+ if (!mTransparentPolicy.applyOnOpaqueActivityBelow(
ActivityRecord::recomputeConfiguration)) {
onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration());
}
@@ -9411,7 +9417,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
// Only allow to scale down.
- mSizeCompatScale = mLetterboxUiController.findOpaqueNotFinishingActivityBelow()
+ mSizeCompatScale = mTransparentPolicy.findOpaqueNotFinishingActivityBelow()
.map(activityRecord -> activityRecord.mSizeCompatScale)
.orElseGet(() -> {
final int contentW = resolvedAppBounds.width();
@@ -9424,7 +9430,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) {
- if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+ if (mTransparentPolicy.isRunning()) {
// To avoid wrong app behaviour, we decided to disable SCM when a translucent activity
// is letterboxed.
return false;
@@ -9487,7 +9493,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
public Rect getBounds() {
// TODO(b/268458693): Refactor configuration inheritance in case of translucent activities
final Rect superBounds = super.getBounds();
- return mLetterboxUiController.findOpaqueNotFinishingActivityBelow()
+ return mTransparentPolicy.findOpaqueNotFinishingActivityBelow()
.map(ActivityRecord::getBounds)
.orElseGet(() -> {
if (mSizeCompatBounds != null) {
@@ -9851,8 +9857,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
* Returns the min aspect ratio of this activity.
*/
float getMinAspectRatio() {
- if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
- return mLetterboxUiController.getInheritedMinAspectRatio();
+ if (mTransparentPolicy.isRunning()) {
+ return mTransparentPolicy.getInheritedMinAspectRatio();
}
if (info.applicationInfo == null) {
return info.getMinAspectRatio();
@@ -9902,8 +9908,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
float getMaxAspectRatio() {
- if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
- return mLetterboxUiController.getInheritedMaxAspectRatio();
+ if (mTransparentPolicy.isRunning()) {
+ return mTransparentPolicy.getInheritedMaxAspectRatio();
}
return info.getMaxAspectRatio();
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a9192c4c6139..d7a696f47b7e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1365,18 +1365,6 @@ class ActivityStarter {
request.outActivity[0] = mLastStartActivityRecord;
}
- // Reset the ActivityRecord#mCurrentLaunchCanTurnScreenOn state of activity started
- // before this one if it is no longer the top-most focusable activity.
- // Doing so in case the state is not yet consumed during rapid activity launch.
- if (previousStart != null && !previousStart.finishing && previousStart.isAttached()
- && previousStart.currentLaunchCanTurnScreenOn()) {
- final ActivityRecord topFocusable = previousStart.getDisplayContent().getActivity(
- ar -> ar.isFocusable() && !ar.finishing);
- if (previousStart != topFocusable) {
- previousStart.setCurrentLaunchCanTurnScreenOn(false);
- }
- }
-
return mLastStartActivityResult;
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 0f5b6c516909..cfd5300417b4 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1510,7 +1510,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
a.persistableMode = ActivityInfo.PERSIST_NEVER;
a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
- a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
+ a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_SHOW_WHEN_LOCKED;
a.configChanges = 0xffffffff;
if (homePanelDream()) {
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 125eb2a3a810..be44629a1fcf 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -194,6 +194,16 @@ public class DeferredDisplayUpdater implements DisplayUpdater {
final Rect startBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth,
mDisplayContent.mInitialDisplayHeight);
final int fromRotation = mDisplayContent.getRotation();
+ if (Flags.blastSyncNotificationShadeOnDisplaySwitch() && physicalDisplayUpdated) {
+ final WindowState notificationShade =
+ mDisplayContent.getDisplayPolicy().getNotificationShade();
+ if (notificationShade != null && notificationShade.isVisible()
+ && mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing(
+ mDisplayContent.mDisplayId)) {
+ Slog.i(TAG, notificationShade + " uses blast for display switch");
+ notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
+ }
+ }
mDisplayContent.mAtmService.deferWindowLayout();
try {
diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags
index d957591ab7f9..cc2249de010c 100644
--- a/services/core/java/com/android/server/wm/EventLogTags.logtags
+++ b/services/core/java/com/android/server/wm/EventLogTags.logtags
@@ -59,6 +59,10 @@ option java_package com.android.server.wm
31002 wm_task_moved (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(ToTop|1),(Index|1)
# Task removed with source explanation.
31003 wm_task_removed (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(Reason|3)
+# Embedded TaskFragment created
+31004 wm_tf_created (Token|1|5),(TaskId|1|5)
+# Embedded TaskFragment removed
+31005 wm_tf_removed (Token|1|5),(TaskId|1|5)
# Set the requested orientation of an activity.
31006 wm_set_requested_orientation (Orientation|1|5),(Component Name|3)
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index b8d0694047c7..194771f6b387 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -40,7 +40,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
import static android.content.pm.ActivityInfo.isFixedOrientation;
import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
@@ -54,10 +53,6 @@ import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
-import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
-import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
-import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
@@ -80,7 +75,6 @@ import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANG
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
@@ -135,12 +129,7 @@ import com.android.server.wm.utils.OptPropFactory.OptProp;
import com.android.window.flags.Flags;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
/** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
// TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
@@ -150,13 +139,8 @@ import java.util.function.Predicate;
// TODO(b/263021211): Consider renaming to more generic CompatUIController.
final class LetterboxUiController {
- private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
- ActivityRecord::occludesParent;
-
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
- private static final float UNDEFINED_ASPECT_RATIO = 0f;
-
// Minimum value of mSetOrientationRequestCounter before qualifying as orientation request loop
@VisibleForTesting
static final int MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP = 2;
@@ -188,10 +172,6 @@ final class LetterboxUiController {
// Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
private final boolean mIsOverrideRespectRequestedOrientationEnabled;
- // The list of observers for the destroy event of candidate opaque activities
- // when dealing with translucent activities.
- private final List<LetterboxUiController> mDestroyListeners = new ArrayList<>();
-
@NonNull
private final OptProp mAllowOrientationOverrideOptProp;
@NonNull
@@ -206,34 +186,11 @@ final class LetterboxUiController {
@NonNull
private final OptProp mAllowUserAspectRatioFullscreenOverrideOptProp;
- /*
- * WindowContainerListener responsible to make translucent activities inherit
- * constraints from the first opaque activity beneath them. It's null for not
- * translucent activities.
- */
- @Nullable
- private WindowContainerListener mLetterboxConfigListener;
-
- @Nullable
- @VisibleForTesting
- ActivityRecord mFirstOpaqueActivityBeneath;
-
private boolean mShowWallpaperForLetterboxBackground;
- // In case of transparent activities we might need to access the aspectRatio of the
- // first opaque activity beneath.
- private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
- private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
-
// Updated when ActivityRecord#setRequestedOrientation is called
private long mTimeMsLastSetOrientationRequest = 0;
- @Configuration.Orientation
- private int mInheritedOrientation = ORIENTATION_UNDEFINED;
-
- // The app compat state for the opaque activity if any
- private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
-
// Counter for ActivityRecord#setRequestedOrientation
private int mSetOrientationRequestCounter = 0;
@@ -242,9 +199,6 @@ final class LetterboxUiController {
@PackageManager.UserMinAspectRatio
private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
- // The CompatDisplayInsets of the opaque activity beneath the translucent one.
- private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
-
@Nullable
private Letterbox mLetterbox;
@@ -361,14 +315,7 @@ final class LetterboxUiController {
mLetterbox.destroy();
mLetterbox = null;
}
- for (int i = mDestroyListeners.size() - 1; i >= 0; i--) {
- mDestroyListeners.get(i).updateInheritedLetterbox();
- }
- mDestroyListeners.clear();
- if (mLetterboxConfigListener != null) {
- mLetterboxConfigListener.onRemoved();
- mLetterboxConfigListener = null;
- }
+ mActivityRecord.mTransparentPolicy.stop();
}
void onMovedToDisplay(int displayId) {
@@ -877,7 +824,7 @@ final class LetterboxUiController {
// For this reason we use ActivityRecord#getBounds() that the translucent activity
// inherits from the first opaque activity beneath and also takes care of the scaling
// in case of activities in size compat mode.
- final Rect innerFrame = hasInheritedLetterboxBehavior()
+ final Rect innerFrame = mActivityRecord.mTransparentPolicy.isRunning()
? mActivityRecord.getBounds() : w.getFrame();
mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
if (mDoubleTapEvent) {
@@ -1343,9 +1290,9 @@ final class LetterboxUiController {
}
// Use screen resolved bounds which uses resolved bounds or size compat bounds
// as activity bounds can sometimes be empty
- final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
- ? mFirstOpaqueActivityBeneath.getScreenResolvedBounds()
- : mActivityRecord.getScreenResolvedBounds();
+ final Rect opaqueActivityBounds = mActivityRecord.mTransparentPolicy
+ .getFirstOpaqueActivity().map(ActivityRecord::getScreenResolvedBounds)
+ .orElse(mActivityRecord.getScreenResolvedBounds());
return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
@@ -1380,10 +1327,10 @@ final class LetterboxUiController {
return false;
}
// Use screen resolved bounds which uses resolved bounds or size compat bounds
- // as activity bounds can sometimes be empty
- final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
- ? mFirstOpaqueActivityBeneath.getScreenResolvedBounds()
- : mActivityRecord.getScreenResolvedBounds();
+ // as activity bounds can sometimes be empty.
+ final Rect opaqueActivityBounds = mActivityRecord.mTransparentPolicy
+ .getFirstOpaqueActivity().map(ActivityRecord::getScreenResolvedBounds)
+ .orElse(mActivityRecord.getScreenResolvedBounds());
return mLetterboxConfiguration.getIsVerticalReachabilityEnabled()
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
@@ -1490,7 +1437,8 @@ final class LetterboxUiController {
// corners because we assume the specific layout would. This is the case when the layout
// of the translucent activity uses only a part of all the bounds because of the use of
// LayoutParams.WRAP_CONTENT.
- if (hasInheritedLetterboxBehavior() && (cropBounds.width() != mainWindow.mRequestedWidth
+ if (mActivityRecord.mTransparentPolicy.isRunning()
+ && (cropBounds.width() != mainWindow.mRequestedWidth
|| cropBounds.height() != mainWindow.mRequestedHeight)) {
return null;
}
@@ -1794,173 +1742,6 @@ final class LetterboxUiController {
);
}
- /**
- * Handles translucent activities letterboxing inheriting constraints from the
- * first opaque activity beneath.
- * @param parent The parent container.
- */
- void updateInheritedLetterbox() {
- final WindowContainer<?> parent = mActivityRecord.getParent();
- if (parent == null) {
- return;
- }
- if (!mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
- return;
- }
- if (mLetterboxConfigListener != null) {
- mLetterboxConfigListener.onRemoved();
- clearInheritedConfig();
- }
- // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
- // opaque activity constraints because we're expecting the activity is already letterboxed.
- mFirstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
- FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
- mActivityRecord /* boundary */, false /* includeBoundary */,
- true /* traverseTopToBottom */);
- if (mFirstOpaqueActivityBeneath == null || mFirstOpaqueActivityBeneath.isEmbedded()) {
- // We skip letterboxing if the translucent activity doesn't have any opaque
- // activities beneath or the activity below is embedded which never has letterbox.
- mActivityRecord.recomputeConfiguration();
- return;
- }
- if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
- || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
- return;
- }
- mFirstOpaqueActivityBeneath.mLetterboxUiController.mDestroyListeners.add(this);
- inheritConfiguration(mFirstOpaqueActivityBeneath);
- mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
- mActivityRecord, mFirstOpaqueActivityBeneath,
- (opaqueConfig, transparentOverrideConfig) -> {
- resetTranslucentOverrideConfig(transparentOverrideConfig);
- final Rect parentBounds = parent.getWindowConfiguration().getBounds();
- final Rect bounds = transparentOverrideConfig.windowConfiguration.getBounds();
- final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
- // We cannot use letterboxBounds directly here because the position relies on
- // letterboxing. Using letterboxBounds directly, would produce a double offset.
- bounds.set(parentBounds.left, parentBounds.top,
- parentBounds.left + letterboxBounds.width(),
- parentBounds.top + letterboxBounds.height());
- // We need to initialize appBounds to avoid NPE. The actual value will
- // be set ahead when resolving the Configuration for the activity.
- transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect());
- inheritConfiguration(mFirstOpaqueActivityBeneath);
- return transparentOverrideConfig;
- });
- }
-
- /**
- * @return {@code true} if the current activity is translucent with an opaque activity
- * beneath. In this case it will inherit bounds, orientation and aspect ratios from
- * the first opaque activity beneath.
- */
- boolean hasInheritedLetterboxBehavior() {
- return mLetterboxConfigListener != null;
- }
-
- /**
- * @return {@code true} if the current activity is translucent with an opaque activity
- * beneath and needs to inherit its orientation.
- */
- boolean hasInheritedOrientation() {
- // To force a different orientation, the transparent one needs to have an explicit one
- // otherwise the existing one is fine and the actual orientation will depend on the
- // bounds.
- // To avoid wrong behaviour, we're not forcing orientation for activities with not
- // fixed orientation (e.g. permission dialogs).
- return hasInheritedLetterboxBehavior()
- && mActivityRecord.getOverrideOrientation()
- != SCREEN_ORIENTATION_UNSPECIFIED;
- }
-
- float getInheritedMinAspectRatio() {
- return mInheritedMinAspectRatio;
- }
-
- float getInheritedMaxAspectRatio() {
- return mInheritedMaxAspectRatio;
- }
-
- int getInheritedAppCompatState() {
- return mInheritedAppCompatState;
- }
-
- @Configuration.Orientation
- int getInheritedOrientation() {
- return mInheritedOrientation;
- }
-
- ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
- return mInheritedCompatDisplayInsets;
- }
-
- void clearInheritedCompatDisplayInsets() {
- mInheritedCompatDisplayInsets = null;
- }
-
- /**
- * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque
- * activity beneath using the given consumer and returns {@code true}.
- */
- boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
- return findOpaqueNotFinishingActivityBelow()
- .map(activityRecord -> {
- consumer.accept(activityRecord);
- return true;
- }).orElse(false);
- }
-
- /**
- * @return The first not finishing opaque activity beneath the current translucent activity
- * if it exists and the strategy is enabled.
- */
- Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
- if (!hasInheritedLetterboxBehavior() || mActivityRecord.getTask() == null) {
- return Optional.empty();
- }
- return Optional.ofNullable(mFirstOpaqueActivityBeneath);
- }
-
- /** Resets the screen size related fields so they can be resolved by requested bounds later. */
- private static void resetTranslucentOverrideConfig(Configuration config) {
- // The values for the following properties will be defined during the configuration
- // resolution in {@link ActivityRecord#resolveOverrideConfiguration} using the
- // properties inherited from the first not finishing opaque activity beneath.
- config.orientation = ORIENTATION_UNDEFINED;
- config.screenWidthDp = config.compatScreenWidthDp = SCREEN_WIDTH_DP_UNDEFINED;
- config.screenHeightDp = config.compatScreenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED;
- config.smallestScreenWidthDp = config.compatSmallestScreenWidthDp =
- SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
- }
-
- private void inheritConfiguration(ActivityRecord firstOpaque) {
- // To avoid wrong behaviour, we're not forcing a specific aspect ratio to activities
- // which are not already providing one (e.g. permission dialogs) and presumably also
- // not resizable.
- if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) {
- mInheritedMinAspectRatio = firstOpaque.getMinAspectRatio();
- }
- if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) {
- mInheritedMaxAspectRatio = firstOpaque.getMaxAspectRatio();
- }
- mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation();
- mInheritedAppCompatState = firstOpaque.getAppCompatState();
- mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets();
- }
-
- private void clearInheritedConfig() {
- if (mFirstOpaqueActivityBeneath != null) {
- mFirstOpaqueActivityBeneath.mLetterboxUiController.mDestroyListeners.remove(this);
- }
- mFirstOpaqueActivityBeneath = null;
- mLetterboxConfigListener = null;
- mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
- mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
- mInheritedOrientation = ORIENTATION_UNDEFINED;
- mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
- mInheritedCompatDisplayInsets = null;
- }
-
@NonNull
private static BooleanSupplier asLazy(@NonNull BooleanSupplier supplier) {
return new BooleanSupplier() {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 61022cc971e2..b8b746a3de7f 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2999,6 +2999,9 @@ class TaskFragment extends WindowContainer<WindowContainer> {
@Override
void removeImmediately() {
+ if (asTask() == null) {
+ EventLogTags.writeWmTfRemoved(System.identityHashCode(this), getTaskId());
+ }
mIsRemovalRequested = false;
resetAdjacentTaskFragment();
cleanUpEmbeddedTaskFragment();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 7b1c66132c7e..4aa3e3644daa 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1374,13 +1374,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
// processed all the participants first (in particular, we want to trigger pip-enter first)
for (int i = 0; i < mParticipants.size(); ++i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
+ if (ar == null) continue;
+
// If the activity was just inserted to an invisible task, it will keep INITIALIZING
// state. Then no need to notify the callback to avoid clearing some states
// unexpectedly, e.g. launch-task-behind.
- if (ar != null && (ar.isVisibleRequested()
- || !ar.isState(ActivityRecord.State.INITIALIZING))) {
+ if (ar.isVisibleRequested() || !ar.isState(ActivityRecord.State.INITIALIZING)) {
mController.dispatchLegacyAppTransitionFinished(ar);
}
+
+ // Reset the ActivityRecord#mCurrentLaunchCanTurnScreenOn state if it is not the top
+ // running activity. Doing so in case the state is not yet consumed during rapid
+ // activity launch.
+ if (ar.currentLaunchCanTurnScreenOn() && ar.getDisplayContent() != null
+ && ar.getDisplayContent().topRunningActivity() != ar) {
+ ar.setCurrentLaunchCanTurnScreenOn(false);
+ }
}
// Update the input-sink (touch-blocking) state now that the animation is finished.
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
new file mode 100644
index 000000000000..b408397d1861
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TransparentPolicy.java
@@ -0,0 +1,353 @@
+/*
+ * 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.server.wm;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
+
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Encapsulate logic about translucent activities.
+ * <p/>
+ * An activity is defined as translucent if {@link ActivityRecord#fillsParent()} returns
+ * {@code false}. When the policy is running for a letterboxed activity, a transparent activity
+ * will inherit constraints about bounds, aspect ratios and orientation from the first not finishing
+ * activity below.
+ */
+class TransparentPolicy {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "TransparentPolicy" : TAG_ATM;
+
+ // The predicate used to find the first opaque not finishing activity below the potential
+ // transparent activity.
+ private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
+ ActivityRecord::occludesParent;
+
+ // The ActivityRecord this policy relates to.
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+
+ // If transparent activity policy is enabled.
+ @NonNull
+ private final BooleanSupplier mIsTranslucentLetterboxingEnabledSupplier;
+
+ // The list of observers for the destroy event of candidate opaque activities
+ // when dealing with translucent activities.
+ @NonNull
+ private final List<TransparentPolicy> mDestroyListeners = new ArrayList<>();
+
+ // The current state for the possible transparent activity
+ @NonNull
+ private final TransparentPolicyState mTransparentPolicyState;
+
+ TransparentPolicy(@NonNull ActivityRecord activityRecord,
+ @NonNull LetterboxConfiguration letterboxConfiguration) {
+ mActivityRecord = activityRecord;
+ mIsTranslucentLetterboxingEnabledSupplier =
+ letterboxConfiguration::isTranslucentLetterboxingEnabled;
+ mTransparentPolicyState = new TransparentPolicyState(activityRecord);
+ }
+
+ /**
+ * Handles translucent activities letterboxing inheriting constraints from the
+ * first opaque activity beneath.
+ */
+ void start() {
+ if (!mIsTranslucentLetterboxingEnabledSupplier.getAsBoolean()) {
+ return;
+ }
+ final WindowContainer<?> parent = mActivityRecord.getParent();
+ if (parent == null) {
+ return;
+ }
+ mTransparentPolicyState.reset();
+ // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
+ // opaque activity constraints because we're expecting the activity is already letterboxed.
+ final ActivityRecord firstOpaqueActivity = mActivityRecord.getTask().getActivity(
+ FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
+ mActivityRecord /* boundary */, false /* includeBoundary */,
+ true /* traverseTopToBottom */);
+ // We check if we need for some reason to skip the policy gievn the specific first
+ // opaque activity
+ if (shouldSkipTransparentPolicy(firstOpaqueActivity)) {
+ return;
+ }
+ mTransparentPolicyState.start(firstOpaqueActivity);
+ }
+
+ void stop() {
+ for (int i = mDestroyListeners.size() - 1; i >= 0; i--) {
+ mDestroyListeners.get(i).start();
+ }
+ mDestroyListeners.clear();
+ mTransparentPolicyState.reset();
+ }
+
+ /**
+ * @return {@code true} if the current activity is translucent with an opaque activity
+ * beneath and the related policy is running. In this case it will inherit bounds, orientation
+ * and aspect ratios from the first opaque activity beneath.
+ */
+ boolean isRunning() {
+ return mTransparentPolicyState.isRunning();
+ }
+
+ /**
+ * @return {@code true} if the current activity is translucent with an opaque activity
+ * beneath and needs to inherit its orientation.
+ */
+ boolean hasInheritedOrientation() {
+ // To avoid wrong behaviour (e.g. permission dialogs not centered or with wrong size),
+ // transparent activities inherit orientation from the first opaque activity below only if
+ // they explicitly define an orientation different from SCREEN_ORIENTATION_UNSPECIFIED.
+ return isRunning()
+ && mActivityRecord.getOverrideOrientation()
+ != SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+
+ float getInheritedMinAspectRatio() {
+ return mTransparentPolicyState.mInheritedMinAspectRatio;
+ }
+
+ float getInheritedMaxAspectRatio() {
+ return mTransparentPolicyState.mInheritedMaxAspectRatio;
+ }
+
+ int getInheritedAppCompatState() {
+ return mTransparentPolicyState.mInheritedAppCompatState;
+ }
+
+ @Configuration.Orientation
+ int getInheritedOrientation() {
+ return mTransparentPolicyState.mInheritedOrientation;
+ }
+
+ ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
+ return mTransparentPolicyState.mInheritedCompatDisplayInsets;
+ }
+
+ void clearInheritedCompatDisplayInsets() {
+ mTransparentPolicyState.clearInheritedCompatDisplayInsets();
+ }
+
+ TransparentPolicyState getTransparentPolicyState() {
+ return mTransparentPolicyState;
+ }
+
+ /**
+ * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque
+ * activity beneath using the given consumer and returns {@code true}.
+ */
+ boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
+ return mTransparentPolicyState.applyOnOpaqueActivityBelow(consumer);
+ }
+
+ @NonNull
+ Optional<ActivityRecord> getFirstOpaqueActivity() {
+ return isRunning() ? Optional.of(mTransparentPolicyState.mFirstOpaqueActivity)
+ : Optional.empty();
+ }
+
+ /**
+ * @return The first not finishing opaque activity beneath the current translucent activity
+ * if it exists and the strategy is enabled.
+ */
+ Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
+ return mTransparentPolicyState.findOpaqueNotFinishingActivityBelow();
+ }
+
+ // We evaluate the case when the policy should not be applied.
+ private boolean shouldSkipTransparentPolicy(@Nullable ActivityRecord opaqueActivity) {
+ if (opaqueActivity == null || opaqueActivity.isEmbedded()) {
+ // We skip letterboxing if the translucent activity doesn't have any
+ // opaque activities beneath or the activity below is embedded which
+ // never has letterbox.
+ mActivityRecord.recomputeConfiguration();
+ return true;
+ }
+ if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
+ || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Resets the screen size related fields so they can be resolved by requested bounds later. */
+ private static void resetTranslucentOverrideConfig(Configuration config) {
+ // The values for the following properties will be defined during the configuration
+ // resolution in {@link ActivityRecord#resolveOverrideConfiguration} using the
+ // properties inherited from the first not finishing opaque activity beneath.
+ config.orientation = ORIENTATION_UNDEFINED;
+ config.screenWidthDp = config.compatScreenWidthDp = SCREEN_WIDTH_DP_UNDEFINED;
+ config.screenHeightDp = config.compatScreenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED;
+ config.smallestScreenWidthDp = config.compatSmallestScreenWidthDp =
+ SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
+ }
+
+ private void inheritConfiguration(ActivityRecord firstOpaque) {
+ mTransparentPolicyState.inheritFromOpaque(firstOpaque);
+ }
+
+ /**
+ * Encapsulate the state for the current translucent activity when the transparent policy
+ * has started.
+ */
+ static class TransparentPolicyState {
+ // Aspect ratio value to consider as undefined.
+ private static final float UNDEFINED_ASPECT_RATIO = 0f;
+
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+
+ @Configuration.Orientation
+ private int mInheritedOrientation = ORIENTATION_UNDEFINED;
+ private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
+ private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
+
+ // The app compat state for the opaque activity if any
+ private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+
+ // The CompatDisplayInsets of the opaque activity beneath the translucent one.
+ @Nullable
+ private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
+
+ @Nullable
+ private ActivityRecord mFirstOpaqueActivity;
+
+ /*
+ * WindowContainerListener responsible to make translucent activities inherit
+ * constraints from the first opaque activity beneath them. It's null for not
+ * translucent activities.
+ */
+ @Nullable
+ private WindowContainerListener mLetterboxConfigListener;
+
+ TransparentPolicyState(@NonNull ActivityRecord activityRecord) {
+ mActivityRecord = activityRecord;
+ }
+
+ private void start(@NonNull ActivityRecord firstOpaqueActivity) {
+ mFirstOpaqueActivity = firstOpaqueActivity;
+ mFirstOpaqueActivity.mTransparentPolicy
+ .mDestroyListeners.add(mActivityRecord.mTransparentPolicy);
+ inheritFromOpaque(firstOpaqueActivity);
+ final WindowContainer<?> parent = mActivityRecord.getParent();
+ mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
+ mActivityRecord, mFirstOpaqueActivity,
+ (opaqueConfig, transparentOverrideConfig) -> {
+ resetTranslucentOverrideConfig(transparentOverrideConfig);
+ final Rect parentBounds = parent.getWindowConfiguration().getBounds();
+ final Rect bounds = transparentOverrideConfig
+ .windowConfiguration.getBounds();
+ final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
+ // We cannot use letterboxBounds directly here because the position relies
+ // on letterboxing. Using letterboxBounds directly, would produce a
+ // double offset.
+ bounds.set(parentBounds.left, parentBounds.top,
+ parentBounds.left + letterboxBounds.width(),
+ parentBounds.top + letterboxBounds.height());
+ // We need to initialize appBounds to avoid NPE. The actual value will
+ // be set ahead when resolving the Configuration for the activity.
+ transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect());
+ inheritFromOpaque(mFirstOpaqueActivity);
+ return transparentOverrideConfig;
+ });
+ }
+
+ private void inheritFromOpaque(@NonNull ActivityRecord opaqueActivity) {
+ // To avoid wrong behaviour, we're not forcing a specific aspect ratio to activities
+ // which are not already providing one (e.g. permission dialogs) and presumably also
+ // not resizable.
+ if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) {
+ mInheritedMinAspectRatio = opaqueActivity.getMinAspectRatio();
+ }
+ if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) {
+ mInheritedMaxAspectRatio = opaqueActivity.getMaxAspectRatio();
+ }
+ mInheritedOrientation = opaqueActivity.getRequestedConfigurationOrientation();
+ mInheritedAppCompatState = opaqueActivity.getAppCompatState();
+ mInheritedCompatDisplayInsets = opaqueActivity.getCompatDisplayInsets();
+ }
+
+ private void reset() {
+ if (mLetterboxConfigListener != null) {
+ mLetterboxConfigListener.onRemoved();
+ }
+ mLetterboxConfigListener = null;
+ mInheritedOrientation = ORIENTATION_UNDEFINED;
+ mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
+ mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
+ mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+ mInheritedCompatDisplayInsets = null;
+ if (mFirstOpaqueActivity != null) {
+ mFirstOpaqueActivity.mTransparentPolicy
+ .mDestroyListeners.remove(mActivityRecord.mTransparentPolicy);
+ }
+ mFirstOpaqueActivity = null;
+ }
+
+ private boolean isRunning() {
+ return mLetterboxConfigListener != null;
+ }
+
+ private void clearInheritedCompatDisplayInsets() {
+ mInheritedCompatDisplayInsets = null;
+ }
+
+ /**
+ * @return The first not finishing opaque activity beneath the current translucent activity
+ * if it exists and the strategy is enabled.
+ */
+ private Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
+ if (!isRunning() || mActivityRecord.getTask() == null) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(mFirstOpaqueActivity);
+ }
+
+ /**
+ * In case of translucent activities, it consumes the {@link ActivityRecord} of the first
+ * opaque activity beneath using the given consumer and returns {@code true}.
+ */
+ private boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
+ return findOpaqueNotFinishingActivityBelow()
+ .map(activityRecord -> {
+ consumer.accept(activityRecord);
+ return true;
+ }).orElse(false);
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index cf7a6c9d65de..72109d34ec8a 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -2388,6 +2388,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
position = POSITION_TOP;
}
ownerTask.addChild(taskFragment, position);
+ EventLogTags.writeWmTfCreated(System.identityHashCode(taskFragment), ownerTask.mTaskId);
taskFragment.setWindowingMode(creationParams.getWindowingMode());
if (!creationParams.getInitialRelativeBounds().isEmpty()) {
// The surface operations for the task fragment should sync with the transition.
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index c0259135132b..cd70ed23a824 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -323,16 +323,14 @@ public final class ProfcollectForwardingService extends SystemService {
"dex2oat_trace_freq", 25);
int randomNum = ThreadLocalRandom.current().nextInt(100);
if (randomNum < traceFrequency) {
- BackgroundThread.get().getThreadHandler().post(() -> {
+ // Dex2oat could take a while before it starts. Add a short delay before start tracing.
+ BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
- // Dex2oat could take a while before it starts. Add a short delay before start
- // tracing.
- Thread.sleep(1000);
mIProfcollect.trace_once("dex2oat");
- } catch (RemoteException | InterruptedException e) {
+ } catch (RemoteException e) {
Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
}
- });
+ }, 1000);
}
}
@@ -394,15 +392,14 @@ public final class ProfcollectForwardingService extends SystemService {
if (randomNum >= traceFrequency) {
return;
}
- BackgroundThread.get().getThreadHandler().post(() -> {
+ // Wait for 1s before starting tracing.
+ BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
- // Wait for a short time before starting tracing.
- Thread.sleep(1000);
mIProfcollect.trace_once("camera");
- } catch (RemoteException | InterruptedException e) {
+ } catch (RemoteException e) {
Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
}
- });
+ }, 1000);
}
}, null);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index c7f502045ac8..8129c3d030be 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -77,6 +77,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.compat.testing.PlatformCompatChangeRule;
@@ -110,7 +111,7 @@ import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
/**
- * Test class for {@link LetterboxUiControllerTest}.
+ * Test class for {@link LetterboxUiController}.
*
* Build/Install/Run:
* atest WmTests:LetterboxUiControllerTest
@@ -521,8 +522,8 @@ public class LetterboxUiControllerTest extends WindowTestsBase {
final Rect opaqueBounds = new Rect(0, 0, 500, 300);
doReturn(opaqueBounds).when(mActivity).getBounds();
// Activity is translucent
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController).hasInheritedLetterboxBehavior();
+ spyOn(mActivity.mTransparentPolicy);
+ when(mActivity.mTransparentPolicy.isRunning()).thenReturn(true);
// Makes requested sizes different
mainWindow.mRequestedWidth = opaqueBounds.width() - 1;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 7ced9d50ab3f..c81ead9bd0d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -215,7 +215,7 @@ public class SizeCompatTests extends WindowTestsBase {
translucentActivity.setState(DESTROYED, "testing");
translucentActivity.removeImmediately();
- assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertFalse(translucentActivity.mTransparentPolicy.isRunning());
}
@Test
@@ -376,7 +376,7 @@ public class SizeCompatTests extends WindowTestsBase {
.build();
mTask.addChild(opaqueActivity);
// Transparent activity strategy not applied
- assertFalse(opaqueActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertFalse(opaqueActivity.mTransparentPolicy.isRunning());
// Launch translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
@@ -386,17 +386,17 @@ public class SizeCompatTests extends WindowTestsBase {
.build();
mTask.addChild(translucentActivity);
// Transparent strategy applied
- assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertTrue(translucentActivity.mTransparentPolicy.isRunning());
- spyOn(translucentActivity.mLetterboxUiController);
- clearInvocations(translucentActivity.mLetterboxUiController);
+ spyOn(translucentActivity.mTransparentPolicy);
+ clearInvocations(translucentActivity.mTransparentPolicy);
// We destroy the first opaque activity
opaqueActivity.setState(DESTROYED, "testing");
opaqueActivity.removeImmediately();
// Check that updateInheritedLetterbox() is invoked again
- verify(translucentActivity.mLetterboxUiController).updateInheritedLetterbox();
+ verify(translucentActivity.mTransparentPolicy).start();
}
// TODO(b/333663877): Enable test after fix
@@ -464,18 +464,17 @@ public class SizeCompatTests extends WindowTestsBase {
.build();
mTask.addChild(translucentActivity);
// Transparent strategy applied
- assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
- assertNotNull(translucentActivity.mLetterboxUiController.mFirstOpaqueActivityBeneath);
+ assertTrue(translucentActivity.mTransparentPolicy.isRunning());
- spyOn(translucentActivity.mLetterboxUiController);
- clearInvocations(translucentActivity.mLetterboxUiController);
+ spyOn(translucentActivity.mTransparentPolicy);
+ clearInvocations(translucentActivity.mTransparentPolicy);
// We destroy the first opaque activity
mActivity.removeImmediately();
- // Check that updateInheritedLetterbox() is invoked again
- verify(translucentActivity.mLetterboxUiController).updateInheritedLetterbox();
- assertNull(translucentActivity.mLetterboxUiController.mFirstOpaqueActivityBeneath);
+ // Check that updateInheritedLetterbox() is invoked again on the TransparentPolicy
+ verify(translucentActivity.mTransparentPolicy).start();
+ assertFalse(translucentActivity.mTransparentPolicy.isRunning());
}
@Test
@@ -491,7 +490,7 @@ public class SizeCompatTests extends WindowTestsBase {
.build();
mTask.addChild(opaqueActivity);
// Transparent activity strategy not applied
- assertFalse(opaqueActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertFalse(opaqueActivity.mTransparentPolicy.isRunning());
// Launch translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
@@ -501,13 +500,13 @@ public class SizeCompatTests extends WindowTestsBase {
.build();
mTask.addChild(translucentActivity);
// Transparent strategy applied
- assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertTrue(translucentActivity.mTransparentPolicy.isRunning());
- spyOn(translucentActivity.mLetterboxUiController);
- clearInvocations(translucentActivity.mLetterboxUiController);
+ spyOn(translucentActivity.mTransparentPolicy);
+ clearInvocations(translucentActivity.mTransparentPolicy);
// Check that updateInheritedLetterbox() is invoked again
- verify(translucentActivity.mLetterboxUiController, never()).updateInheritedLetterbox();
+ verify(translucentActivity.mTransparentPolicy, never()).start();
}
@Test
@@ -615,7 +614,7 @@ public class SizeCompatTests extends WindowTestsBase {
doReturn(false).when(translucentActivity).matchParentBounds();
mTask.addChild(translucentActivity);
// Check the strategy has not being applied
- assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertFalse(translucentActivity.mTransparentPolicy.isRunning());
}
@Test
@@ -661,7 +660,7 @@ public class SizeCompatTests extends WindowTestsBase {
assertFalse(mActivity.occludesParent());
mTask.addChild(translucentActivity);
// The translucent activity won't inherit letterbox behavior from a finishing activity.
- assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
+ assertFalse(translucentActivity.mTransparentPolicy.isRunning());
}
@Test
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 47f53f372d33..2a359cd56d1b 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -193,6 +193,14 @@ public final class SatelliteManager {
/**
* Bundle key to get the response from
+ * {@link #requestSessionStats(Executor, OutcomeReceiver)}.
+ * @hide
+ */
+
+ public static final String KEY_SESSION_STATS = "session_stats";
+
+ /**
+ * Bundle key to get the response from
* {@link #requestIsProvisioned(Executor, OutcomeReceiver)}.
* @hide
*/
@@ -2493,6 +2501,65 @@ public final class SatelliteManager {
}
}
+ /**
+ * Request to get the {@link SatelliteSessionStats} of the satellite service.
+ *
+ * @param executor The executor on which the callback will be called.
+ * @param callback The callback object to which the result will be delivered.
+ * If the request is successful, {@link OutcomeReceiver#onResult(Object)}
+ * will return the {@link SatelliteSessionStats} of the satellite service.
+ * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
+ * will return a {@link SatelliteException} with the {@link SatelliteResult}.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ * @hide
+ */
+ @RequiresPermission(allOf = {Manifest.permission.PACKAGE_USAGE_STATS,
+ Manifest.permission.MODIFY_PHONE_STATE})
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public void requestSessionStats(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<SatelliteSessionStats, SatelliteException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ ResultReceiver receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SATELLITE_RESULT_SUCCESS) {
+ if (resultData.containsKey(KEY_SESSION_STATS)) {
+ SatelliteSessionStats stats =
+ resultData.getParcelable(KEY_SESSION_STATS,
+ SatelliteSessionStats.class);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onResult(stats)));
+ } else {
+ loge("KEY_SESSION_STATS does not exist.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(
+ SATELLITE_RESULT_REQUEST_FAILED))));
+ }
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(resultCode))));
+ }
+ }
+ };
+ telephony.requestSatelliteSessionStats(mSubId, receiver);
+ } else {
+ loge("requestSessionStats() invalid telephony");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ } catch (RemoteException ex) {
+ loge("requestSessionStats() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ }
+
@Nullable
private static ITelephony getITelephony() {
ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/android/telephony/satellite/SatelliteSessionStats.aidl b/telephony/java/android/telephony/satellite/SatelliteSessionStats.aidl
new file mode 100644
index 000000000000..417512504969
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSessionStats.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.telephony.satellite;
+
+ parcelable SatelliteSessionStats; \ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteSessionStats.java b/telephony/java/android/telephony/satellite/SatelliteSessionStats.java
new file mode 100644
index 000000000000..aabb058691d8
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSessionStats.java
@@ -0,0 +1,224 @@
+/*
+ * 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 android.telephony.satellite;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * SatelliteSessionStats is used to represent the usage stats of the satellite service.
+ * @hide
+ */
+public class SatelliteSessionStats implements Parcelable {
+ private int mCountOfSuccessfulUserMessages;
+ private int mCountOfUnsuccessfulUserMessages;
+ private int mCountOfTimedOutUserMessagesWaitingForConnection;
+ private int mCountOfTimedOutUserMessagesWaitingForAck;
+ private int mCountOfUserMessagesInQueueToBeSent;
+
+ /**
+ * SatelliteSessionStats constructor
+ * @param builder Builder to create SatelliteSessionStats object/
+ */
+ public SatelliteSessionStats(@NonNull Builder builder) {
+ mCountOfSuccessfulUserMessages = builder.mCountOfSuccessfulUserMessages;
+ mCountOfUnsuccessfulUserMessages = builder.mCountOfUnsuccessfulUserMessages;
+ mCountOfTimedOutUserMessagesWaitingForConnection =
+ builder.mCountOfTimedOutUserMessagesWaitingForConnection;
+ mCountOfTimedOutUserMessagesWaitingForAck =
+ builder.mCountOfTimedOutUserMessagesWaitingForAck;
+ mCountOfUserMessagesInQueueToBeSent = builder.mCountOfUserMessagesInQueueToBeSent;
+ }
+
+ private SatelliteSessionStats(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mCountOfSuccessfulUserMessages);
+ out.writeInt(mCountOfUnsuccessfulUserMessages);
+ out.writeInt(mCountOfTimedOutUserMessagesWaitingForConnection);
+ out.writeInt(mCountOfTimedOutUserMessagesWaitingForAck);
+ out.writeInt(mCountOfUserMessagesInQueueToBeSent);
+ }
+
+ @NonNull
+ public static final Creator<SatelliteSessionStats> CREATOR = new Parcelable.Creator<>() {
+
+ @Override
+ public SatelliteSessionStats createFromParcel(Parcel in) {
+ return new SatelliteSessionStats(in);
+ }
+
+ @Override
+ public SatelliteSessionStats[] newArray(int size) {
+ return new SatelliteSessionStats[size];
+ }
+ };
+
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("countOfSuccessfulUserMessages:");
+ sb.append(mCountOfSuccessfulUserMessages);
+ sb.append(",");
+
+ sb.append("countOfUnsuccessfulUserMessages:");
+ sb.append(mCountOfUnsuccessfulUserMessages);
+ sb.append(",");
+
+ sb.append("countOfTimedOutUserMessagesWaitingForConnection:");
+ sb.append(mCountOfTimedOutUserMessagesWaitingForConnection);
+ sb.append(",");
+
+ sb.append("countOfTimedOutUserMessagesWaitingForAck:");
+ sb.append(mCountOfTimedOutUserMessagesWaitingForAck);
+ sb.append(",");
+
+ sb.append("countOfUserMessagesInQueueToBeSent:");
+ sb.append(mCountOfUserMessagesInQueueToBeSent);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SatelliteSessionStats that = (SatelliteSessionStats) o;
+ return mCountOfSuccessfulUserMessages == that.mCountOfSuccessfulUserMessages
+ && mCountOfUnsuccessfulUserMessages == that.mCountOfUnsuccessfulUserMessages
+ && mCountOfTimedOutUserMessagesWaitingForConnection
+ == that.mCountOfTimedOutUserMessagesWaitingForConnection
+ && mCountOfTimedOutUserMessagesWaitingForAck
+ == that.mCountOfTimedOutUserMessagesWaitingForAck
+ && mCountOfUserMessagesInQueueToBeSent
+ == that.mCountOfUserMessagesInQueueToBeSent;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCountOfSuccessfulUserMessages, mCountOfUnsuccessfulUserMessages,
+ mCountOfTimedOutUserMessagesWaitingForConnection,
+ mCountOfTimedOutUserMessagesWaitingForAck,
+ mCountOfUserMessagesInQueueToBeSent);
+ }
+
+ public int getCountOfSuccessfulUserMessages() {
+ return mCountOfSuccessfulUserMessages;
+ }
+
+ public int getCountOfUnsuccessfulUserMessages() {
+ return mCountOfUnsuccessfulUserMessages;
+ }
+
+ public int getCountOfTimedOutUserMessagesWaitingForConnection() {
+ return mCountOfTimedOutUserMessagesWaitingForConnection;
+ }
+
+ public int getCountOfTimedOutUserMessagesWaitingForAck() {
+ return mCountOfTimedOutUserMessagesWaitingForAck;
+ }
+
+ public int getCountOfUserMessagesInQueueToBeSent() {
+ return mCountOfUserMessagesInQueueToBeSent;
+ }
+
+ private void readFromParcel(Parcel in) {
+ mCountOfSuccessfulUserMessages = in.readInt();
+ mCountOfUnsuccessfulUserMessages = in.readInt();
+ mCountOfTimedOutUserMessagesWaitingForConnection = in.readInt();
+ mCountOfTimedOutUserMessagesWaitingForAck = in.readInt();
+ mCountOfUserMessagesInQueueToBeSent = in.readInt();
+ }
+
+ /**
+ * A builder class to create {@link SatelliteSessionStats} data object.
+ */
+ public static final class Builder {
+ private int mCountOfSuccessfulUserMessages;
+ private int mCountOfUnsuccessfulUserMessages;
+ private int mCountOfTimedOutUserMessagesWaitingForConnection;
+ private int mCountOfTimedOutUserMessagesWaitingForAck;
+ private int mCountOfUserMessagesInQueueToBeSent;
+
+ /**
+ * Sets countOfSuccessfulUserMessages value of {@link SatelliteSessionStats}
+ * and then returns the Builder class.
+ */
+ @NonNull
+ public Builder setCountOfSuccessfulUserMessages(int count) {
+ mCountOfSuccessfulUserMessages = count;
+ return this;
+ }
+
+ /**
+ * Sets countOfUnsuccessfulUserMessages value of {@link SatelliteSessionStats}
+ * and then returns the Builder class.
+ */
+ @NonNull
+ public Builder setCountOfUnsuccessfulUserMessages(int count) {
+ mCountOfUnsuccessfulUserMessages = count;
+ return this;
+ }
+
+ /**
+ * Sets countOfTimedOutUserMessagesWaitingForConnection value of
+ * {@link SatelliteSessionStats} and then returns the Builder class.
+ */
+ @NonNull
+ public Builder setCountOfTimedOutUserMessagesWaitingForConnection(int count) {
+ mCountOfTimedOutUserMessagesWaitingForConnection = count;
+ return this;
+ }
+
+ /**
+ * Sets countOfTimedOutUserMessagesWaitingForAck value of {@link SatelliteSessionStats}
+ * and then returns the Builder class.
+ */
+ @NonNull
+ public Builder setCountOfTimedOutUserMessagesWaitingForAck(int count) {
+ mCountOfTimedOutUserMessagesWaitingForAck = count;
+ return this;
+ }
+
+ /**
+ * Sets countOfUserMessagesInQueueToBeSent value of {@link SatelliteSessionStats}
+ * and then returns the Builder class.
+ */
+ @NonNull
+ public Builder setCountOfUserMessagesInQueueToBeSent(int count) {
+ mCountOfUserMessagesInQueueToBeSent = count;
+ return this;
+ }
+
+ /** Returns SatelliteSessionStats object. */
+ @NonNull
+ public SatelliteSessionStats build() {
+ return new SatelliteSessionStats(this);
+ }
+ }
+}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 9af73eae48a6..9b01b71fc596 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3390,4 +3390,14 @@ interface ITelephony {
* @return {@code true} if the setting is successful, {@code false} otherwise.
*/
boolean setIsSatelliteCommunicationAllowedForCurrentLocationCache(in String state);
+
+ /**
+ * Request to get the session stats of the satellite service.
+ *
+ * @param subId The subId of the subscription to get the session stats for.
+ * @param receiver Result receiver to get the error code of the request and the requested
+ * session stats of the satellite service.
+ * @hide
+ */
+ void requestSatelliteSessionStats(int subId, in ResultReceiver receiver);
}
diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
index c7da778b752b..c49b509a9db3 100644
--- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
+++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
@@ -21,6 +21,7 @@ import android.tools.device.apphelpers.StandardAppHelper
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.subject.layers.LayerTraceEntrySubject
+import android.tools.flicker.subject.layers.LayersTraceSubject
import android.tools.traces.component.ComponentNameMatcher
import android.tools.traces.component.IComponentMatcher
import android.tools.traces.surfaceflinger.Display
@@ -46,6 +47,7 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker
flicker.assertLayers {
this.visibleLayersShownMoreThanOneConsecutiveEntry(
ignoreLayers =
+ LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS +
listOf(
ComponentNameMatcher.SPLASH_SCREEN,
ComponentNameMatcher.SNAPSHOT,