diff options
65 files changed, 1854 insertions, 573 deletions
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index 6bf2474beb17..514df59f1989 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -175,10 +175,7 @@ public class UiTranslationController implements Dumpable { */ public void onActivityDestroyed() { synchronized (mLock) { - if (DEBUG) { - Log.i(TAG, - "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState)); - } + Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState)); if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) { notifyTranslationFinished(/* activityDestroyed= */ true); } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 1bc8e6d7ef65..8815ab35b671 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -135,8 +135,11 @@ public final class TransitionInfo implements Parcelable { /** This change happened underneath something else. */ public static final int FLAG_IS_OCCLUDED = 1 << 15; + /** The container is a system window, excluding wallpaper and input-method. */ + public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 16; + public static final int FLAG_FIRST_CUSTOM = 1 << 17; /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { @@ -157,6 +160,7 @@ public final class TransitionInfo implements Parcelable { FLAG_CROSS_PROFILE_WORK_THUMBNAIL, FLAG_IS_BEHIND_STARTING_WINDOW, FLAG_IS_OCCLUDED, + FLAG_IS_SYSTEM_WINDOW, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} @@ -369,6 +373,9 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_IS_OCCLUDED) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED"); } + if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) { + sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM"); } @@ -701,14 +708,37 @@ public final class TransitionInfo implements Parcelable { @Override public String toString() { - String out = "{" + mContainer + "(" + mParent + ") leash=" + mLeash - + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb=" - + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r=" - + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation - + " endFixedRotation=" + mEndFixedRotation; - if (mSnapshot != null) out += " snapshot=" + mSnapshot; - if (mLastParent != null) out += " lastParent=" + mLastParent; - return out + "}"; + final StringBuilder sb = new StringBuilder(); + sb.append('{'); sb.append(mContainer); + sb.append(" m="); sb.append(modeToString(mMode)); + sb.append(" f="); sb.append(flagsToString(mFlags)); + if (mParent != null) { + sb.append(" p="); sb.append(mParent); + } + if (mLeash != null) { + sb.append(" leash="); sb.append(mLeash); + } + sb.append(" sb="); sb.append(mStartAbsBounds); + sb.append(" eb="); sb.append(mEndAbsBounds); + if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) { + sb.append(" eo="); sb.append(mEndRelOffset); + } + if (mStartRotation != mEndRotation) { + sb.append(" r="); sb.append(mStartRotation); + sb.append("->"); sb.append(mEndRotation); + sb.append(':'); sb.append(mRotationAnimation); + } + if (mEndFixedRotation != ROTATION_UNDEFINED) { + sb.append(" endFixedRotation="); sb.append(mEndFixedRotation); + } + if (mSnapshot != null) { + sb.append(" snapshot="); sb.append(mSnapshot); + } + if (mLastParent != null) { + sb.append(" lastParent="); sb.append(mLastParent); + } + sb.append('}'); + return sb.toString(); } } diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 4f74ca72b4aa..2ae2c09680bf 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.widget.TextView; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.DisplayResolveInfo; @@ -86,6 +87,7 @@ public class ChooserListAdapter extends ResolverListAdapter { private final ChooserActivityLogger mChooserActivityLogger; private int mNumShortcutResults = 0; + private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>(); private boolean mApplySharingAppLimits; // Reserve spots for incoming direct share targets by adding placeholders @@ -239,7 +241,6 @@ public class ChooserListAdapter extends ResolverListAdapter { mListViewDataChanged = false; } - private void createPlaceHolders() { mNumShortcutResults = 0; mServiceTargets.clear(); @@ -268,12 +269,16 @@ public class ChooserListAdapter extends ResolverListAdapter { holder.bindIcon(info); if (info instanceof SelectableTargetInfo) { // direct share targets should append the application name for a better readout - DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); + SelectableTargetInfo sti = (SelectableTargetInfo) info; + DisplayResolveInfo rInfo = sti.getDisplayResolveInfo(); CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; CharSequence extendedInfo = info.getExtendedInfo(); String contentDescription = String.join(" ", info.getDisplayLabel(), extendedInfo != null ? extendedInfo : "", appName); holder.updateContentDescription(contentDescription); + if (!sti.hasDisplayIcon()) { + loadDirectShareIcon(sti); + } } else if (info instanceof DisplayResolveInfo) { DisplayResolveInfo dri = (DisplayResolveInfo) info; if (!dri.hasDisplayIcon()) { @@ -318,6 +323,20 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + private void loadDirectShareIcon(SelectableTargetInfo info) { + LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); + if (task == null) { + task = createLoadDirectShareIconTask(info); + mIconLoaders.put(info, task); + task.loadIcon(); + } + } + + @VisibleForTesting + protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) { + return new LoadDirectShareIconTask(info); + } + void updateAlphabeticalList() { new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { @Override @@ -332,7 +351,7 @@ public class ChooserListAdapter extends ResolverListAdapter { Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); for (DisplayResolveInfo info : allTargets) { String resolvedTarget = info.getResolvedComponentName().getPackageName() - + '#' + info.getDisplayLabel(); + + '#' + info.getDisplayLabel(); DisplayResolveInfo multiDri = consolidated.get(resolvedTarget); if (multiDri == null) { consolidated.put(resolvedTarget, info); @@ -341,7 +360,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } else { // create consolidated target from the single DisplayResolveInfo MultiDisplayResolveInfo multiDisplayResolveInfo = - new MultiDisplayResolveInfo(resolvedTarget, multiDri); + new MultiDisplayResolveInfo(resolvedTarget, multiDri); multiDisplayResolveInfo.addTarget(info); consolidated.put(resolvedTarget, multiDisplayResolveInfo); } @@ -731,7 +750,8 @@ public class ChooserListAdapter extends ResolverListAdapter { * Necessary methods to communicate between {@link ChooserListAdapter} * and {@link ChooserActivity}. */ - interface ChooserListCommunicator extends ResolverListCommunicator { + @VisibleForTesting + public interface ChooserListCommunicator extends ResolverListCommunicator { int getMaxRankedTargets(); @@ -739,4 +759,35 @@ public class ChooserListAdapter extends ResolverListAdapter { boolean isSendAction(Intent targetIntent); } + + /** + * Loads direct share targets icons. + */ + @VisibleForTesting + public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> { + private final SelectableTargetInfo mTargetInfo; + + private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) { + mTargetInfo = targetInfo; + } + + @Override + protected Boolean doInBackground(Void... voids) { + return mTargetInfo.loadIcon(); + } + + @Override + protected void onPostExecute(Boolean isLoaded) { + if (isLoaded) { + notifyDataSetChanged(); + } + } + + /** + * An alias for execute to use with unit tests. + */ + public void loadIcon() { + execute(); + } + } } diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index f6075b008f72..4a1f7eb06c40 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -870,7 +870,12 @@ public class ResolverListAdapter extends BaseAdapter { void onHandlePackagesChanged(ResolverListAdapter listAdapter); } - static class ViewHolder { + /** + * A view holder keeps a reference to a list view and provides functionality for managing its + * state. + */ + @VisibleForTesting + public static class ViewHolder { public View itemView; public Drawable defaultItemViewBackground; @@ -878,7 +883,8 @@ public class ResolverListAdapter extends BaseAdapter { public TextView text2; public ImageView icon; - ViewHolder(View view) { + @VisibleForTesting + public ViewHolder(View view) { itemView = view; defaultItemViewBackground = view.getBackground(); text = (TextView) view.findViewById(com.android.internal.R.id.text1); diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java index 4b9b7cb98dac..d7f3a76c61e0 100644 --- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java +++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java @@ -37,6 +37,7 @@ import android.service.chooser.ChooserTarget; import android.text.SpannableStringBuilder; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.ChooserActivity; import com.android.internal.app.ResolverActivity; import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; @@ -59,8 +60,11 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { private final String mDisplayLabel; private final PackageManager mPm; private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator; + @GuardedBy("this") + private ShortcutInfo mShortcutInfo; private Drawable mBadgeIcon = null; private CharSequence mBadgeContentDescription; + @GuardedBy("this") private Drawable mDisplayIcon; private final Intent mFillInIntent; private final int mFillInFlags; @@ -78,6 +82,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { mModifiedScore = modifiedScore; mPm = mContext.getPackageManager(); mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator; + mShortcutInfo = shortcutInfo; mIsPinned = shortcutInfo != null && shortcutInfo.isPinned(); if (sourceInfo != null) { final ResolveInfo ri = sourceInfo.getResolveInfo(); @@ -92,8 +97,6 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { } } } - // TODO(b/121287224): do this in the background thread, and only for selected targets - mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo); if (sourceInfo != null) { mBackupResolveInfo = null; @@ -118,7 +121,10 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { mChooserTarget = other.mChooserTarget; mBadgeIcon = other.mBadgeIcon; mBadgeContentDescription = other.mBadgeContentDescription; - mDisplayIcon = other.mDisplayIcon; + synchronized (other) { + mShortcutInfo = other.mShortcutInfo; + mDisplayIcon = other.mDisplayIcon; + } mFillInIntent = fillInIntent; mFillInFlags = flags; mModifiedScore = other.mModifiedScore; @@ -141,6 +147,27 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { return mSourceInfo; } + /** + * Load display icon, if needed. + */ + public boolean loadIcon() { + ShortcutInfo shortcutInfo; + Drawable icon; + synchronized (this) { + shortcutInfo = mShortcutInfo; + icon = mDisplayIcon; + } + boolean shouldLoadIcon = icon == null && shortcutInfo != null; + if (shouldLoadIcon) { + icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo); + synchronized (this) { + mDisplayIcon = icon; + mShortcutInfo = null; + } + } + return shouldLoadIcon; + } + private Drawable getChooserTargetIconDrawable(ChooserTarget target, @Nullable ShortcutInfo shortcutInfo) { Drawable directShareIcon = null; @@ -271,10 +298,17 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { } @Override - public Drawable getDisplayIcon(Context context) { + public synchronized Drawable getDisplayIcon(Context context) { return mDisplayIcon; } + /** + * @return true if display icon is available + */ + public synchronized boolean hasDisplayIcon() { + return mDisplayIcon != null; + } + public ChooserTarget getChooserTarget() { return mChooserTarget; } diff --git a/core/res/Android.bp b/core/res/Android.bp index c42517d8a873..179eff809c96 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -130,6 +130,10 @@ android_app { // Allow overlay to add resource "--auto-add-overlay", + + // Framework resources benefit tremendously from enabling sparse encoding, saving tens + // of MBs in size and RAM use. + "--enable-sparse-encoding", ], resource_zips: [ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5ae133bbe6e6..964fe2d57b0d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1035,25 +1035,38 @@ android:priority="900" /> <!-- Allows an application to read from external storage. - <p>Any app that declares the {@link #WRITE_EXTERNAL_STORAGE} permission is implicitly - granted this permission.</p> + <p class="note"><strong>Note: </strong>Starting in API level 33, this permission has no + effect. If your app accesses other apps' media files, request one or more of these permissions + instead: <a href="#READ_MEDIA_IMAGES"><code>READ_MEDIA_IMAGES</code></a>, + <a href="#READ_MEDIA_VIDEO"><code>READ_MEDIA_VIDEO</code></a>, + <a href="#READ_MEDIA_AUDIO"><code>READ_MEDIA_AUDIO</code></a>. Learn more about the + <a href="{@docRoot}training/data-storage/shared/media#storage-permission">storage + permissions</a> that are associated with media files.</p> + <p>This permission is enforced starting in API level 19. Before API level 19, this permission is not enforced and all apps still have access to read from external storage. You can test your app with the permission enforced by enabling <em>Protect USB - storage</em> under Developer options in the Settings app on a device running Android 4.1 or - higher.</p> + storage</em> under <b>Developer options</b> in the Settings app on a device running Android + 4.1 or higher.</p> <p>Also starting in API level 19, this permission is <em>not</em> required to - read/write files in your application-specific directories returned by + read or write files in your application-specific directories returned by {@link android.content.Context#getExternalFilesDir} and - {@link android.content.Context#getExternalCacheDir}. - <p class="note"><strong>Note:</strong> If <em>both</em> your <a + {@link android.content.Context#getExternalCacheDir}.</p> + <p>Starting in API level 29, apps don't need to request this permission to access files in + their app-specific directory on external storage, or their own files in the + <a href="{@docRoot}reference/android/provider/MediaStore"><code>MediaStore</code></a>. Apps + shouldn't request this permission unless they need to access other apps' files in the + <code>MediaStore</code>. Read more about these changes in the + <a href="{@docRoot}training/data-storage#scoped-storage">scoped storage</a> section of the + developer documentation.</p> + <p>If <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a> and <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> values are set to 3 or lower, the system implicitly grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code - targetSdkVersion}</a> is 4 or higher. + targetSdkVersion}</a> is 4 or higher.</p> <p> This is a soft restricted permission which cannot be held by an app it its full form until the installer on record allowlists the permission. diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml index ded23feaca8f..38a71f0e17f6 100644 --- a/core/res/res/layout/miniresolver.xml +++ b/core/res/res/layout/miniresolver.xml @@ -65,8 +65,7 @@ android:paddingTop="32dp" android:paddingBottom="@dimen/resolver_button_bar_spacing" android:orientation="vertical" - android:background="?attr/colorBackground" - android:layout_ignoreOffset="true"> + android:background="?attr/colorBackground"> <RelativeLayout style="?attr/buttonBarStyle" android:layout_width="match_parent" diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt new file mode 100644 index 000000000000..8218b9869b5d --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 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.internal.app + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.Bundle +import android.service.chooser.ChooserTarget +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.R +import com.android.internal.app.ChooserListAdapter.LoadDirectShareIconTask +import com.android.internal.app.chooser.SelectableTargetInfo +import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator +import com.android.internal.app.chooser.TargetInfo +import com.android.server.testutils.any +import com.android.server.testutils.mock +import com.android.server.testutils.whenever +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ChooserListAdapterTest { + private val packageManager = mock<PackageManager> { + whenever(resolveActivity(any(), anyInt())).thenReturn(mock()) + } + private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val resolverListController = mock<ResolverListController>() + private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> { + whenever(maxRankedTargets).thenReturn(0) + } + private val selectableTargetInfoCommunicator = + mock<SelectableTargetInfoCommunicator> { + whenever(targetIntent).thenReturn(mock()) + } + private val chooserActivityLogger = mock<ChooserActivityLogger>() + + private fun createChooserListAdapter( + taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask + ) = + ChooserListAdapterOverride( + context, + emptyList(), + emptyArray(), + emptyList(), + false, + resolverListController, + chooserListCommunicator, + selectableTargetInfoCommunicator, + packageManager, + chooserActivityLogger, + taskProvider + ) + + @Test + fun testDirectShareTargetLoadingIconIsStarted() { + val view = createView() + val viewHolder = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolder + val targetInfo = createSelectableTargetInfo() + val iconTask = mock<LoadDirectShareIconTask>() + val testSubject = createChooserListAdapter { iconTask } + testSubject.testViewBind(view, targetInfo, 0) + + verify(iconTask, times(1)).loadIcon() + } + + @Test + fun testOnlyOneTaskPerTarget() { + val view = createView() + val viewHolderOne = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderOne + val targetInfo = createSelectableTargetInfo() + val iconTaskOne = mock<LoadDirectShareIconTask>() + val testTaskProvider = mock<() -> LoadDirectShareIconTask> { + whenever(invoke()).thenReturn(iconTaskOne) + } + val testSubject = createChooserListAdapter { testTaskProvider.invoke() } + testSubject.testViewBind(view, targetInfo, 0) + + val viewHolderTwo = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderTwo + whenever(testTaskProvider()).thenReturn(mock()) + + testSubject.testViewBind(view, targetInfo, 0) + + verify(iconTaskOne, times(1)).loadIcon() + verify(testTaskProvider, times(1)).invoke() + } + + private fun createSelectableTargetInfo(): SelectableTargetInfo = + SelectableTargetInfo( + context, + null, + createChooserTarget(), + 1f, + selectableTargetInfoCommunicator, + null + ) + + private fun createChooserTarget(): ChooserTarget = + ChooserTarget( + "Title", + null, + 1f, + ComponentName("package", "package.Class"), + Bundle() + ) + + private fun createView(): View { + val view = FrameLayout(context) + TextView(context).apply { + id = R.id.text1 + view.addView(this) + } + TextView(context).apply { + id = R.id.text2 + view.addView(this) + } + ImageView(context).apply { + id = R.id.icon + view.addView(this) + } + return view + } +} + +private class ChooserListAdapterOverride( + context: Context?, + payloadIntents: List<Intent>?, + initialIntents: Array<out Intent>?, + rList: List<ResolveInfo>?, + filterLastUsed: Boolean, + resolverListController: ResolverListController?, + chooserListCommunicator: ChooserListCommunicator?, + selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?, + packageManager: PackageManager?, + chooserActivityLogger: ChooserActivityLogger?, + private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask +) : ChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + resolverListController, + chooserListCommunicator, + selectableTargetInfoCommunicator, + packageManager, + chooserActivityLogger +) { + override fun createLoadDirectShareIconTask( + info: SelectableTargetInfo? + ): LoadDirectShareIconTask = + taskProvider.invoke(info) + + fun testViewBind(view: View?, info: TargetInfo?, position: Int) { + onBindView(view, info, position) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 30124a5363a4..616d447247de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -745,6 +745,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Directly move PiP to its final destination bounds without animation. mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds); } + + // if the pip window size is beyond allowed bounds user resize to normal bounds + if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x + || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x + || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y + || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) { + mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction); + } + } else { updateDisplayLayout.run(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 89d85e4b292d..41ff0b35a035 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -96,6 +96,7 @@ public class PipResizeGestureHandler { private final Rect mDisplayBounds = new Rect(); private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateResizeBoundsCallback; private int mDelta; private float mTouchSlop; @@ -137,6 +138,13 @@ public class PipResizeGestureHandler { mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; } public void init() { @@ -508,15 +516,50 @@ public class PipResizeGestureHandler { } } + private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { + final int leftEdge = bounds.left; + + + final int fromLeft = Math.abs(leftEdge - movementBounds.left); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + + // The PIP will be snapped to either the right or left edge, so calculate which one + // is closest to the current position. + final int newLeft = fromLeft < fromRight + ? movementBounds.left : movementBounds.right; + + bounds.offsetTo(newLeft, mLastResizeBounds.top); + } + + /** + * Resizes the pip window and updates user-resized bounds. + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + Rect finalBounds = new Rect(bounds); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); + + // snap the target bounds to the either left or right edge, by choosing the closer one + snapToMovementBoundsEdge(finalBounds, movementBounds); + + // apply the requested snap fraction onto the target bounds + mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); + + // resize from current bounds to target bounds without animation + mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); + // set the flag that pip has been resized + mPipBoundsState.setHasUserResizedPip(true); + + // finish the resize operation and update the state of the bounds + mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); + } + private void finishResize() { if (!mLastResizeBounds.isEmpty()) { - final Consumer<Rect> callback = (rect) -> { - mUserResizeBounds.set(mLastResizeBounds); - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - resetState(); - }; - // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped // position correctly. Drag-resize does not need to move, so just finalize resize. if (mOngoingPinchToResize) { @@ -526,24 +569,23 @@ public class PipResizeGestureHandler { || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); } - final int leftEdge = mLastResizeBounds.left; - final Rect movementBounds = - mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds); - final int fromLeft = Math.abs(leftEdge - movementBounds.left); - final int fromRight = Math.abs(movementBounds.right - leftEdge); - // The PIP will be snapped to either the right or left edge, so calculate which one - // is closest to the current position. - final int newLeft = fromLeft < fromRight - ? movementBounds.left : movementBounds.right; - mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, - PINCH_RESIZE_SNAP_DURATION, mAngle, callback); + PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback); } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback); + PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, + mUpdateResizeBoundsCallback); } final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; mPipDismissTargetHandler diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 1f3f31e025a0..975d4bba276e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -825,6 +825,16 @@ public class PipTouchHandler { } /** + * Resizes the pip window and updates user resized bounds + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + mPipResizeGestureHandler.userResizeTo(bounds, snapFraction); + } + + /** * Gesture controlling normal movement of the PIP. */ private class DefaultPipTouchGesture extends PipTouchGesture { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index d1bc7384d78c..db1f19aa87b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -322,6 +322,11 @@ public class Transitions implements RemoteCallable<Transitions> { boolean isOpening = isOpeningType(info.getType()); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) { + // Currently system windows are controlled by WindowState, so don't change their + // surfaces. Otherwise their window tokens could be hidden unexpectedly. + continue; + } final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index dba037db72eb..3bd2ae76ebfd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip.phone; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -55,6 +56,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PipResizeGestureHandlerTest extends ShellTestCase { + private static final float DEFAULT_SNAP_FRACTION = 2.0f; private static final int STEP_SIZE = 40; private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2]; @@ -196,6 +198,51 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { < mPipBoundsState.getBounds().width()); } + @Test + public void testUserResizeTo() { + // resizing the bounds to normal bounds at first + mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(), + DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleFinishResizePip(any(), any()); + + // bounds with max size + final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + + // resizing the bounds to maximum bounds the second time + mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(maxBounds); + + // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes + // the total number of invocations 2 for each method + verify(mPipTaskOrganizer, times(2)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(2)) + .scheduleFinishResizePip(any(), any()); + } + + private void assertPipBoundsUserResizedTo(Rect bounds) { + // check user-resized bounds + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width()); + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height()); + + // check if the bounds are the same + assertEquals(mPipBoundsState.getBounds().width(), bounds.width()); + assertEquals(mPipBoundsState.getBounds().height(), bounds.height()); + + // a flag should be set to indicate pip has been resized by the user + assertTrue(mPipBoundsState.hasUserResizedPip()); + } + private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) { final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2]; for (int i = 0; i < 2; i++) { diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt new file mode 100644 index 000000000000..1d808ba7ee16 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 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.internal.systemui.lint + +import com.android.SdkConstants.CLASS_CONTEXT +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifierListOwner +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.getParentOfType + +/** + * Warns if {@code Context.bindService}, {@code Context.bindServiceAsUser}, or {@code + * Context.unbindService} is not called on a {@code WorkerThread} + */ +@Suppress("UnstableApiUsage") +class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return listOf("bindService", "bindServiceAsUser", "unbindService") + } + + private fun hasWorkerThreadAnnotation( + context: JavaContext, + annotated: PsiModifierListOwner? + ): Boolean { + return context.evaluator.getAnnotations(annotated, inHierarchy = true).any { uAnnotation -> + uAnnotation.qualifiedName == "androidx.annotation.WorkerThread" + } + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { + if ( + !hasWorkerThreadAnnotation(context, node.getParentOfType(UMethod::class.java)) && + !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java)) + ) { + context.report( + ISSUE, + method, + context.getLocation(node), + "This method should be annotated with `@WorkerThread` because " + + "it calls ${method.name}", + ) + } + } + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "BindServiceOnMainThread", + briefDescription = "Service bound or unbound on main thread", + explanation = + """ + Binding and unbinding services are synchronous calls to `ActivityManager`. \ + They usually take multiple milliseconds to complete. If called on the main \ + thread, it will likely cause missed frames. To fix it, use a `@Background \ + Executor` and annotate the calling method with `@WorkerThread`. + """, + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + BindServiceOnMainThreadDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt index 8d48f0957be4..112992913661 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -48,14 +49,14 @@ class BroadcastSentViaContextDetector : Detector(), SourceCodeScanner { return } - val evaulator = context.evaluator - if (evaulator.isMemberInSubClassOf(method, "android.content.Context")) { + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( ISSUE, method, context.getNameLocation(node), - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead." + "`Context.${method.name}()` should be replaced with " + + "`BroadcastSender.${method.name}()`" ) } } @@ -65,14 +66,14 @@ class BroadcastSentViaContextDetector : Detector(), SourceCodeScanner { val ISSUE: Issue = Issue.create( id = "BroadcastSentViaContext", - briefDescription = "Broadcast sent via Context instead of BroadcastSender.", - explanation = - "Broadcast was sent via " + - "Context.sendBroadcast/Context.sendBroadcastAsUser. Please use " + - "BroadcastSender.sendBroadcast/BroadcastSender.sendBroadcastAsUser " + - "which will schedule dispatch of broadcasts on background thread. " + - "Sending broadcasts on main thread causes jank due to synchronous " + - "Binder calls.", + briefDescription = "Broadcast sent via `Context` instead of `BroadcastSender`", + // lint trims indents and converts \ to line continuations + explanation = """ + Broadcasts sent via `Context.sendBroadcast()` or \ + `Context.sendBroadcastAsUser()` will block the main thread and may cause \ + missed frames. Instead, use `BroadcastSender.sendBroadcast()` or \ + `BroadcastSender.sendBroadcastAsUser()` which will schedule and dispatch \ + broadcasts on a background worker thread.""", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt deleted file mode 100644 index a629eeeb0102..000000000000 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2022 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.internal.systemui.lint - -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Implementation -import com.android.tools.lint.detector.api.Issue -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Scope -import com.android.tools.lint.detector.api.Severity -import com.android.tools.lint.detector.api.SourceCodeScanner -import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UCallExpression - -@Suppress("UnstableApiUsage") -class GetMainLooperViaContextDetector : Detector(), SourceCodeScanner { - - override fun getApplicableMethodNames(): List<String> { - return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor") - } - - override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { - context.report( - ISSUE, - method, - context.getNameLocation(node), - "Please inject a @Main Executor instead." - ) - } - } - - companion object { - @JvmField - val ISSUE: Issue = - Issue.create( - id = "GetMainLooperViaContextDetector", - briefDescription = "Please use idiomatic SystemUI executors, injecting " + - "them via Dagger.", - explanation = "Injecting the @Main Executor is preferred in order to make" + - "dependencies explicit and increase testability. It's much " + - "easier to pass a FakeExecutor on your test ctor than to " + - "deal with loopers in unit tests.", - category = Category.LINT, - priority = 8, - severity = Severity.WARNING, - implementation = Implementation(GetMainLooperViaContextDetector::class.java, - Scope.JAVA_FILE_SCOPE) - ) - } -} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt index 925fae0ebfb4..bab76ab4bce2 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -28,20 +29,19 @@ import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression @Suppress("UnstableApiUsage") -class BindServiceViaContextDetector : Detector(), SourceCodeScanner { +class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List<String> { - return listOf("bindService", "bindServiceAsUser", "unbindService") + return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor") } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Binding or unbinding services are synchronous calls, please make " + - "sure you're on a @Background Executor." + ISSUE, + method, + context.getNameLocation(node), + "Replace with injected `@Main Executor`." ) } } @@ -50,18 +50,20 @@ class BindServiceViaContextDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "BindServiceViaContextDetector", - briefDescription = "Service bound/unbound via Context, please make sure " + - "you're on a background thread.", + id = "NonInjectedMainThread", + briefDescription = "Main thread usage without dependency injection", explanation = - "Binding or unbinding services are synchronous calls to ActivityManager, " + - "they usually take multiple milliseconds to complete and will make" + - "the caller drop frames. Make sure you're on a @Background Executor.", - category = Category.PERFORMANCE, + """ + Main thread should be injected using the `@Main Executor` instead \ + of using the accessors in `Context`. This is to make the \ + dependencies explicit and increase testability. It's much easier \ + to pass a `FakeExecutor` on test constructors than it is to deal \ + with loopers in unit tests.""", + category = Category.LINT, priority = 8, severity = Severity.WARNING, implementation = - Implementation(BindServiceViaContextDetector::class.java, Scope.JAVA_FILE_SCOPE) + Implementation(NonInjectedMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE) ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt index 4eb7c7dd0d7e..b62290025437 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -32,7 +33,7 @@ import org.jetbrains.uast.UCallExpression class NonInjectedServiceDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List<String> { - return listOf("getSystemService") + return listOf("getSystemService", "get") } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { @@ -40,14 +41,25 @@ class NonInjectedServiceDetector : Detector(), SourceCodeScanner { if ( !evaluator.isStatic(method) && method.name == "getSystemService" && - method.containingClass?.qualifiedName == "android.content.Context" + method.containingClass?.qualifiedName == CLASS_CONTEXT ) { context.report( ISSUE, method, context.getNameLocation(node), - "Use @Inject to get the handle to a system-level services instead of using " + - "Context.getSystemService()" + "Use `@Inject` to get system-level service handles instead of " + + "`Context.getSystemService()`" + ) + } else if ( + evaluator.isStatic(method) && + method.name == "get" && + method.containingClass?.qualifiedName == "android.accounts.AccountManager" + ) { + context.report( + ISSUE, + method, + context.getNameLocation(node), + "Replace `AccountManager.get()` with an injected instance of `AccountManager`" ) } } @@ -57,14 +69,14 @@ class NonInjectedServiceDetector : Detector(), SourceCodeScanner { val ISSUE: Issue = Issue.create( id = "NonInjectedService", - briefDescription = - "System-level services should be retrieved using " + - "@Inject instead of Context.getSystemService().", + briefDescription = "System service not injected", explanation = - "Context.getSystemService() should be avoided because it makes testing " + - "difficult. Instead, use an injected service. For example, " + - "instead of calling Context.getSystemService(UserManager.class), " + - "use @Inject and add UserManager to the constructor", + """ + `Context.getSystemService()` should be avoided because it makes testing \ + difficult. Instead, use an injected service. For example, instead of calling \ + `Context.getSystemService(UserManager.class)` in a class, annotate the class' \ + constructor with `@Inject` and add `UserManager` to the parameters. + """, category = Category.CORRECTNESS, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt index eb71d32b2d8b..4ba3afc7f7e2 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -35,12 +36,12 @@ class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( ISSUE, method, context.getNameLocation(node), - "BroadcastReceivers should be registered via BroadcastDispatcher." + "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`" ) } } @@ -49,14 +50,16 @@ class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "RegisterReceiverViaContextDetector", - briefDescription = "Broadcast registrations via Context are blocking " + - "calls. Please use BroadcastDispatcher.", - explanation = - "Context#registerReceiver is a blocking call to the system server, " + - "making it very likely that you'll drop a frame. Please use " + - "BroadcastDispatcher instead (or move this call to a " + - "@Background Executor.)", + id = "RegisterReceiverViaContext", + briefDescription = "Blocking broadcast registration", + // lint trims indents and converts \ to line continuations + explanation = """ + `Context.registerReceiver()` is a blocking call to the system server, \ + making it very likely that you'll drop a frame. Please use \ + `BroadcastDispatcher` instead, which registers the receiver on a \ + background thread. `BroadcastDispatcher` also improves our visibility \ + into ANRs.""", + moreInfo = "go/identifying-broadcast-threads", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt index b00661575c14..7be21a512f89 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt @@ -49,8 +49,7 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { ISSUE_SLOW_USER_ID_QUERY, method, context.getNameLocation(node), - "ActivityManager.getCurrentUser() is slow. " + - "Use UserTracker.getUserId() instead." + "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`" ) } if ( @@ -62,7 +61,7 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { ISSUE_SLOW_USER_INFO_QUERY, method, context.getNameLocation(node), - "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead." + "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`" ) } } @@ -72,11 +71,13 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { val ISSUE_SLOW_USER_ID_QUERY: Issue = Issue.create( id = "SlowUserIdQuery", - briefDescription = "User ID queried using ActivityManager instead of UserTracker.", + briefDescription = "User ID queried using ActivityManager", explanation = - "ActivityManager.getCurrentUser() makes a binder call and is slow. " + - "Instead, inject a UserTracker and call UserTracker.getUserId(). For " + - "more info, see: http://go/multi-user-in-systemui-slides", + """ + `ActivityManager.getCurrentUser()` uses a blocking binder call and is slow. \ + Instead, inject a `UserTracker` and call `UserTracker.getUserId()`. + """, + moreInfo = "http://go/multi-user-in-systemui-slides", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, @@ -88,11 +89,13 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { val ISSUE_SLOW_USER_INFO_QUERY: Issue = Issue.create( id = "SlowUserInfoQuery", - briefDescription = "User info queried using UserManager instead of UserTracker.", + briefDescription = "User info queried using UserManager", explanation = - "UserManager.getUserInfo() makes a binder call and is slow. " + - "Instead, inject a UserTracker and call UserTracker.getUserInfo(). For " + - "more info, see: http://go/multi-user-in-systemui-slides", + """ + `UserManager.getUserInfo()` uses a blocking binder call and is slow. \ + Instead, inject a `UserTracker` and call `UserTracker.getUserInfo()`. + """, + moreInfo = "http://go/multi-user-in-systemui-slides", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index a584894fed71..4eeeb850292a 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -47,7 +47,7 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { ISSUE, referenced, context.getNameLocation(referenced), - "Usage of Config.HARDWARE is highly encouraged." + "Replace software bitmap with `Config.HARDWARE`" ) } } @@ -56,12 +56,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "SoftwareBitmapDetector", - briefDescription = "Software bitmap detected. Please use Config.HARDWARE instead.", - explanation = - "Software bitmaps occupy twice as much memory, when compared to Config.HARDWARE. " + - "In case you need to manipulate the pixels, please consider to either use" + - "a shader (encouraged), or a short lived software bitmap.", + id = "SoftwareBitmap", + briefDescription = "Software bitmap", + explanation = """ + Software bitmaps occupy twice as much memory as `Config.HARDWARE` bitmaps \ + do. However, hardware bitmaps are read-only. If you need to manipulate the \ + pixels, use a shader (preferably) or a short lived software bitmap.""", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 312810ba4633..cf7c1b5e44a2 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -28,11 +28,11 @@ class SystemUIIssueRegistry : IssueRegistry() { override val issues: List<Issue> get() = listOf( - BindServiceViaContextDetector.ISSUE, + BindServiceOnMainThreadDetector.ISSUE, BroadcastSentViaContextDetector.ISSUE, SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY, SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY, - GetMainLooperViaContextDetector.ISSUE, + NonInjectedMainThreadDetector.ISSUE, RegisterReceiverViaContextDetector.ISSUE, SoftwareBitmapDetector.ISSUE, NonInjectedServiceDetector.ISSUE, diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt index 26bd8d0a6ff4..486af9dd5d98 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -16,16 +16,21 @@ package com.android.internal.systemui.lint +import com.android.annotations.NonNull import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import org.intellij.lang.annotations.Language + +@Suppress("UnstableApiUsage") +@NonNull +private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented() /* * This file contains stubs of framework APIs and System UI classes for testing purposes only. The * stubs are not used in the lint detectors themselves. */ -@Suppress("UnstableApiUsage") internal val androidStubs = arrayOf( - java( + indentedJava( """ package android.app; @@ -34,7 +39,16 @@ public class ActivityManager { } """ ), - java( + indentedJava( + """ +package android.accounts; + +public class AccountManager { + public static AccountManager get(Context context) { return null; } +} +""" + ), + indentedJava( """ package android.os; import android.content.pm.UserInfo; @@ -45,39 +59,39 @@ public class UserManager { } """ ), - java(""" + indentedJava(""" package android.annotation; public @interface UserIdInt {} """), - java(""" + indentedJava(""" package android.content.pm; public class UserInfo {} """), - java(""" + indentedJava(""" package android.os; public class Looper {} """), - java(""" + indentedJava(""" package android.os; public class Handler {} """), - java(""" + indentedJava(""" package android.content; public class ServiceConnection {} """), - java(""" + indentedJava(""" package android.os; public enum UserHandle { ALL } """), - java( + indentedJava( """ package android.content; import android.os.UserHandle; @@ -108,7 +122,7 @@ public class Context { } """ ), - java( + indentedJava( """ package android.app; import android.content.Context; @@ -116,7 +130,7 @@ import android.content.Context; public class Activity extends Context {} """ ), - java( + indentedJava( """ package android.graphics; @@ -132,17 +146,17 @@ public class Bitmap { } """ ), - java(""" + indentedJava(""" package android.content; public class BroadcastReceiver {} """), - java(""" + indentedJava(""" package android.content; public class IntentFilter {} """), - java( + indentedJava( """ package com.android.systemui.settings; import android.content.pm.UserInfo; @@ -153,4 +167,23 @@ public interface UserTracker { } """ ), + indentedJava( + """ +package androidx.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +@Retention(SOURCE) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) +public @interface WorkerThread { +} +""" + ), ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt new file mode 100644 index 000000000000..6ae8fd3f25a1 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 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.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class BindServiceOnMainThreadDetectorTest : LintDetectorTest() { + + override fun getDetector(): Detector = BindServiceOnMainThreadDetector() + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE) + + @Test + fun testBindService() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void bind(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.bindService(intent, null, 0); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls bindService [BindServiceOnMainThread] + context.bindService(intent, null, 0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testBindServiceAsUser() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.UserHandle; + + public class TestClass { + public void bind(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: This method should be annotated with @WorkerThread because it calls bindServiceAsUser [BindServiceOnMainThread] + context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testUnbindService() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + + public class TestClass { + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls unbindService [BindServiceOnMainThread] + context.unbindService(connection); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testWorkerMethod() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + import androidx.annotation.WorkerThread; + + public class TestClass { + @WorkerThread + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + + public class ChildTestClass extends TestClass { + @Override + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testWorkerClass() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + import androidx.annotation.WorkerThread; + + @WorkerThread + public class TestClass { + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + + public class ChildTestClass extends TestClass { + @Override + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + + public void bind(Context context, ServiceConnection connection) { + context.bind(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt deleted file mode 100644 index 564afcb773fd..000000000000 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2022 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.internal.systemui.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue -import org.junit.Test - -@Suppress("UnstableApiUsage") -class BindServiceViaContextDetectorTest : LintDetectorTest() { - - override fun getDetector(): Detector = BindServiceViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE) - - private val explanation = "Binding or unbinding services are synchronous calls" - - @Test - fun testBindService() { - lint() - .files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - - public class TestClass1 { - public void bind(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.bindService(intent, null, 0); - } - } - """ - ) - .indented(), - *stubs - ) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testBindServiceAsUser() { - lint() - .files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.os.UserHandle; - - public class TestClass1 { - public void bind(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); - } - } - """ - ) - .indented(), - *stubs - ) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testUnbindService() { - lint() - .files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.content.ServiceConnection; - - public class TestClass1 { - public void unbind(Context context, ServiceConnection connection) { - context.unbindService(connection); - } - } - """ - ) - .indented(), - *stubs - ) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - private val stubs = androidStubs -} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt index 06aee8e35898..7d422807ae08 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt @@ -41,7 +41,7 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { package test.pkg; import android.content.Context; - public class TestClass1 { + public class TestClass { public void send(Context context) { Intent intent = new Intent(Intent.ACTION_VIEW); context.sendBroadcast(intent); @@ -54,10 +54,13 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { ) .issues(BroadcastSentViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead." + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Context.sendBroadcast() should be replaced with BroadcastSender.sendBroadcast() [BroadcastSentViaContext] + context.sendBroadcast(intent); + ~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -71,7 +74,7 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void send(Context context) { Intent intent = new Intent(Intent.ACTION_VIEW); context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); @@ -84,10 +87,13 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { ) .issues(BroadcastSentViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead." + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext] + context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + ~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -101,7 +107,7 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { import android.app.Activity; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void send(Activity activity) { Intent intent = new Intent(Intent.ACTION_VIEW); activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); @@ -115,11 +121,41 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { ) .issues(BroadcastSentViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead." + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext] + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + ~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testSendBroadcastInBroadcastSender() { + lint() + .files( + TestFiles.java( + """ + package com.android.systemui.broadcast; + import android.app.Activity; + import android.os.UserHandle; + + public class BroadcastSender { + public void send(Activity activity) { + Intent intent = new Intent(Intent.ACTION_VIEW); + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + } + + } + """ + ) + .indented(), + *stubs ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expectClean() } @Test @@ -131,7 +167,7 @@ class BroadcastSentViaContextDetectorTest : LintDetectorTest() { package test.pkg; import android.content.Context; - public class TestClass1 { + public class TestClass { public void sendBroadcast() { Intent intent = new Intent(Intent.ACTION_VIEW); context.startActivity(intent); diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt index c55f3995f102..c468af8d09e0 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt @@ -24,14 +24,12 @@ import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class GetMainLooperViaContextDetectorTest : LintDetectorTest() { +class NonInjectedMainThreadDetectorTest : LintDetectorTest() { - override fun getDetector(): Detector = GetMainLooperViaContextDetector() + override fun getDetector(): Detector = NonInjectedMainThreadDetector() override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - override fun getIssues(): List<Issue> = listOf(GetMainLooperViaContextDetector.ISSUE) - - private val explanation = "Please inject a @Main Executor instead." + override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE) @Test fun testGetMainThreadHandler() { @@ -43,7 +41,7 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import android.os.Handler; - public class TestClass1 { + public class TestClass { public void test(Context context) { Handler mainThreadHandler = context.getMainThreadHandler(); } @@ -53,10 +51,16 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { .indented(), *stubs ) - .issues(GetMainLooperViaContextDetector.ISSUE) + .issues(NonInjectedMainThreadDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Handler mainThreadHandler = context.getMainThreadHandler(); + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test @@ -69,7 +73,7 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import android.os.Looper; - public class TestClass1 { + public class TestClass { public void test(Context context) { Looper mainLooper = context.getMainLooper(); } @@ -79,10 +83,16 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { .indented(), *stubs ) - .issues(GetMainLooperViaContextDetector.ISSUE) + .issues(NonInjectedMainThreadDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Looper mainLooper = context.getMainLooper(); + ~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test @@ -95,7 +105,7 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import java.util.concurrent.Executor; - public class TestClass1 { + public class TestClass { public void test(Context context) { Executor mainExecutor = context.getMainExecutor(); } @@ -105,10 +115,16 @@ class GetMainLooperViaContextDetectorTest : LintDetectorTest() { .indented(), *stubs ) - .issues(GetMainLooperViaContextDetector.ISSUE) + .issues(NonInjectedMainThreadDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Executor mainExecutor = context.getMainExecutor(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } private val stubs = androidStubs diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt index 6b9f88fedbdd..c83a35b46ca6 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt @@ -39,7 +39,7 @@ class NonInjectedServiceDetectorTest : LintDetectorTest() { package test.pkg; import android.content.Context; - public class TestClass1 { + public class TestClass { public void getSystemServiceWithoutDagger(Context context) { context.getSystemService("user"); } @@ -51,8 +51,14 @@ class NonInjectedServiceDetectorTest : LintDetectorTest() { ) .issues(NonInjectedServiceDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains("Use @Inject to get the handle") + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService] + context.getSystemService("user"); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test @@ -65,7 +71,7 @@ class NonInjectedServiceDetectorTest : LintDetectorTest() { import android.content.Context; import android.os.UserManager; - public class TestClass2 { + public class TestClass { public void getSystemServiceWithoutDagger(Context context) { context.getSystemService(UserManager.class); } @@ -77,8 +83,46 @@ class NonInjectedServiceDetectorTest : LintDetectorTest() { ) .issues(NonInjectedServiceDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains("Use @Inject to get the handle") + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService] + context.getSystemService(UserManager.class); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testGetAccountManager() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.accounts.AccountManager; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + AccountManager.get(context); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedServiceDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace AccountManager.get() with an injected instance of AccountManager [NonInjectedService] + AccountManager.get(context); + ~~~ + 0 errors, 1 warnings + """ + ) } private val stubs = androidStubs diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt index 802ceba4196c..ebcddebfbc28 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt @@ -31,8 +31,6 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE) - private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher." - @Test fun testRegisterReceiver() { lint() @@ -44,7 +42,7 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import android.content.IntentFilter; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter) { context.registerReceiver(receiver, filter, 0); @@ -57,8 +55,14 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { ) .issues(RegisterReceiverViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiver(receiver, filter, 0); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test @@ -74,7 +78,7 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.os.Handler; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) { context.registerReceiverAsUser(receiver, UserHandle.ALL, filter, @@ -88,8 +92,14 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { ) .issues(RegisterReceiverViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiverAsUser(receiver, UserHandle.ALL, filter, + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test @@ -105,7 +115,7 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.os.Handler; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) { context.registerReceiverForAllUsers(receiver, filter, "permission", @@ -119,8 +129,14 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { ) .issues(RegisterReceiverViaContextDetector.ISSUE) .run() - .expectWarningCount(1) - .expectContains(explanation) + .expect( + """ + src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiverForAllUsers(receiver, filter, "permission", + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } private val stubs = androidStubs diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt index e26583793e20..b03a11c4f02f 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt @@ -44,7 +44,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import android.app.ActivityManager; - public class TestClass1 { + public class TestClass { public void slewlyGetCurrentUser() { ActivityManager.getCurrentUser(); } @@ -59,10 +59,13 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY ) .run() - .expectWarningCount(1) - .expectContains( - "ActivityManager.getCurrentUser() is slow. " + - "Use UserTracker.getUserId() instead." + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserId() instead of ActivityManager.getCurrentUser() [SlowUserIdQuery] + ActivityManager.getCurrentUser(); + ~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -75,7 +78,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import android.os.UserManager; - public class TestClass2 { + public class TestClass { public void slewlyGetUserInfo(UserManager userManager) { userManager.getUserInfo(); } @@ -90,9 +93,13 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY ) .run() - .expectWarningCount(1) - .expectContains( - "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead." + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserInfo() instead of UserManager.getUserInfo() [SlowUserInfoQuery] + userManager.getUserInfo(); + ~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -105,7 +112,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import com.android.systemui.settings.UserTracker; - public class TestClass3 { + public class TestClass { public void quicklyGetUserId(UserTracker userTracker) { userTracker.getUserId(); } @@ -132,7 +139,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import com.android.systemui.settings.UserTracker; - public class TestClass4 { + public class TestClass { public void quicklyGetUserId(UserTracker userTracker) { userTracker.getUserInfo(); } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index fd6ab09a2ccd..fb6537e92d15 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -31,8 +31,6 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE) - private val explanation = "Usage of Config.HARDWARE is highly encouraged." - @Test fun testSoftwareBitmap() { lint() @@ -41,7 +39,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { """ import android.graphics.Bitmap; - public class TestClass1 { + public class TestClass { public void test() { Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); @@ -54,8 +52,17 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { ) .issues(SoftwareBitmapDetector.ISSUE) .run() - .expectWarningCount(2) - .expectContains(explanation) + .expect( + """ + src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + ARGB_8888, + ~~~~~~~~~ + src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + RGB_565, + ~~~~~~~ + 0 errors, 2 warnings + """ + ) } @Test @@ -66,7 +73,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { """ import android.graphics.Bitmap; - public class TestClass1 { + public class TestClass { public void test() { Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE); } @@ -78,7 +85,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { ) .issues(SoftwareBitmapDetector.ISSUE) .run() - .expectWarningCount(0) + .expectClean() } private val stubs = androidStubs diff --git a/packages/SystemUI/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md index 6b6dce5da169..10bd3679a13b 100644 --- a/packages/SystemUI/docs/device-entry/doze.md +++ b/packages/SystemUI/docs/device-entry/doze.md @@ -1,5 +1,7 @@ # Doze +`Dozing` is a low-powered state of the device. If Always-on Display (AOD), pulsing, or wake-gestures are enabled, then the device will enter the `dozing` state after a user intent to turn off the screen (ie: power button) or the screen times out. + Always-on Display (AOD) provides an alternative 'screen-off' experience. Instead, of completely turning the display off, it provides a distraction-free, glanceable experience for the phone in a low-powered mode. In this low-powered mode, the display will have a lower refresh rate and the UI should frequently shift its displayed contents in order to prevent burn-in. The recommended max on-pixel-ratio (OPR) is 5% to reduce battery consumption.  @@ -58,7 +60,7 @@ When Dozing is enabled, it can still be suppressed based on the device state. On Refer to the documentation in [DozeSuppressors][15] for more information. ## AOD burn-in and image retention -Because AOD will show an image on the screen for an elogated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention. +Because AOD will show an image on the screen for an elongated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention. To prevent burn-in, it is recommended to often shift UI on the screen. [DozeUi][17] schedules a call to dozeTimeTick every minute to request a shift in UI for all elements on AOD. The amount of shift can be determined by undergoing simulated AOD testing since this may vary depending on the display. diff --git a/packages/SystemUI/docs/device-entry/glossary.md b/packages/SystemUI/docs/device-entry/glossary.md index f3d12c21a3a5..7f19b1688de0 100644 --- a/packages/SystemUI/docs/device-entry/glossary.md +++ b/packages/SystemUI/docs/device-entry/glossary.md @@ -2,38 +2,38 @@ ## Keyguard -| Term | Description | -| :-----------: | ----------- | -| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.| -| Lock screen<br><br>| The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10].| -| Bouncer, [bouncer.md][2]<br><br>| The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.| -| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]| -| Ambient display (AOD), [doze.md][6]<br><br>| UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.| +| Term | Description | +|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer. | +| Lock screen<br><br> | The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10]. | +| Bouncer, [bouncer.md][2]<br><br> | The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM. | +| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5] | +| Ambient display (AOD), [doze.md][6]<br><br> | UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices. | ## General Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input.| -| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7].| +| Term | Description | +|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input. | +| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7]. | ## Face Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry.| -| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).| -| Bypass User Journey <br><br>| Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.| -| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.| +| Term | Description | +|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry. | +| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above). | +| Bypass User Journey <br><br> | Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen. | +| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility. | ## Fingerprint Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after two hard-fingerprint-failures, the primary authentication bouncer is shown</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul>| -| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.| +| Term | Description | +|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after multiple consecutive hard-fingerprint-failures, the primary authentication bouncer is shown. The exact number of attempts is defined in: [BiometricUnlockController#UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER][4]</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul> | +| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade. | ## Other Authentication Terms -| Term | Description | -| ---------- | ----------- | -| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently.| +| Term | Description | +|--------------|-----------------------------------------------------------------------| +| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently. | [1]: /frameworks/base/packages/SystemUI/docs/device-entry/keyguard.md @@ -46,3 +46,4 @@ [8]: /frameworks/base/packages/SystemUI/res-keyguard/font/clock.xml [9]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java [10]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java +[11]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java diff --git a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml index 69390848245d..33c68bf1f6ac 100644 --- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml +++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml @@ -16,6 +16,6 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <solid android:color="@android:color/white" /> + <solid android:color="@android:color/transparent" /> <corners android:radius="@dimen/qs_media_album_radius" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index c2dffe8bdad0..d05bd5120872 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.people.PeopleProvider; +import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.FoldStateLogger; import com.android.systemui.unfold.FoldStateLoggingProvider; @@ -63,6 +64,7 @@ import dagger.Subcomponent; @Subcomponent(modules = { DefaultComponentBinder.class, DependencyProvider.class, + QsFrameTranslateModule.class, SystemUIBinder.class, SystemUIModule.class, SystemUICoreStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index d70b971dba14..dc3dadb32669 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -61,7 +61,6 @@ import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; @@ -133,7 +132,6 @@ import dagger.Provides; PeopleModule.class, PluginModule.class, PrivacyModule.class, - QsFrameTranslateModule.class, ScreenshotModule.class, SensorModule.class, MultiUserUtilsModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 9dd18b21cc71..80bff83d03a0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -11,6 +11,7 @@ import android.util.MathUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.PathInterpolator import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import com.android.internal.logging.InstanceId @@ -95,7 +96,8 @@ class MediaCarouselController @Inject constructor( * finished */ @MediaLocation - private var currentEndLocation: Int = -1 + @VisibleForTesting + var currentEndLocation: Int = -1 /** * The ending location of the view where it ends when all animations and transitions have @@ -126,7 +128,8 @@ class MediaCarouselController @Inject constructor( lateinit var settingsButton: View private set private val mediaContent: ViewGroup - private val pageIndicator: PageIndicator + @VisibleForTesting + val pageIndicator: PageIndicator private val visualStabilityCallback: OnReorderingAllowedListener private var needsReordering: Boolean = false private var keysNeedRemoval = mutableSetOf<String>() @@ -149,6 +152,27 @@ class MediaCarouselController @Inject constructor( } } } + + companion object { + const val ANIMATION_BASE_DURATION = 2200f + const val DURATION = 167f + const val DETAILS_DELAY = 1067f + const val CONTROLS_DELAY = 1400f + const val PAGINATION_DELAY = 1900f + const val MEDIATITLES_DELAY = 1000f + const val MEDIACONTAINERS_DELAY = 967f + val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F) + val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F) + + fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { + val transformStartFraction = delay / ANIMATION_BASE_DURATION + val transformDurationFraction = duration / ANIMATION_BASE_DURATION + val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) + return MathUtils.constrain((squishinessToTime - transformStartFraction) / + transformDurationFraction, 0F, 1F) + } + } + private val configListener = object : ConfigurationController.ConfigurationListener { override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may occur @@ -633,12 +657,17 @@ class MediaCarouselController @Inject constructor( } } - private fun updatePageIndicatorAlpha() { + @VisibleForTesting + fun updatePageIndicatorAlpha() { val hostStates = mediaHostStatesManager.mediaHostStates val endIsVisible = hostStates[currentEndLocation]?.visible ?: false val startIsVisible = hostStates[currentStartLocation]?.visible ?: false val startAlpha = if (startIsVisible) 1.0f else 0.0f - val endAlpha = if (endIsVisible) 1.0f else 0.0f + // when squishing in split shade, only use endState, which keeps changing + // to provide squishFraction + val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F + val endAlpha = (if (endIsVisible) 1.0f else 0.0f) * + calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) var alpha = 1.0f if (!endIsVisible || !startIsVisible) { var progress = currentTransitionProgress @@ -687,6 +716,7 @@ class MediaCarouselController @Inject constructor( mediaCarouselScrollHandler.setCarouselBounds( currentCarouselWidth, currentCarouselHeight) updatePageIndicatorLocation() + updatePageIndicatorAlpha() } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index ef49fd35d703..a776897b2fd5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -47,7 +47,7 @@ private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. */ private val translationConfig = PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY) /** @@ -289,7 +289,10 @@ class MediaCarouselScrollHandler( return false } } - if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { + if (motionEvent.action == MotionEvent.ACTION_MOVE) { + // cancel on going animation if there is any. + PhysicsAnimator.getInstance(this).cancel() + } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { // It's an up and the fling didn't take it above val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding val scrollXAmount: Int diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index bffb0fdec707..864592238b73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -203,6 +203,14 @@ class MediaHost constructor( } } + override var squishFraction: Float = 1.0f + set(value) { + if (!value.equals(field)) { + field = value + changedListener?.invoke() + } + } + override var showsOnlyActiveMedia: Boolean = false set(value) { if (!value.equals(field)) { @@ -253,6 +261,7 @@ class MediaHost constructor( override fun copy(): MediaHostState { val mediaHostState = MediaHostStateHolder() mediaHostState.expansion = expansion + mediaHostState.squishFraction = squishFraction mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.measurementInput = measurementInput?.copy() mediaHostState.visible = visible @@ -271,6 +280,9 @@ class MediaHost constructor( if (expansion != other.expansion) { return false } + if (squishFraction != other.squishFraction) { + return false + } if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) { return false } @@ -289,6 +301,7 @@ class MediaHost constructor( override fun hashCode(): Int { var result = measurementInput?.hashCode() ?: 0 result = 31 * result + expansion.hashCode() + result = 31 * result + squishFraction.hashCode() result = 31 * result + falsingProtectionNeeded.hashCode() result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + if (visible) 1 else 2 @@ -329,6 +342,11 @@ interface MediaHostState { var expansion: Float /** + * Fraction of the height animation. + */ + var squishFraction: Float + + /** * Is this host only showing active media or is it showing all of them including resumption? */ var showsOnlyActiveMedia: Boolean diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index ac59175d4646..faa7aaee3c9a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -18,8 +18,15 @@ package com.android.systemui.media import android.content.Context import android.content.res.Configuration +import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.R +import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.TransitionLayout @@ -50,6 +57,24 @@ class MediaViewController @Inject constructor( companion object { @JvmField val GUTS_ANIMATION_DURATION = 500L + val controlIds = setOf( + R.id.media_progress_bar, + R.id.actionNext, + R.id.actionPrev, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.media_scrubbing_elapsed_time, + R.id.media_scrubbing_total_time + ) + + val detailIds = setOf( + R.id.header_title, + R.id.header_artist, + R.id.actionPlayPause, + ) } /** @@ -57,6 +82,7 @@ class MediaViewController @Inject constructor( */ lateinit var sizeChangedListener: () -> Unit private var firstRefresh: Boolean = true + @VisibleForTesting private var transitionLayout: TransitionLayout? = null private val layoutController = TransitionLayoutController() private var animationDelay: Long = 0 @@ -279,10 +305,47 @@ class MediaViewController @Inject constructor( } /** + * Apply squishFraction to a copy of viewState such that the cached version is untouched. + */ + internal fun squishViewState( + viewState: TransitionViewState, + squishFraction: Float + ): TransitionViewState { + val squishedViewState = viewState.copy() + squishedViewState.height = (squishedViewState.height * squishFraction).toInt() + controlIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION) + } + } + + detailIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION) + } + } + + RecommendationViewHolder.mediaContainersIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION) + } + } + + RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION) + } + } + + return squishedViewState + } + + /** * Obtain a new viewState for a given media state. This usually returns a cached state, but if * it's not available, it will recreate one by measuring, which may be expensive. */ - private fun obtainViewState(state: MediaHostState?): TransitionViewState? { + @VisibleForTesting + fun obtainViewState(state: MediaHostState?): TransitionViewState? { if (state == null || state.measurementInput == null) { return null } @@ -291,41 +354,46 @@ class MediaViewController @Inject constructor( val viewState = viewStates[cacheKey] if (viewState != null) { // we already have cached this measurement, let's continue + if (state.squishFraction <= 1f) { + return squishViewState(viewState, state.squishFraction) + } return viewState } // Copy the key since this might call recursively into it and we're using tmpKey cacheKey = cacheKey.copy() val result: TransitionViewState? - if (transitionLayout != null) { - // Let's create a new measurement - if (state.expansion == 0.0f || state.expansion == 1.0f) { - result = transitionLayout!!.calculateViewState( - state.measurementInput!!, - constraintSetForExpansion(state.expansion), - TransitionViewState()) - - setGutsViewState(result) - // We don't want to cache interpolated or null states as this could quickly fill up - // our cache. We only cache the start and the end states since the interpolation - // is cheap - viewStates[cacheKey] = result - } else { - // This is an interpolated state - val startState = state.copy().also { it.expansion = 0.0f } - - // Given that we have a measurement and a view, let's get (guaranteed) viewstates - // from the start and end state and interpolate them - val startViewState = obtainViewState(startState) as TransitionViewState - val endState = state.copy().also { it.expansion = 1.0f } - val endViewState = obtainViewState(endState) as TransitionViewState - result = layoutController.getInterpolatedState( - startViewState, - endViewState, - state.expansion) - } + if (transitionLayout == null) { + return null + } + // Let's create a new measurement + if (state.expansion == 0.0f || state.expansion == 1.0f) { + result = transitionLayout!!.calculateViewState( + state.measurementInput!!, + constraintSetForExpansion(state.expansion), + TransitionViewState()) + + setGutsViewState(result) + // We don't want to cache interpolated or null states as this could quickly fill up + // our cache. We only cache the start and the end states since the interpolation + // is cheap + viewStates[cacheKey] = result } else { - result = null + // This is an interpolated state + val startState = state.copy().also { it.expansion = 0.0f } + + // Given that we have a measurement and a view, let's get (guaranteed) viewstates + // from the start and end state and interpolate them + val startViewState = obtainViewState(startState) as TransitionViewState + val endState = state.copy().also { it.expansion = 1.0f } + val endViewState = obtainViewState(endState) as TransitionViewState + result = layoutController.getInterpolatedState( + startViewState, + endViewState, + state.expansion) + } + if (state.squishFraction <= 1f) { + return squishViewState(result, state.squishFraction) } return result } diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt index 52ac4e0682a3..8ae75fc34acb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt @@ -106,5 +106,20 @@ class RecommendationViewHolder private constructor(itemView: View) { R.id.media_subtitle2, R.id.media_subtitle3 ) + + val mediaTitlesAndSubtitlesIds = setOf( + R.id.media_title1, + R.id.media_title2, + R.id.media_title3, + R.id.media_subtitle1, + R.id.media_subtitle2, + R.id.media_subtitle3 + ) + + val mediaContainersIds = setOf( + R.id.media_cover1_container, + R.id.media_cover2_container, + R.id.media_cover3_container + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 1ef6426d52a0..0fe3d1699de0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -691,6 +691,15 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mQSAnimator != null) { mQSAnimator.setPosition(expansion); } + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD + || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { + // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen + // and media player expect no change by squishiness in lock screen shade + mQsMediaHost.setSquishFraction(1.0F); + } else { + mQsMediaHost.setSquishFraction(mSquishinessFraction); + } + } private void setAlphaAnimationProgress(float progress) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index 3e445ddfc2a1..d39368012487 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -36,6 +36,7 @@ import android.util.ArraySet; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; @@ -182,6 +183,10 @@ public class TileLifecycleManager extends BroadcastReceiver implements setBindService(true); } + /** + * Binds or unbinds to IQSService + */ + @WorkerThread public void setBindService(boolean bind) { if (mBound && mUnbindImmediate) { // If we are already bound and expecting to unbind, this means we should stay bound diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt index 309059fdb9ad..95cc0dcadfb4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt @@ -76,7 +76,7 @@ class RequestProcessor @Inject constructor( ) } else { // Create a new request of the same type which includes the top component - ScreenshotRequest(request.source, request.type, info.component) + ScreenshotRequest(request.type, request.source, info.component) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ee15f4eec2a8..e3318127af93 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -3686,8 +3686,9 @@ public final class NotificationPanelViewController { } private void updateQsFrameTranslation() { - mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, mOverExpansion, - mQsTranslationForFullShadeTransition); + mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, + mNavigationBarBottomHeight + mAmbientState.getStackTopMargin()); + } private void onTrackingStarted() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java index 78077386179a..59afb18195dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java @@ -36,8 +36,7 @@ public abstract class QsFrameTranslateController { /** * Calculate and translate the QS Frame on the Y-axis. */ - public abstract void translateQsFrame(View qsFrame, QS qs, float overExpansion, - float qsTranslationForFullShadeTransition); + public abstract void translateQsFrame(View qsFrame, QS qs, int bottomInset); /** * Calculate the top padding for notifications panel. This could be the supplied diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java index 33e224579bef..85b522cbd9d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java @@ -27,6 +27,8 @@ import javax.inject.Inject; /** * Default implementation of QS Translation. This by default does not do much. + * This class can be subclassed to allow System UI variants the flexibility to change position of + * the Quick Settings frame. */ @SysUISingleton public class QsFrameTranslateImpl extends QsFrameTranslateController { @@ -37,8 +39,8 @@ public class QsFrameTranslateImpl extends QsFrameTranslateController { } @Override - public void translateQsFrame(View qsFrame, QS qs, float overExpansion, - float qsTranslationForFullShadeTransition) { + public void translateQsFrame(View qsFrame, QS qs, int bottomInset) { + // Empty implementation by default, meant to be overridden by subclasses. } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index ccf6feca6992..8f3eb4f7e223 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -440,6 +440,42 @@ class HeadsUpCoordinator @Inject constructor( override fun onEntryCleanUp(entry: NotificationEntry) { mHeadsUpViewBinder.abortBindCallback(entry) } + + /** + * Identify notifications whose heads-up state changes when the notification rankings are + * updated, and have those changed notifications alert if necessary. + * + * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any + * handling of ranking changes needs to take into account that we may have just made a + * PostedEntry for some of these notifications. + */ + override fun onRankingApplied() { + // Because a ranking update may cause some notifications that are no longer (or were + // never) in mPostedEntries to need to alert, we need to check every notification + // known to the pipeline. + for (entry in mNotifPipeline.allNotifs) { + // The only entries we can consider alerting for here are entries that have never + // interrupted and that now say they should heads up; if they've alerted in the + // past, we don't want to incorrectly alert a second time if there wasn't an + // explicit notification update. + if (entry.hasInterrupted()) continue + + // The cases where we should consider this notification to be updated: + // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp + // state + // - if it is present in PostedEntries and the previous state of shouldHeadsUp + // differs from the updated one + val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry, + /* log= */ false) + val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false + val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver + + if (shouldUpdateEntry) { + mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver) + onEntryUpdated(entry) + } + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index 204a494c32e8..8625cdbc89d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -59,4 +59,13 @@ class HeadsUpCoordinatorLogger constructor( " numPostedEntries=$int1 logicalGroupSize=$int2" }) } + + fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = key + bool1 = shouldHun + }, { + "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1" + }) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 087dc71f6cf2..1b006485c83d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1487,7 +1487,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.setAlpha(alpha); } if (mChildrenContainer != null) { - mChildrenContainer.setAlpha(alpha); + mChildrenContainer.setContentAlpha(alpha); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 7b23a56af836..0dda2632db66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -461,6 +461,20 @@ public class NotificationChildrenContainer extends ViewGroup return mAttachedChildren; } + /** + * Sets the alpha on the content, while leaving the background of the container itself as is. + * + * @param alpha alpha value to apply to the content + */ + public void setContentAlpha(float alpha) { + for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { + mNotificationHeader.getChildAt(i).setAlpha(alpha); + } + for (ExpandableNotificationRow child : getAttachedChildren()) { + child.setContentAlpha(alpha); + } + } + /** To be called any time the rows have been updated */ public void updateExpansionStates() { if (mChildrenExpanded || mUserLocked) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 836cacc185c6..55c577f1ea39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1115,6 +1115,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable updateAlgorithmLayoutMinHeight(); updateOwnTranslationZ(); + // Give The Algorithm information regarding the QS height so it can layout notifications + // properly. Needed for some devices that grows notifications down-to-top + mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight()); + // Once the layout has finished, we don't need to animate any scrolling clampings anymore. mAnimateStackYForContentHeightChange = false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 8d28f7524f9a..0502159f46cd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -417,12 +417,19 @@ public class StackScrollAlgorithm { } /** + * Update the position of QS Frame. + */ + public void updateQSFrameTop(int qsHeight) { + // Intentionally empty for sub-classes in other device form factors to override + } + + /** * Determine the positions for the views. This is the main part of the algorithm. * * @param algorithmState The state in which the current pass of the algorithm is currently in * @param ambientState The current ambient state */ - private void updatePositionsForState(StackScrollAlgorithmState algorithmState, + protected void updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState) { if (!ambientState.isOnKeyguard() || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { @@ -448,7 +455,7 @@ public class StackScrollAlgorithm { * @return Fraction to apply to view height and gap between views. * Does not include shelf height even if shelf is showing. */ - private float getExpansionFractionWithoutShelf( + protected float getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt index 5ad354247a04..f34c2ac57a5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt @@ -25,6 +25,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener @@ -398,4 +403,24 @@ class MediaCarouselControllerTest : SysuiTestCase() { // added to the end because it was active less recently. assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) } + + @Test + fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { + val delta = 0.0001F + val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION) + val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION) + whenever(mediaHostStatesManager.mediaHostStates) + .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) + whenever(mediaHostState.visible).thenReturn(true) + mediaCarouselController.currentEndLocation = LOCATION_QS + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt new file mode 100644 index 000000000000..622a512720d9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.TransitionLayout +import com.android.systemui.util.animation.TransitionViewState +import com.android.systemui.util.animation.WidgetState +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.floatThat +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaViewControllerTest : SysuiTestCase() { + private val mediaHostStateHolder = MediaHost.MediaHostStateHolder() + private val mediaHostStatesManager = MediaHostStatesManager() + private val configurationController = + com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context) + private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) + private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) + @Mock lateinit var logger: MediaViewLogger + @Mock private lateinit var mockViewState: TransitionViewState + @Mock private lateinit var mockCopiedState: TransitionViewState + @Mock private lateinit var detailWidgetState: WidgetState + @Mock private lateinit var controlWidgetState: WidgetState + @Mock private lateinit var mediaTitleWidgetState: WidgetState + @Mock private lateinit var mediaContainerWidgetState: WidgetState + + val delta = 0.0001F + + private lateinit var mediaViewController: MediaViewController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaViewController = + MediaViewController(context, configurationController, mediaHostStatesManager, logger) + } + + @Test + fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + player.measureState = TransitionViewState().apply { this.height = 100 } + mediaHostStateHolder.expansion = 1f + val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + mediaHostStateHolder.measurementInput = + MeasurementInput(widthMeasureSpec, heightMeasureSpec) + + // Test no squish + mediaHostStateHolder.squishFraction = 1f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) + + // Test half squish + mediaHostStateHolder.squishFraction = 0.5f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) + } + + @Test + fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() { + mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) + recommendation.measureState = TransitionViewState().apply { this.height = 100 } + mediaHostStateHolder.expansion = 1f + val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + mediaHostStateHolder.measurementInput = + MeasurementInput(widthMeasureSpec, heightMeasureSpec) + + // Test no squish + mediaHostStateHolder.squishFraction = 1f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) + + // Test half squish + mediaHostStateHolder.squishFraction = 0.5f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) + } + + @Test + fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() { + whenever(mockViewState.copy()).thenReturn(mockCopiedState) + whenever(mockCopiedState.widgetStates) + .thenReturn( + mutableMapOf( + R.id.media_progress_bar to controlWidgetState, + R.id.header_artist to detailWidgetState + ) + ) + + val detailSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, detailSquishMiddle) + verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val detailSquishEnd = + TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION) + mediaViewController.squishViewState(mockViewState, detailSquishEnd) + verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + + val controlSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, controlSquishMiddle) + verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val controlSquishEnd = + TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION) + mediaViewController.squishViewState(mockViewState, controlSquishEnd) + verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + } + + @Test + fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() { + whenever(mockViewState.copy()).thenReturn(mockCopiedState) + whenever(mockCopiedState.widgetStates) + .thenReturn( + mutableMapOf( + R.id.media_title1 to mediaTitleWidgetState, + R.id.media_cover1_container to mediaContainerWidgetState + ) + ) + + val containerSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, containerSquishMiddle) + verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val containerSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, containerSquishEnd) + verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + + val titleSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, titleSquishMiddle) + verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val titleSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, titleSquishEnd) + verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt index 5cb27a47d384..46a502acba16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt @@ -99,13 +99,14 @@ class RequestProcessorTest { policy.getDefaultDisplayId(), DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) - val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD) + val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER) val processor = RequestProcessor(imageCapture, policy, flags, scope) val processedRequest = processor.process(request) // Request has topComponent added, but otherwise unchanged. assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN) + assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER) assertThat(processedRequest.topComponent).isEqualTo(component) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index f9e279eb5871..5b34a95d4fb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt @@ -3,72 +3,72 @@ package com.android.systemui.shared.system import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import java.lang.Thread.UncaughtExceptionHandler import org.junit.Assert.assertThrows import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.only import org.mockito.Mockito.any +import org.mockito.Mockito.only import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.lang.Thread.UncaughtExceptionHandler @SmallTest class UncaughtExceptionPreHandlerTest : SysuiTestCase() { - private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager + private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager - @Mock - private lateinit var mockHandler: UncaughtExceptionHandler + @Mock private lateinit var mockHandler: UncaughtExceptionHandler - @Mock - private lateinit var mockHandler2: UncaughtExceptionHandler + @Mock private lateinit var mockHandler2: UncaughtExceptionHandler - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager = UncaughtExceptionPreHandlerManager() - } + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager = UncaughtExceptionPreHandlerManager() + } - @Test - fun registerHandler_registersOnceOnly() { - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_registersOnceOnly() { + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_setsUncaughtExceptionPreHandler() { - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager.registerHandler(mockHandler) - assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() - } + @Test + fun registerHandler_setsUncaughtExceptionPreHandler() { + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager.registerHandler(mockHandler) + assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() + } - @Test - fun registerHandler_preservesOriginalHandler() { - Thread.setUncaughtExceptionPreHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_preservesOriginalHandler() { + Thread.setUncaughtExceptionPreHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_toleratesHandlersThatThrow() { - `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler2, only()).uncaughtException(any(), any()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + @Ignore + fun registerHandler_toleratesHandlersThatThrow() { + `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler2, only()).uncaughtException(any(), any()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_doesNotSetUpTwice() { - UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) - assertThrows(IllegalStateException::class.java) { - preHandlerManager.registerHandler(mockHandler) - } + @Test + fun registerHandler_doesNotSetUpTwice() { + UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) + assertThrows(IllegalStateException::class.java) { + preHandlerManager.registerHandler(mockHandler) } -}
\ No newline at end of file + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 2970807afb36..340bc96f80c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -47,6 +47,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock +import java.util.ArrayList +import java.util.function.Consumer import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -57,10 +59,8 @@ import org.mockito.BDDMockito.given import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import java.util.ArrayList -import java.util.function.Consumer import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -671,8 +671,64 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupChild2) } + @Test + fun testOnRankingApplied_newEntryShouldAlert() { + // GIVEN that mEntry has never interrupted in the past, and now should + assertFalse(mEntry.hasInterrupted()) + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is shown + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { + // GIVEN that mEntry has alerted in the past + mEntry.setInterruption() + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + + @Test + fun testOnRankingApplied_entryUpdatedToHun() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it is then updated such that it should now HUN + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is shown + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) + whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) + .thenReturn(should) } private fun finishBind(entry: NotificationEntry) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9c080e856500..2eb2cf643c42 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4076,7 +4076,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // to the restarted activity. nowVisible = mVisibleRequested; } - mTransitionController.requestCloseTransitionIfNeeded(this); + // upgrade transition trigger to task if this is the last activity since it means we are + // closing the task. + final WindowContainer trigger = remove && task != null && task.getChildCount() == 1 + ? task : this; + mTransitionController.requestCloseTransitionIfNeeded(trigger); cleanUp(true /* cleanServices */, true /* setState */); if (remove) { if (mStartingData != null && mVisible && task != null) { @@ -7634,6 +7638,31 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); } + /** + * Returns the requested {@link Configuration.Orientation} for the current activity. + * + * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the + * requested orientation for the activity below which is the first activity with an explicit + * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link + * SCREEN_ORIENTATION_BEHIND}. + */ + @Configuration.Orientation + @Override + int getRequestedConfigurationOrientation(boolean forDisplay) { + if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) { + // We use Task here because we want to be consistent with what happens in + // multi-window mode where other tasks orientations are ignored. + final ActivityRecord belowCandidate = task.getActivity( + a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing + && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this, + false /* includeBoundary */, true /* traverseTopToBottom */); + if (belowCandidate != null) { + return belowCandidate.getRequestedConfigurationOrientation(forDisplay); + } + } + return super.getRequestedConfigurationOrientation(forDisplay); + } + @Override void onCancelFixedRotationTransform(int originalDisplayRotation) { if (this != mDisplayContent.getLastOrientationSource()) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index b1862b6f274f..46253c1933b6 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1878,15 +1878,15 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe flags |= FLAG_TRANSLUCENT; } final Task task = wc.asTask(); - if (task != null && task.voiceSession != null) { - flags |= FLAG_IS_VOICE_INTERACTION; - } if (task != null) { final ActivityRecord topActivity = task.getTopNonFinishingActivity(); if (topActivity != null && topActivity.mStartingData != null && topActivity.mStartingData.hasImeSurface()) { flags |= FLAG_WILL_IME_SHOWN; } + if (task.voiceSession != null) { + flags |= FLAG_IS_VOICE_INTERACTION; + } } Task parentTask = null; final ActivityRecord record = wc.asActivityRecord(); @@ -1914,20 +1914,26 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Whether the container fills its parent Task bounds. flags |= FLAG_FILLS_TASK; } - } - final DisplayContent dc = wc.asDisplayContent(); - if (dc != null) { - flags |= FLAG_IS_DISPLAY; - if (dc.hasAlertWindowSurfaces()) { - flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS; + } else { + final DisplayContent dc = wc.asDisplayContent(); + if (dc != null) { + flags |= FLAG_IS_DISPLAY; + if (dc.hasAlertWindowSurfaces()) { + flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS; + } + } else if (isWallpaper(wc)) { + flags |= FLAG_IS_WALLPAPER; + } else if (isInputMethod(wc)) { + flags |= FLAG_IS_INPUT_METHOD; + } else { + // In this condition, the wc can only be WindowToken or DisplayArea. + final int type = wc.getWindowType(); + if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { + flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW; + } } } - if (isWallpaper(wc)) { - flags |= FLAG_IS_WALLPAPER; - } - if (isInputMethod(wc)) { - flags |= FLAG_IS_INPUT_METHOD; - } if (occludesKeyguard(wc)) { flags |= FLAG_OCCLUDES_KEYGUARD; } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index e8682f7a3b22..26ce4ae8415c 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -126,19 +126,27 @@ class TransitionController { mTransitionTracer = transitionTracer; mTransitionPlayerDeath = () -> { synchronized (mAtm.mGlobalLock) { - // Clean-up/finish any playing transitions. - for (int i = 0; i < mPlayingTransitions.size(); ++i) { - mPlayingTransitions.get(i).cleanUpOnFailure(); - } - mPlayingTransitions.clear(); - mTransitionPlayer = null; - mTransitionPlayerProc = null; - mRemotePlayer.clear(); - mRunningLock.doNotifyLocked(); + detachPlayer(); } }; } + private void detachPlayer() { + if (mTransitionPlayer == null) return; + // Clean-up/finish any playing transitions. + for (int i = 0; i < mPlayingTransitions.size(); ++i) { + mPlayingTransitions.get(i).cleanUpOnFailure(); + } + mPlayingTransitions.clear(); + if (mCollectingTransition != null) { + mCollectingTransition.abort(); + } + mTransitionPlayer = null; + mTransitionPlayerProc = null; + mRemotePlayer.clear(); + mRunningLock.doNotifyLocked(); + } + /** @see #createTransition(int, int) */ @NonNull Transition createTransition(int type) { @@ -193,7 +201,7 @@ class TransitionController { if (mTransitionPlayer.asBinder() != null) { mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0); } - mTransitionPlayer = null; + detachPlayer(); } if (player.asBinder() != null) { player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 949fa9662258..32a110ea530e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1136,10 +1136,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final LauncherAppsServiceInternal launcherApps = LocalServices.getService( LauncherAppsServiceInternal.class); - launcherApps.startShortcut(caller.mUid, caller.mPid, callingPackage, - hop.getShortcutInfo().getPackage(), null /* default featureId */, + final boolean success = launcherApps.startShortcut(caller.mUid, caller.mPid, + callingPackage, hop.getShortcutInfo().getPackage(), null /* featureId */, hop.getShortcutInfo().getId(), null /* sourceBounds */, launchOpts, hop.getShortcutInfo().getUserId()); + if (success) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } break; } case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index d5447447a7b2..462957a88a6c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2319,6 +2319,22 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie)); } + @Test + public void testOrientationForScreenOrientationBehind() { + final Task task = createTask(mDisplayContent); + // Activity below + new ActivityBuilder(mAtm) + .setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) + .build(); + final ActivityRecord activityTop = new ActivityBuilder(mAtm) + .setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_BEHIND) + .build(); + final int topOrientation = activityTop.getRequestedConfigurationOrientation(); + assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation); + } + private void verifyProcessInfoUpdate(ActivityRecord activity, State state, boolean shouldUpdate, boolean activityChange) { reset(activity.app); diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java index eafcef2f1d38..1e74451a8d4d 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -210,21 +210,15 @@ final class TranslationManagerServiceImpl extends final int translatedAppUid = getAppUidByComponentName(getContext(), componentName, getUserId()); final String packageName = componentName.getPackageName(); - if (activityDestroyed) { - // In the Activity destroy case, we only calls onTranslationFinished() in - // non-finisTranslation() state. If there is a finisTranslation() calls by apps, we - // should remove the waiting callback to avoid callback twice. + // In the Activity destroyed case, we only call onTranslationFinished() in + // non-finishTranslation() state. If there is a finishTranslation() call by apps, we + // should remove the waiting callback to avoid invoking callbacks twice. + if (activityDestroyed || mWaitingFinishedCallbackActivities.contains(token)) { invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, /* sourceSpec= */ null, /* targetSpec= */ null, packageName, translatedAppUid); mWaitingFinishedCallbackActivities.remove(token); - } else { - if (mWaitingFinishedCallbackActivities.contains(token)) { - invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, - /* sourceSpec= */ null, /* targetSpec= */ null, - packageName, translatedAppUid); - mWaitingFinishedCallbackActivities.remove(token); - } + mActiveTranslations.remove(token); } } @@ -237,6 +231,9 @@ final class TranslationManagerServiceImpl extends // Activity is the new Activity, the original Activity is paused in the same task. // To make sure the operation still work, we use the token to find the target Activity in // this task, not the top Activity only. + // + // Note: getAttachedNonFinishingActivityForTask() takes the shareable activity token. We + // call this method so that we can get the regular activity token below. ActivityTokens candidateActivityTokens = mActivityTaskManagerInternal.getAttachedNonFinishingActivityForTask(taskId, token); if (candidateActivityTokens == null) { @@ -263,27 +260,27 @@ final class TranslationManagerServiceImpl extends getAppUidByComponentName(getContext(), componentName, getUserId()); String packageName = componentName.getPackageName(); - invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, activityToken, + invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, token, translatedAppUid); - updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, activityToken, + updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, token, translatedAppUid); } @GuardedBy("mLock") private void updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec, - TranslationSpec targetSpec, String packageName, IBinder activityToken, + TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid) { // We keep track of active translations and their state so that we can: // 1. Trigger callbacks that are registered after translation has started. // See registerUiTranslationStateCallbackLocked(). // 2. NOT trigger callbacks when the state didn't change. // See invokeCallbacksIfNecessaryLocked(). - ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken); + ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); switch (state) { case STATE_UI_TRANSLATION_STARTED: { if (activeTranslation == null) { try { - activityToken.linkToDeath(this, /* flags= */ 0); + shareableActivityToken.linkToDeath(this, /* flags= */ 0); } catch (RemoteException e) { Slog.w(TAG, "Failed to call linkToDeath for translated app with uid=" + translatedAppUid + "; activity is already dead", e); @@ -294,7 +291,7 @@ final class TranslationManagerServiceImpl extends packageName, translatedAppUid); return; } - mActiveTranslations.put(activityToken, + mActiveTranslations.put(shareableActivityToken, new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid, packageName)); } @@ -317,7 +314,7 @@ final class TranslationManagerServiceImpl extends case STATE_UI_TRANSLATION_FINISHED: { if (activeTranslation != null) { - mActiveTranslations.remove(activityToken); + mActiveTranslations.remove(shareableActivityToken); } break; } @@ -332,12 +329,12 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec, - TranslationSpec targetSpec, String packageName, IBinder activityToken, + TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid) { boolean shouldInvokeCallbacks = true; int stateForCallbackInvocation = state; - ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken); + ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); if (activeTranslation == null) { if (state != STATE_UI_TRANSLATION_STARTED) { shouldInvokeCallbacks = false; @@ -403,14 +400,6 @@ final class TranslationManagerServiceImpl extends } } - if (DEBUG) { - Slog.d(TAG, - (shouldInvokeCallbacks ? "" : "NOT ") - + "Invoking callbacks for translation state=" - + stateForCallbackInvocation + " for app with uid=" + translatedAppUid - + " packageName=" + packageName); - } - if (shouldInvokeCallbacks) { invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName, translatedAppUid); @@ -448,7 +437,7 @@ final class TranslationManagerServiceImpl extends pw.println(waitingFinishCallbackSize); for (IBinder activityToken : mWaitingFinishedCallbackActivities) { pw.print(prefix); - pw.print("activityToken: "); + pw.print("shareableActivityToken: "); pw.println(activityToken); } } @@ -458,7 +447,14 @@ final class TranslationManagerServiceImpl extends int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, int translatedAppUid) { Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName); - if (mCallbacks.getRegisteredCallbackCount() == 0) { + int registeredCallbackCount = mCallbacks.getRegisteredCallbackCount(); + if (DEBUG) { + Slog.d(TAG, "Invoking " + registeredCallbackCount + " callbacks for translation state=" + + state + " for app with uid=" + translatedAppUid + + " packageName=" + packageName); + } + + if (registeredCallbackCount == 0) { return; } List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); @@ -521,8 +517,10 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) { mCallbacks.register(callback, sourceUid); - - if (mActiveTranslations.size() == 0) { + int numActiveTranslations = mActiveTranslations.size(); + Slog.i(TAG, "New registered callback for sourceUid=" + sourceUid + " with currently " + + numActiveTranslations + " active translations"); + if (numActiveTranslations == 0) { return; } |