diff options
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, |