diff options
86 files changed, 2713 insertions, 687 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index e23860c1eca3..d4a1cd234c39 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -19,6 +19,8 @@ package com.android.server.tare; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME; +import static com.android.server.tare.EconomicPolicy.REGULATION_BG_RESTRICTED; +import static com.android.server.tare.EconomicPolicy.REGULATION_BG_UNRESTRICTED; import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT; import static com.android.server.tare.EconomicPolicy.REGULATION_DEMOTION; import static com.android.server.tare.EconomicPolicy.REGULATION_PROMOTION; @@ -510,12 +512,12 @@ class Agent { } final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); final long originalBalance = ledger.getCurrentBalance(); + final long maxBalance = economicPolicy.getMaxSatiatedBalance(userId, pkgName); if (transaction.delta > 0 - && originalBalance + transaction.delta > economicPolicy.getMaxSatiatedBalance()) { + && originalBalance + transaction.delta > maxBalance) { // Set lower bound at 0 so we don't accidentally take away credits when we were trying // to _give_ the app credits. - final long newDelta = - Math.max(0, economicPolicy.getMaxSatiatedBalance() - originalBalance); + final long newDelta = Math.max(0, maxBalance - originalBalance); Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction " + eventToString(transaction.eventId) + (transaction.tag == null ? "" : ":" + transaction.tag) @@ -660,6 +662,47 @@ class Agent { } } + /** + * Reclaim all ARCs from an app that was just restricted. + */ + @GuardedBy("mLock") + void onAppRestrictedLocked(final int userId, @NonNull final String pkgName) { + final long curBalance = getBalanceLocked(userId, pkgName); + final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName); + if (curBalance <= minBalance) { + return; + } + if (DEBUG) { + Slog.i(TAG, "App restricted! Taking " + curBalance + + " from " + appToString(userId, pkgName)); + } + + final long now = getCurrentTimeMillis(); + final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); + recordTransactionLocked(userId, pkgName, ledger, + new Ledger.Transaction(now, now, REGULATION_BG_RESTRICTED, null, -curBalance, 0), + true); + } + + /** + * Give an app that was just unrestricted some ARCs. + */ + @GuardedBy("mLock") + void onAppUnrestrictedLocked(final int userId, @NonNull final String pkgName) { + final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); + if (ledger.getCurrentBalance() > 0) { + Slog.wtf(TAG, "App " + pkgName + " had credits while it was restricted"); + // App already got credits somehow. Move along. + return; + } + + final long now = getCurrentTimeMillis(); + + recordTransactionLocked(userId, pkgName, ledger, + new Ledger.Transaction(now, now, REGULATION_BG_UNRESTRICTED, null, + mIrs.getMinBalanceLocked(userId, pkgName), 0), true); + } + /** Returns true if an app should be given credits in the general distributions. */ private boolean shouldGiveCredits(@NonNull InstalledPackageInfo packageInfo) { // Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful. @@ -668,7 +711,8 @@ class Agent { } final int userId = UserHandle.getUserId(packageInfo.uid); // No point allocating ARCs to the system. It can do whatever it wants. - return !mIrs.isSystem(userId, packageInfo.packageName); + return !mIrs.isSystem(userId, packageInfo.packageName) + && !mIrs.isPackageRestricted(userId, packageInfo.packageName); } void onCreditSupplyChanged() { diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java index aa66e92a091f..e791e98a6698 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java @@ -170,6 +170,9 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { @Override long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) { + if (mIrs.isPackageRestricted(userId, pkgName)) { + return 0; + } if (mIrs.isPackageExempted(userId, pkgName)) { return mMinSatiatedBalanceExempted; } @@ -178,7 +181,10 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { } @Override - long getMaxSatiatedBalance() { + long getMaxSatiatedBalance(int userId, @NonNull String pkgName) { + if (mIrs.isPackageRestricted(userId, pkgName)) { + return 0; + } // TODO(230501287): adjust balance based on whether the app has the SCHEDULE_EXACT_ALARM // permission granted. Apps without the permission granted shouldn't need a high balance // since they won't be able to use exact alarms. Apps with the permission granted could diff --git a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java index 5d9cce84a7ac..625f99d64ef4 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java @@ -36,7 +36,6 @@ public class CompleteEconomicPolicy extends EconomicPolicy { /** Lazily populated set of rewards covered by this policy. */ private final SparseArray<Reward> mRewards = new SparseArray<>(); private final int[] mCostModifiers; - private long mMaxSatiatedBalance; private long mInitialConsumptionLimit; private long mHardConsumptionLimit; @@ -80,16 +79,13 @@ public class CompleteEconomicPolicy extends EconomicPolicy { } private void updateLimits() { - long maxSatiatedBalance = 0; long initialConsumptionLimit = 0; long hardConsumptionLimit = 0; for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) { final EconomicPolicy economicPolicy = mEnabledEconomicPolicies.valueAt(i); - maxSatiatedBalance += economicPolicy.getMaxSatiatedBalance(); initialConsumptionLimit += economicPolicy.getInitialSatiatedConsumptionLimit(); hardConsumptionLimit += economicPolicy.getHardSatiatedConsumptionLimit(); } - mMaxSatiatedBalance = maxSatiatedBalance; mInitialConsumptionLimit = initialConsumptionLimit; mHardConsumptionLimit = hardConsumptionLimit; } @@ -104,8 +100,12 @@ public class CompleteEconomicPolicy extends EconomicPolicy { } @Override - long getMaxSatiatedBalance() { - return mMaxSatiatedBalance; + long getMaxSatiatedBalance(int userId, @NonNull String pkgName) { + long max = 0; + for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) { + max += mEnabledEconomicPolicies.valueAt(i).getMaxSatiatedBalance(userId, pkgName); + } + return max; } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java index 564ffb9c4169..2fb0c1a36e07 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java @@ -67,6 +67,9 @@ public abstract class EconomicPolicy { static final int REGULATION_WEALTH_RECLAMATION = TYPE_REGULATION | 2; static final int REGULATION_PROMOTION = TYPE_REGULATION | 3; static final int REGULATION_DEMOTION = TYPE_REGULATION | 4; + /** App is fully restricted from running in the background. */ + static final int REGULATION_BG_RESTRICTED = TYPE_REGULATION | 5; + static final int REGULATION_BG_UNRESTRICTED = TYPE_REGULATION | 6; static final int REWARD_NOTIFICATION_SEEN = TYPE_REWARD | 0; static final int REWARD_NOTIFICATION_INTERACTION = TYPE_REWARD | 1; @@ -210,7 +213,7 @@ public abstract class EconomicPolicy { * exists to ensure that no single app accumulate all available resources and increases fairness * for all apps. */ - abstract long getMaxSatiatedBalance(); + abstract long getMaxSatiatedBalance(int userId, @NonNull String pkgName); /** * Returns the maximum number of cakes that should be consumed during a full 100% discharge diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index 7a7d669ae229..da5a0c0e1e3f 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -29,6 +29,7 @@ import static com.android.server.tare.TareUtils.getCurrentTimeMillis; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; +import android.app.AppOpsManager; import android.app.tare.IEconomyManager; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; @@ -64,6 +65,8 @@ import android.util.SparseArrayMap; import android.util.SparseSetArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.app.IAppOpsCallback; +import com.android.internal.app.IAppOpsService; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -119,6 +122,7 @@ public class InternalResourceService extends SystemService { private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; + private IAppOpsService mAppOpsService; private IDeviceIdleController mDeviceIdleController; private final Agent mAgent; @@ -145,6 +149,12 @@ public class InternalResourceService extends SystemService { private final CopyOnWriteArraySet<TareStateChangeListener> mStateChangeListeners = new CopyOnWriteArraySet<>(); + /** + * List of packages that are fully restricted and shouldn't be allowed to run in the background. + */ + @GuardedBy("mLock") + private final SparseSetArray<String> mRestrictedApps = new SparseSetArray<>(); + /** List of packages that are "exempted" from battery restrictions. */ // TODO(144864180): include userID @GuardedBy("mLock") @@ -160,6 +170,30 @@ public class InternalResourceService extends SystemService { @GuardedBy("mLock") private int mCurrentBatteryLevel; + private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() { + @Override + public void opChanged(int op, int uid, String packageName) { + boolean restricted = false; + try { + restricted = mAppOpsService.checkOperation( + AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName) + != AppOpsManager.MODE_ALLOWED; + } catch (RemoteException e) { + // Shouldn't happen + } + final int userId = UserHandle.getUserId(uid); + synchronized (mLock) { + if (restricted) { + if (mRestrictedApps.add(userId, packageName)) { + mAgent.onAppRestrictedLocked(userId, packageName); + } + } else if (mRestrictedApps.remove(UserHandle.getUserId(uid), packageName)) { + mAgent.onAppUnrestrictedLocked(userId, packageName); + } + } + } + }; + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Nullable private String getPackageName(Intent intent) { @@ -280,9 +314,11 @@ public class InternalResourceService extends SystemService { switch (phase) { case PHASE_SYSTEM_SERVICES_READY: - mConfigObserver.start(); + mAppOpsService = IAppOpsService.Stub.asInterface( + ServiceManager.getService(Context.APP_OPS_SERVICE)); mDeviceIdleController = IDeviceIdleController.Stub.asInterface( ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); + mConfigObserver.start(); onBootPhaseSystemServicesReady(); break; case PHASE_THIRD_PARTY_APPS_CAN_START: @@ -365,6 +401,12 @@ public class InternalResourceService extends SystemService { } } + boolean isPackageRestricted(final int userId, @NonNull String pkgName) { + synchronized (mLock) { + return mRestrictedApps.contains(userId, pkgName); + } + } + boolean isSystem(final int userId, @NonNull String pkgName) { if ("android".equals(pkgName)) { return true; @@ -711,6 +753,13 @@ public class InternalResourceService extends SystemService { UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class); usmi.registerListener(mSurveillanceAgent); + + try { + mAppOpsService + .startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null, mApbListener); + } catch (RemoteException e) { + // shouldn't happen. + } } /** Perform long-running and/or heavy setup work. This should be called off the main thread. */ @@ -815,6 +864,11 @@ public class InternalResourceService extends SystemService { UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class); usmi.unregisterListener(mSurveillanceAgent); + try { + mAppOpsService.stopWatchingMode(mApbListener); + } catch (RemoteException e) { + // shouldn't happen. + } } synchronized (mPackageToUidCache) { mPackageToUidCache.clear(); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java index 03c5fdd63250..cbb88c0f5e31 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java @@ -172,6 +172,9 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { @Override long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) { + if (mIrs.isPackageRestricted(userId, pkgName)) { + return 0; + } if (mIrs.isPackageExempted(userId, pkgName)) { return mMinSatiatedBalanceExempted; } @@ -180,7 +183,10 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { } @Override - long getMaxSatiatedBalance() { + long getMaxSatiatedBalance(int userId, @NonNull String pkgName) { + if (mIrs.isPackageRestricted(userId, pkgName)) { + return 0; + } return mMaxSatiatedBalance; } diff --git a/core/java/android/service/selectiontoolbar/DefaultSelectionToolbarRenderService.java b/core/java/android/service/selectiontoolbar/DefaultSelectionToolbarRenderService.java index f028ed3e6b00..ad73a53cfd87 100644 --- a/core/java/android/service/selectiontoolbar/DefaultSelectionToolbarRenderService.java +++ b/core/java/android/service/selectiontoolbar/DefaultSelectionToolbarRenderService.java @@ -69,7 +69,7 @@ public final class DefaultSelectionToolbarRenderService extends SelectionToolbar if (mToolbarCache.indexOfKey(callingUid) < 0) { RemoteSelectionToolbar toolbar = new RemoteSelectionToolbar(this, - widgetToken, showInfo.getHostInputToken(), + widgetToken, showInfo, callbackWrapper, this::transferTouch); mToolbarCache.put(callingUid, new Pair<>(widgetToken, toolbar)); } diff --git a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java index d75fbc0c64e0..95bcda5f7c55 100644 --- a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java +++ b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java @@ -22,7 +22,6 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.AnimatedVectorDrawable; @@ -162,15 +161,14 @@ final class RemoteSelectionToolbar { private final Rect mTempContentRectForRoot = new Rect(); private final int[] mTempCoords = new int[2]; - RemoteSelectionToolbar(Context context, long selectionToolbarToken, IBinder hostInputToken, + RemoteSelectionToolbar(Context context, long selectionToolbarToken, ShowInfo showInfo, SelectionToolbarRenderService.RemoteCallbackWrapper callbackWrapper, SelectionToolbarRenderService.TransferTouchListener transferTouchListener) { - mContext = applyDefaultTheme(context); + mContext = applyDefaultTheme(context, showInfo.isIsLightTheme()); mSelectionToolbarToken = selectionToolbarToken; mCallbackWrapper = callbackWrapper; mTransferTouchListener = transferTouchListener; - mHostInputToken = hostInputToken; - + mHostInputToken = showInfo.getHostInputToken(); mContentContainer = createContentContainer(mContext); mMarginHorizontal = mContext.getResources() .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); @@ -1359,12 +1357,9 @@ final class RemoteSelectionToolbar { /** * Returns a re-themed context with controlled look and feel for views. */ - private static Context applyDefaultTheme(Context originalContext) { - TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme}); - boolean isLightTheme = a.getBoolean(0, true); + private static Context applyDefaultTheme(Context originalContext, boolean isLightTheme) { int themeId = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault; - a.recycle(); return new ContextThemeWrapper(originalContext, themeId); } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 8f9c5fe2b87f..a66427843af0 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -723,9 +723,9 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private void releaseSurfaces(boolean releaseSurfacePackage) { mSurfaceAlpha = 1f; - mSurface.destroy(); synchronized (mSurfaceControlLock) { + mSurface.destroy(); if (mBlastBufferQueue != null) { mBlastBufferQueue.destroy(); mBlastBufferQueue = null; @@ -774,99 +774,105 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall Transaction surfaceUpdateTransaction) { boolean realSizeChanged = false; - mDrawingStopped = !mVisible; - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Cur surface: " + mSurface); + mSurfaceLock.lock(); + try { + mDrawingStopped = !mVisible; - // If we are creating the surface control or the parent surface has not - // changed, then set relative z. Otherwise allow the parent - // SurfaceChangedCallback to update the relative z. This is needed so that - // we do not change the relative z before the server is ready to swap the - // parent surface. - if (creating) { - updateRelativeZ(surfaceUpdateTransaction); - if (mSurfacePackage != null) { - reparentSurfacePackage(surfaceUpdateTransaction, mSurfacePackage); + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "Cur surface: " + mSurface); + + // If we are creating the surface control or the parent surface has not + // changed, then set relative z. Otherwise allow the parent + // SurfaceChangedCallback to update the relative z. This is needed so that + // we do not change the relative z before the server is ready to swap the + // parent surface. + if (creating) { + updateRelativeZ(surfaceUpdateTransaction); + if (mSurfacePackage != null) { + reparentSurfacePackage(surfaceUpdateTransaction, mSurfacePackage); + } } - } - mParentSurfaceSequenceId = viewRoot.getSurfaceSequenceId(); - - if (mViewVisibility) { - surfaceUpdateTransaction.show(mSurfaceControl); - } else { - surfaceUpdateTransaction.hide(mSurfaceControl); - } + mParentSurfaceSequenceId = viewRoot.getSurfaceSequenceId(); + if (mViewVisibility) { + surfaceUpdateTransaction.show(mSurfaceControl); + } else { + surfaceUpdateTransaction.hide(mSurfaceControl); + } - updateBackgroundVisibility(surfaceUpdateTransaction); - updateBackgroundColor(surfaceUpdateTransaction); - if (mUseAlpha) { - float alpha = getFixedAlpha(); - surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha); - mSurfaceAlpha = alpha; - } - surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius); - if ((sizeChanged || hintChanged) && !creating) { - setBufferSize(surfaceUpdateTransaction); - } - if (sizeChanged || creating || !isHardwareAccelerated()) { - // Set a window crop when creating the surface or changing its size to - // crop the buffer to the surface size since the buffer producer may - // use SCALING_MODE_SCALE and submit a larger size than the surface - // size. - if (mClipSurfaceToBounds && mClipBounds != null) { - surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mClipBounds); - } else { - surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth, - mSurfaceHeight); + updateBackgroundVisibility(surfaceUpdateTransaction); + updateBackgroundColor(surfaceUpdateTransaction); + if (mUseAlpha) { + float alpha = getFixedAlpha(); + surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha); + mSurfaceAlpha = alpha; } - surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth, - mSurfaceHeight); + surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius); + if ((sizeChanged || hintChanged) && !creating) { + setBufferSize(surfaceUpdateTransaction); + } + if (sizeChanged || creating || !isHardwareAccelerated()) { + + // Set a window crop when creating the surface or changing its size to + // crop the buffer to the surface size since the buffer producer may + // use SCALING_MODE_SCALE and submit a larger size than the surface + // size. + if (mClipSurfaceToBounds && mClipBounds != null) { + surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mClipBounds); + } else { + surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth, + mSurfaceHeight); + } - if (isHardwareAccelerated()) { - // This will consume the passed in transaction and the transaction will be - // applied on a render worker thread. - replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight); - } else { - onSetSurfacePositionAndScale(surfaceUpdateTransaction, mSurfaceControl, - mScreenRect.left /*positionLeft*/, - mScreenRect.top /*positionTop*/, - mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, - mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); + surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth, + mSurfaceHeight); + + if (isHardwareAccelerated()) { + // This will consume the passed in transaction and the transaction will be + // applied on a render worker thread. + replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight); + } else { + onSetSurfacePositionAndScale(surfaceUpdateTransaction, mSurfaceControl, + mScreenRect.left /*positionLeft*/, + mScreenRect.top /*positionTop*/, + mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, + mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); + } + if (DEBUG_POSITION) { + Log.d(TAG, String.format( + "%d performSurfaceTransaction %s " + + "position = [%d, %d, %d, %d] surfaceSize = %dx%d", + System.identityHashCode(this), + isHardwareAccelerated() ? "RenderWorker" : "UI Thread", + mScreenRect.left, mScreenRect.top, mScreenRect.right, + mScreenRect.bottom, mSurfaceWidth, mSurfaceHeight)); + } } - if (DEBUG_POSITION) { - Log.d(TAG, String.format( - "%d performSurfaceTransaction %s " - + "position = [%d, %d, %d, %d] surfaceSize = %dx%d", - System.identityHashCode(this), - isHardwareAccelerated() ? "RenderWorker" : "UI Thread", - mScreenRect.left, mScreenRect.top, mScreenRect.right, - mScreenRect.bottom, mSurfaceWidth, mSurfaceHeight)); + applyTransactionOnVriDraw(surfaceUpdateTransaction); + updateEmbeddedAccessibilityMatrix(false); + + mSurfaceFrame.left = 0; + mSurfaceFrame.top = 0; + if (translator == null) { + mSurfaceFrame.right = mSurfaceWidth; + mSurfaceFrame.bottom = mSurfaceHeight; + } else { + float appInvertedScale = translator.applicationInvertedScale; + mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); + mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); } + final int surfaceWidth = mSurfaceFrame.right; + final int surfaceHeight = mSurfaceFrame.bottom; + realSizeChanged = mLastSurfaceWidth != surfaceWidth + || mLastSurfaceHeight != surfaceHeight; + mLastSurfaceWidth = surfaceWidth; + mLastSurfaceHeight = surfaceHeight; + } finally { + mSurfaceLock.unlock(); } - applyTransactionOnVriDraw(surfaceUpdateTransaction); - updateEmbeddedAccessibilityMatrix(false); - mSurfaceFrame.left = 0; - mSurfaceFrame.top = 0; - if (translator == null) { - mSurfaceFrame.right = mSurfaceWidth; - mSurfaceFrame.bottom = mSurfaceHeight; - } else { - float appInvertedScale = translator.applicationInvertedScale; - mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); - mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); - } - final int surfaceWidth = mSurfaceFrame.right; - final int surfaceHeight = mSurfaceFrame.bottom; - realSizeChanged = mLastSurfaceWidth != surfaceWidth - || mLastSurfaceHeight != surfaceHeight; - mLastSurfaceWidth = surfaceWidth; - mLastSurfaceHeight = surfaceHeight; - return realSizeChanged; } @@ -1133,30 +1139,21 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * Surface for compatibility reasons. */ private void copySurface(boolean surfaceControlCreated, boolean bufferSizeChanged) { - // Some legacy applications use the underlying native {@link Surface} object - // as a key to whether anything has changed. In these cases, updates to the - // existing {@link Surface} will be ignored when the size changes. - // Therefore, we must explicitly recreate the {@link Surface} in these - // cases. - boolean needsWorkaround = bufferSizeChanged && - getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O; - if (!surfaceControlCreated && !needsWorkaround) { - return; - } - mSurfaceLock.lock(); - try { - if (surfaceControlCreated) { - mSurface.copyFrom(mBlastBufferQueue); - } - - if (needsWorkaround) { - if (mBlastBufferQueue != null) { - mSurface.transferFrom(mBlastBufferQueue.createSurfaceWithHandle()); - } - } - } finally { - mSurfaceLock.unlock(); - } + if (surfaceControlCreated) { + mSurface.copyFrom(mBlastBufferQueue); + } + + if (bufferSizeChanged && getContext().getApplicationInfo().targetSdkVersion + < Build.VERSION_CODES.O) { + // Some legacy applications use the underlying native {@link Surface} object + // as a key to whether anything has changed. In these cases, updates to the + // existing {@link Surface} will be ignored when the size changes. + // Therefore, we must explicitly recreate the {@link Surface} in these + // cases. + if (mBlastBufferQueue != null) { + mSurface.transferFrom(mBlastBufferQueue.createSurfaceWithHandle()); + } + } } private void setBufferSize(Transaction transaction) { diff --git a/core/java/android/view/selectiontoolbar/ShowInfo.java b/core/java/android/view/selectiontoolbar/ShowInfo.java index d9adef2c920b..28b4480d4967 100644 --- a/core/java/android/view/selectiontoolbar/ShowInfo.java +++ b/core/java/android/view/selectiontoolbar/ShowInfo.java @@ -75,6 +75,11 @@ public final class ShowInfo implements Parcelable { @NonNull private final IBinder mHostInputToken; + /** + * If the host application uses light theme. + */ + private final boolean mIsLightTheme; + // Code below generated by codegen v1.0.23. @@ -109,6 +114,8 @@ public final class ShowInfo implements Parcelable { * @param hostInputToken * The host application's input token, this allows the remote render service to transfer * the touch focus to the host application. + * @param isLightTheme + * If the host application uses light theme. */ @DataClass.Generated.Member public ShowInfo( @@ -118,7 +125,8 @@ public final class ShowInfo implements Parcelable { @NonNull Rect contentRect, int suggestedWidth, @NonNull Rect viewPortOnScreen, - @NonNull IBinder hostInputToken) { + @NonNull IBinder hostInputToken, + boolean isLightTheme) { this.mWidgetToken = widgetToken; this.mLayoutRequired = layoutRequired; this.mMenuItems = menuItems; @@ -134,6 +142,7 @@ public final class ShowInfo implements Parcelable { this.mHostInputToken = hostInputToken; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mHostInputToken); + this.mIsLightTheme = isLightTheme; // onConstructed(); // You can define this method to get a callback } @@ -196,6 +205,14 @@ public final class ShowInfo implements Parcelable { return mHostInputToken; } + /** + * If the host application uses light theme. + */ + @DataClass.Generated.Member + public boolean isIsLightTheme() { + return mIsLightTheme; + } + @Override @DataClass.Generated.Member public String toString() { @@ -209,7 +226,8 @@ public final class ShowInfo implements Parcelable { "contentRect = " + mContentRect + ", " + "suggestedWidth = " + mSuggestedWidth + ", " + "viewPortOnScreen = " + mViewPortOnScreen + ", " + - "hostInputToken = " + mHostInputToken + + "hostInputToken = " + mHostInputToken + ", " + + "isLightTheme = " + mIsLightTheme + " }"; } @@ -232,7 +250,8 @@ public final class ShowInfo implements Parcelable { && java.util.Objects.equals(mContentRect, that.mContentRect) && mSuggestedWidth == that.mSuggestedWidth && java.util.Objects.equals(mViewPortOnScreen, that.mViewPortOnScreen) - && java.util.Objects.equals(mHostInputToken, that.mHostInputToken); + && java.util.Objects.equals(mHostInputToken, that.mHostInputToken) + && mIsLightTheme == that.mIsLightTheme; } @Override @@ -249,6 +268,7 @@ public final class ShowInfo implements Parcelable { _hash = 31 * _hash + mSuggestedWidth; _hash = 31 * _hash + java.util.Objects.hashCode(mViewPortOnScreen); _hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken); + _hash = 31 * _hash + Boolean.hashCode(mIsLightTheme); return _hash; } @@ -258,9 +278,10 @@ public final class ShowInfo implements Parcelable { // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } - byte flg = 0; + int flg = 0; if (mLayoutRequired) flg |= 0x2; - dest.writeByte(flg); + if (mIsLightTheme) flg |= 0x80; + dest.writeInt(flg); dest.writeLong(mWidgetToken); dest.writeParcelableList(mMenuItems, flags); dest.writeTypedObject(mContentRect, flags); @@ -280,8 +301,9 @@ public final class ShowInfo implements Parcelable { // You can override field unparcelling by defining methods like: // static FieldType unparcelFieldName(Parcel in) { ... } - byte flg = in.readByte(); + int flg = in.readInt(); boolean layoutRequired = (flg & 0x2) != 0; + boolean isLightTheme = (flg & 0x80) != 0; long widgetToken = in.readLong(); List<ToolbarMenuItem> menuItems = new java.util.ArrayList<>(); in.readParcelableList(menuItems, ToolbarMenuItem.class.getClassLoader(), android.view.selectiontoolbar.ToolbarMenuItem.class); @@ -305,6 +327,7 @@ public final class ShowInfo implements Parcelable { this.mHostInputToken = hostInputToken; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mHostInputToken); + this.mIsLightTheme = isLightTheme; // onConstructed(); // You can define this method to get a callback } @@ -324,10 +347,10 @@ public final class ShowInfo implements Parcelable { }; @DataClass.Generated( - time = 1643186262604L, + time = 1645108384245L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/view/selectiontoolbar/ShowInfo.java", - inputSignatures = "private final long mWidgetToken\nprivate final boolean mLayoutRequired\nprivate final @android.annotation.NonNull java.util.List<android.view.selectiontoolbar.ToolbarMenuItem> mMenuItems\nprivate final @android.annotation.NonNull android.graphics.Rect mContentRect\nprivate final int mSuggestedWidth\nprivate final @android.annotation.NonNull android.graphics.Rect mViewPortOnScreen\nprivate final @android.annotation.NonNull android.os.IBinder mHostInputToken\nclass ShowInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") + inputSignatures = "private final long mWidgetToken\nprivate final boolean mLayoutRequired\nprivate final @android.annotation.NonNull java.util.List<android.view.selectiontoolbar.ToolbarMenuItem> mMenuItems\nprivate final @android.annotation.NonNull android.graphics.Rect mContentRect\nprivate final int mSuggestedWidth\nprivate final @android.annotation.NonNull android.graphics.Rect mViewPortOnScreen\nprivate final @android.annotation.NonNull android.os.IBinder mHostInputToken\nprivate final boolean mIsLightTheme\nclass ShowInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/translation/TranslationManager.java b/core/java/android/view/translation/TranslationManager.java index 55c0726f259a..5aad823c374e 100644 --- a/core/java/android/view/translation/TranslationManager.java +++ b/core/java/android/view/translation/TranslationManager.java @@ -40,11 +40,11 @@ import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.SyncResultReceiver; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Random; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; @@ -92,7 +92,8 @@ public final class TranslationManager { private final Map<Consumer<TranslationCapability>, IRemoteCallback> mCapabilityCallbacks = new ArrayMap<>(); - private static final Random ID_GENERATOR = new Random(); + // TODO(b/158778794): make the session ids truly globally unique across processes + private static final SecureRandom ID_GENERATOR = new SecureRandom(); private final Object mLock = new Object(); @NonNull diff --git a/core/java/com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java index 8c2eb1044e01..8787c39458b9 100644 --- a/core/java/com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java +++ b/core/java/com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -107,6 +108,7 @@ public final class RemoteFloatingToolbarPopup implements FloatingToolbarPopup { private int mSuggestedWidth; private final Rect mScreenViewPort = new Rect(); private boolean mWidthChanged = true; + private final boolean mIsLightTheme; private final int[] mCoordsOnScreen = new int[2]; private final int[] mCoordsOnWindow = new int[2]; @@ -116,9 +118,17 @@ public final class RemoteFloatingToolbarPopup implements FloatingToolbarPopup { mPopupWindow = createPopupWindow(context); mSelectionToolbarManager = context.getSystemService(SelectionToolbarManager.class); mSelectionToolbarCallback = new SelectionToolbarCallbackImpl(this); + mIsLightTheme = isLightTheme(context); mFloatingToolbarToken = NO_TOOLBAR_ID; } + private boolean isLightTheme(Context context) { + TypedArray a = context.obtainStyledAttributes(new int[]{R.attr.isLightTheme}); + boolean isLightTheme = a.getBoolean(0, true); + a.recycle(); + return isLightTheme; + } + @UiThread @Override public void show(List<MenuItem> menuItems, @@ -155,7 +165,7 @@ public final class RemoteFloatingToolbarPopup implements FloatingToolbarPopup { contentRect, suggestWidth, mScreenViewPort, - mParent.getViewRootImpl().getInputToken()); + mParent.getViewRootImpl().getInputToken(), mIsLightTheme); if (DEBUG) { Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG, "RemoteFloatingToolbarPopup.show() for " + showInfo); diff --git a/core/jni/OWNERS b/core/jni/OWNERS index ff97ab007efe..671e63493323 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -68,7 +68,7 @@ per-file com_android_internal_net_* = file:/services/core/java/com/android/serve ### Graphics ### per-file android_graphics_* = file:/graphics/java/android/graphics/OWNERS -per-file android_hardware_HardwareBuffer.cpp = file:/graphics/java/android/graphics/OWNERS +per-file *HardwareBuffer* = file:/graphics/java/android/graphics/OWNERS per-file android_hardware_SyncFence.cpp = file:/graphics/java/android/graphics/OWNERS per-file android_os_GraphicsEnvironment.cpp = file:platform/frameworks/native:/opengl/OWNERS diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index 2a625b027c17..25a1f68a0afe 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -86,7 +86,7 @@ message VibrationAttributesProto { optional int32 flags = 3; } -// Next id: 8 +// Next Tag: 9 message VibrationProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; optional int64 start_time = 1; @@ -94,11 +94,43 @@ message VibrationProto { optional CombinedVibrationEffectProto effect = 3; optional CombinedVibrationEffectProto original_effect = 4; optional VibrationAttributesProto attributes = 5; - optional int32 status = 6; optional int64 duration_ms = 7; + optional Status status = 8; + reserved 6; // prev int32 status + + // Also used by VibrationReported from frameworks/proto_logging/stats/atoms.proto. + // Next Tag: 26 + enum Status { + UNKNOWN = 0; + RUNNING = 1; + FINISHED = 2; + FINISHED_UNEXPECTED = 3; // Didn't terminate in the usual way. + FORWARDED_TO_INPUT_DEVICES = 4; + CANCELLED_BINDER_DIED = 5; + CANCELLED_BY_SCREEN_OFF = 6; + CANCELLED_BY_SETTINGS_UPDATE = 7; + CANCELLED_BY_USER = 8; + CANCELLED_BY_UNKNOWN_REASON = 9; + CANCELLED_SUPERSEDED = 10; + IGNORED_ERROR_APP_OPS = 11; + IGNORED_ERROR_CANCELLING = 12; + IGNORED_ERROR_SCHEDULING = 13; + IGNORED_ERROR_TOKEN= 14; + IGNORED_APP_OPS = 15; + IGNORED_BACKGROUND = 16; + IGNORED_UNKNOWN_VIBRATION = 17; + IGNORED_UNSUPPORTED = 18; + IGNORED_FOR_EXTERNAL = 19; + IGNORED_FOR_HIGHER_IMPORTANCE = 20; + IGNORED_FOR_ONGOING = 21; + IGNORED_FOR_POWER = 22; + IGNORED_FOR_RINGER_MODE = 23; + IGNORED_FOR_SETTINGS = 24; + IGNORED_SUPERSEDED = 25; + } } -// Next id: 25 +// Next Tag: 25 message VibratorManagerServiceDumpProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; repeated int32 vibrator_ids = 1; diff --git a/core/res/res/color/system_bar_background_semi_transparent.xml b/core/res/res/color/system_bar_background_semi_transparent.xml new file mode 100644 index 000000000000..839d58ac4bff --- /dev/null +++ b/core/res/res/color/system_bar_background_semi_transparent.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/system_neutral2_900" android:alpha="0.5" /> +</selector> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index ac083277b787..8b1b46d1b396 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -211,9 +211,6 @@ <color name="Red_700">#ffc53929</color> <color name="Red_800">#ffb93221</color> - <!-- Status bar color for semi transparent mode. --> - <color name="system_bar_background_semi_transparent">#66000000</color> <!-- 40% black --> - <color name="resize_shadow_start_color">#2a000000</color> <color name="resize_shadow_end_color">#00000000</color> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 8771ceb71d98..de26b54971ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1094,13 +1094,16 @@ public class BubbleController implements ConfigurationChangeListener { } void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) { + boolean showInShadeBefore = b.showInShade(); boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble()); boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected; b.setEntry(entry); boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade(); b.setSuppressNotification(suppress); b.setShowDot(!isBubbleExpandedAndSelected); - mImpl.mCachedState.updateBubbleSuppressedState(b); + if (showInShadeBefore != b.showInShade()) { + mImpl.mCachedState.updateBubbleSuppressedState(b); + } } @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index ff3c0834cf62..497a6f696df8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -105,6 +105,10 @@ public class DragLayout extends LinearLayout { MATCH_PARENT)); ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1; ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1; + int orientation = getResources().getConfiguration().orientation; + setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE + ? LinearLayout.HORIZONTAL + : LinearLayout.VERTICAL); updateContainerMargins(getResources().getConfiguration().orientation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 0e32663955d3..7096a645ef85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -111,9 +111,6 @@ public abstract class PipContentOverlay { private final TaskSnapshot mSnapshot; private final Rect mSourceRectHint; - private float mTaskSnapshotScaleX; - private float mTaskSnapshotScaleY; - public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { mSnapshot = snapshot; mSourceRectHint = new Rect(sourceRectHint); @@ -125,16 +122,16 @@ public abstract class PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { - mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x + final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x / mSnapshot.getHardwareBuffer().getWidth(); - mTaskSnapshotScaleY = (float) mSnapshot.getTaskSize().y + final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y / mSnapshot.getHardwareBuffer().getHeight(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer()); // Relocate the content to parentLeash's coordinates. tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top); - tx.setScale(mLeash, mTaskSnapshotScaleX, mTaskSnapshotScaleY); + tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY); tx.reparent(mLeash, parentLeash); tx.apply(); } @@ -146,20 +143,6 @@ public abstract class PipContentOverlay { @Override public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { - // Work around to make sure the snapshot overlay is aligned with PiP window before - // the atomicTx is committed along with the final WindowContainerTransaction. - final SurfaceControl.Transaction nonAtomicTx = new SurfaceControl.Transaction(); - final float scaleX = (float) destinationBounds.width() - / mSourceRectHint.width(); - final float scaleY = (float) destinationBounds.height() - / mSourceRectHint.height(); - final float scale = Math.max( - scaleX * mTaskSnapshotScaleX, scaleY * mTaskSnapshotScaleY); - nonAtomicTx.setScale(mLeash, scale, scale); - nonAtomicTx.setPosition(mLeash, - -scale * mSourceRectHint.left / mTaskSnapshotScaleX, - -scale * mSourceRectHint.top / mTaskSnapshotScaleY); - nonAtomicTx.apply(); atomicTx.remove(mLeash); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java index 681d9647d154..7fd03a9a306b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -56,7 +56,7 @@ public class SplitScreenShellCommandHandler implements return false; } final int taskId = new Integer(args[1]); - final int sideStagePosition = args.length > 3 + final int sideStagePosition = args.length > 2 ? new Integer(args[2]) : SPLIT_POSITION_BOTTOM_OR_RIGHT; mController.moveToSideStage(taskId, sideStagePosition); return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 4bc8e913ec4e..7e83d2fa0a0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1374,21 +1374,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } } else if (isSideStage && hasChildren && !mMainStage.isActive()) { - if (mFocusingTaskInfo != null && !isValidToEnterSplitScreen(mFocusingTaskInfo)) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mSideStage.removeAllTasks(wct, true); - wct.reorder(mRootTaskInfo.token, false /* onTop */); - mTaskOrganizer.applyTransaction(wct); - Slog.i(TAG, "cancel entering split screen, reason = " - + exitReasonToString(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW)); - } else { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mSplitLayout.init(); - prepareEnterSplitScreen(wct); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); - } + // TODO (b/238697912) : Add the validation to prevent entering non-recovered status + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSplitLayout.init(); + prepareEnterSplitScreen(wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 08eb2c9b6bbe..6c659667a4a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -64,6 +64,7 @@ import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityThread; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -203,14 +204,24 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @VisibleForTesting - static boolean isRotationSeamless(@NonNull TransitionInfo info, - DisplayController displayController) { + static int getRotationAnimationHint(@NonNull TransitionInfo.Change displayChange, + @NonNull TransitionInfo info, @NonNull DisplayController displayController) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Display is changing, check if it should be seamless."); - boolean checkedDisplayLayout = false; - boolean hasTask = false; - boolean displayExplicitSeamless = false; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { + "Display is changing, resolve the animation hint."); + // The explicit request of display has the highest priority. + if (displayChange.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " display requests explicit seamless"); + return ROTATION_ANIMATION_SEAMLESS; + } + + boolean allTasksSeamless = false; + boolean rejectSeamless = false; + ActivityManager.RunningTaskInfo topTaskInfo = null; + int animationHint = ROTATION_ANIMATION_ROTATE; + // Traverse in top-to-bottom order so that the first task is top-most. + final int size = info.getChanges().size(); + for (int i = 0; i < size; ++i) { final TransitionInfo.Change change = info.getChanges().get(i); // Only look at changing things. showing/hiding don't need to rotate. @@ -223,95 +234,69 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " display has system alert windows, so not seamless."); - return false; + rejectSeamless = true; } - displayExplicitSeamless = - change.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS; } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " wallpaper is participating but isn't seamless."); - return false; + rejectSeamless = true; } } else if (change.getTaskInfo() != null) { - hasTask = true; + final int anim = change.getRotationAnimation(); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + final boolean isTopTask = topTaskInfo == null; + if (isTopTask) { + topTaskInfo = taskInfo; + if (anim != ROTATION_ANIMATION_UNSPECIFIED + && anim != ROTATION_ANIMATION_SEAMLESS) { + animationHint = anim; + } + } // We only enable seamless rotation if all the visible task windows requested it. - if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + if (anim != ROTATION_ANIMATION_SEAMLESS) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " task %s isn't requesting seamless, so not seamless.", - change.getTaskInfo().taskId); - return false; - } - - // This is the only way to get display-id currently, so we will check display - // capabilities here - if (!checkedDisplayLayout) { - // only need to check display once. - checkedDisplayLayout = true; - final DisplayLayout displayLayout = displayController.getDisplayLayout( - change.getTaskInfo().displayId); - // For the upside down rotation we don't rotate seamlessly as the navigation - // bar moves position. Note most apps (using orientation:sensor or user as - // opposed to fullSensor) will not enter the reverse portrait orientation, so - // actually the orientation won't change at all. - int upsideDownRotation = displayLayout.getUpsideDownRotation(); - if (change.getStartRotation() == upsideDownRotation - || change.getEndRotation() == upsideDownRotation) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - " rotation involves upside-down portrait, so not seamless."); - return false; - } - - // If the navigation bar can't change sides, then it will jump when we change - // orientations and we don't rotate seamlessly - unless that is allowed, eg. - // with gesture navigation where the navbar is low-profile enough that this - // isn't very noticeable. - if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() - && (!(displayLayout.navigationBarCanMove() - && (change.getStartAbsBounds().width() - != change.getStartAbsBounds().height())))) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - " nav bar changes sides, so not seamless."); - return false; - } + taskInfo.taskId); + allTasksSeamless = false; + } else if (isTopTask) { + allTasksSeamless = true; } } } - // ROTATION_ANIMATION_SEAMLESS can only be requested by task or display. - if (hasTask || displayExplicitSeamless) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); - return true; + if (!allTasksSeamless || rejectSeamless) { + return animationHint; } - return false; - } - /** - * Gets the rotation animation for the topmost task. Assumes that seamless is checked - * elsewhere, so it will default SEAMLESS to ROTATE. - */ - private int getRotationAnimation(@NonNull TransitionInfo info) { - // Traverse in top-to-bottom order so that the first task is top-most - for (int i = 0; i < info.getChanges().size(); ++i) { - final TransitionInfo.Change change = info.getChanges().get(i); - - // Only look at changing things. showing/hiding don't need to rotate. - if (change.getMode() != TRANSIT_CHANGE) continue; - - // This container isn't rotating, so we can ignore it. - if (change.getEndRotation() == change.getStartRotation()) continue; + // This is the only way to get display-id currently, so check display capabilities here. + final DisplayLayout displayLayout = displayController.getDisplayLayout( + topTaskInfo.displayId); + // For the upside down rotation we don't rotate seamlessly as the navigation bar moves + // position. Note most apps (using orientation:sensor or user as opposed to fullSensor) + // will not enter the reverse portrait orientation, so actually the orientation won't + // change at all. + final int upsideDownRotation = displayLayout.getUpsideDownRotation(); + if (displayChange.getStartRotation() == upsideDownRotation + || displayChange.getEndRotation() == upsideDownRotation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " rotation involves upside-down portrait, so not seamless."); + return animationHint; + } - if (change.getTaskInfo() != null) { - final int anim = change.getRotationAnimation(); - if (anim == ROTATION_ANIMATION_UNSPECIFIED - // Fallback animation for seamless should also be default. - || anim == ROTATION_ANIMATION_SEAMLESS) { - return ROTATION_ANIMATION_ROTATE; - } - return anim; - } + // If the navigation bar can't change sides, then it will jump when we change orientations + // and we don't rotate seamlessly - unless that is allowed, e.g. with gesture navigation + // where the navbar is low-profile enough that this isn't very noticeable. + if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() + && (!(displayLayout.navigationBarCanMove() + && (displayChange.getStartAbsBounds().width() + != displayChange.getStartAbsBounds().height())))) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " nav bar changes sides, so not seamless."); + return animationHint; } - return ROTATION_ANIMATION_ROTATE; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return ROTATION_ANIMATION_SEAMLESS; } @Override @@ -354,8 +339,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { if (info.getType() == TRANSIT_CHANGE) { - isSeamlessDisplayChange = isRotationSeamless(info, mDisplayController); - final int anim = getRotationAnimation(info); + final int anim = getRotationAnimationHint(change, info, mDisplayController); + isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { startRotationAnimation(startTransaction, change, info, anim, animations, onAnimFinish); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index b142039e6aa9..c6492bee040e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; @@ -553,64 +554,77 @@ public class ShellTransitionTests extends ShellTestCase { final @Surface.Rotation int upsideDown = displays .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation(); + TransitionInfo.Change displayChange = new ChangeBuilder(TRANSIT_CHANGE) + .setFlags(FLAG_IS_DISPLAY).setRotate().build(); + // Set non-square display so nav bar won't be allowed to move. + displayChange.getStartAbsBounds().set(0, 0, 1000, 2000); final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, normalDispRotate, displays)); // Seamless if all tasks are seamless final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays)); + assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, rotateSeamless, displays)); // Not seamless if there is PiP (or any other non-seamless task) final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip) .setRotate().build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, pipDispRotate, displays)); + + // Not seamless if there is no changed task. + final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(displayChange) + .build(); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, noTask, displays)); // Not seamless if one of rotations is upside-down + displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build(); final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessUpsideDown, displays)); // Not seamless if system alert windows + displayChange = new ChangeBuilder(TRANSIT_CHANGE) + .setFlags(FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build(); final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags( - FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays)); - - // Not seamless if there is no changed task. - final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate().build()) - .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessButAlert, displays)); // Seamless if display is explicitly seamless. + displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build(); final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .addChange(displayChange) + // The animation hint of task will be ignored. + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_ROTATE).build()) .build(); - assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays)); + assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessDisplay, displays)); } @Test diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 7fbd10025d60..cd3242a9f7c2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -297,6 +297,9 @@ public class CachedBluetoothDeviceManager { mCachedDevices.remove(i); } } + + // To clear the SetMemberPair flag when the Bluetooth is turning off. + mOngoingSetMemberPair = null; } } 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 new file mode 100644 index 000000000000..a584894fed71 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -0,0 +1,72 @@ +/* + * 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.PsiElement +import com.intellij.psi.PsiField +import org.jetbrains.uast.UReferenceExpression + +@Suppress("UnstableApiUsage") +class SoftwareBitmapDetector : Detector(), SourceCodeScanner { + + override fun getApplicableReferenceNames(): List<String> { + return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + } + + override fun visitReference( + context: JavaContext, + reference: UReferenceExpression, + referenced: PsiElement + ) { + + val evaluator = context.evaluator + if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { + context.report( + ISSUE, + referenced, + context.getNameLocation(referenced), + "Usage of Config.HARDWARE is highly encouraged." + ) + } + } + + companion object { + @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.", + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = Implementation(SoftwareBitmapDetector::class.java, + Scope.JAVA_FILE_SCOPE) + ) + } +} 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 78c6d7267dba..c7c73d3c86a1 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 @@ -27,10 +27,12 @@ import com.google.auto.service.AutoService class SystemUIIssueRegistry : IssueRegistry() { override val issues: List<Issue> - get() = listOf(BindServiceViaContextDetector.ISSUE, + get() = listOf( + BindServiceViaContextDetector.ISSUE, BroadcastSentViaContextDetector.ISSUE, GetMainLooperViaContextDetector.ISSUE, - RegisterReceiverViaContextDetector.ISSUE + RegisterReceiverViaContextDetector.ISSUE, + SoftwareBitmapDetector.ISSUE, ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt new file mode 100644 index 000000000000..890f2b8eb924 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -0,0 +1,97 @@ +/* + * 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.TestFile +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 SoftwareBitmapDetectorTest : LintDetectorTest() { + + override fun getDetector(): Detector = SoftwareBitmapDetector() + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE) + + private val explanation = "Usage of Config.HARDWARE is highly encouraged." + + @Test + fun testSoftwareBitmap() { + lint().files( + TestFiles.java( + """ + import android.graphics.Bitmap; + + public class TestClass1 { + public void test() { + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + } + } + """ + ).indented(), + *stubs) + .issues(SoftwareBitmapDetector.ISSUE) + .run() + .expectWarningCount(2) + .expectContains(explanation) + } + + @Test + fun testHardwareBitmap() { + lint().files( + TestFiles.java( + """ + import android.graphics.Bitmap; + + public class TestClass1 { + public void test() { + Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE); + } + } + """ + ).indented(), + *stubs) + .issues(SoftwareBitmapDetector.ISSUE) + .run() + .expectWarningCount(0) + } + + private val bitmapStub: TestFile = java( + """ + package android.graphics; + + public class Bitmap { + public enum Config { + ARGB_8888, + RGB_565, + HARDWARE + } + public static Bitmap createBitmap(int width, int height, Config config) { + return null; + } + } + """ + ) + + private val stubs = arrayOf(bitmapStub) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 87579040372f..00b0ff9b128d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -68,7 +68,7 @@ class MediaTttCommandLineHelper @Inject constructor( .addFeature("feature") val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false") if (useAppIcon) { - routeInfo.setPackageName(TEST_PACKAGE_NAME) + routeInfo.setClientPackageName(TEST_PACKAGE_NAME) } statusBarManager.updateMediaTapToTransferSenderDisplay( @@ -134,7 +134,7 @@ class MediaTttCommandLineHelper @Inject constructor( .addFeature("feature") val useAppIcon = !(args.size >= 2 && args[1] == "useAppIcon=false") if (useAppIcon) { - routeInfo.setPackageName(TEST_PACKAGE_NAME) + routeInfo.setClientPackageName(TEST_PACKAGE_NAME) } statusBarManager.updateMediaTapToTransferReceiverDisplay( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 0f1ae00ae8fc..196ea222e50d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -143,7 +143,7 @@ class MediaTttChipControllerReceiver @Inject constructor( super.updateChipView(newChipInfo, currentChipView) setIcon( currentChipView, - newChipInfo.routeInfo.packageName, + newChipInfo.routeInfo.clientPackageName, newChipInfo.appIconDrawableOverride, newChipInfo.appNameOverride ) diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index b94b8bfabfc1..92d9ea89fce2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -122,7 +122,7 @@ class MediaTttChipControllerSender @Inject constructor( val chipState = newChipInfo.state // App icon - val iconName = setIcon(currentChipView, newChipInfo.routeInfo.packageName) + val iconName = setIcon(currentChipView, newChipInfo.routeInfo.clientPackageName) // Text val otherDeviceName = newChipInfo.routeInfo.name.toString() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index 0b6b929cd93c..c956a2ea1836 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.notification.interruption import android.app.Notification +import android.app.Notification.VISIBILITY_SECRET import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -172,6 +173,8 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( !lockscreenUserManager.shouldShowLockscreenNotifications() -> true // User settings do not allow this notification on the lockscreen, so hide it. userSettingsDisallowNotification(entry) -> true + // Entry is explicitly marked SECRET, so hide it. + entry.sbn.notification.visibility == VISIBILITY_SECRET -> true // if entry is silent, apply custom logic to see if should hide shouldHideIfEntrySilent(entry) -> true else -> false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FeedbackInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FeedbackInfo.java index 2b782b6e3917..3f4fd5006408 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FeedbackInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FeedbackInfo.java @@ -165,7 +165,7 @@ public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsC } private void positiveFeedback(View v) { - mGutsContainer.closeControls(v, false); + mGutsContainer.closeControls(v, /* save= */ false); handleFeedback(true); } @@ -176,7 +176,7 @@ public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsC menuItem = mMenuRowPlugin.getLongpressMenuItem(mContext); } - mGutsContainer.closeControls(v, false); + mGutsContainer.closeControls(v, /* save= */ false); mNotificationGutsManager.openGuts(mExpandableNotificationRow, 0, 0, menuItem); handleFeedback(false); } @@ -203,7 +203,7 @@ public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsC } private void closeControls(View v) { - mGutsContainer.closeControls(v, false); + mGutsContainer.closeControls(v, /* save= */ false); } @Override @@ -232,7 +232,7 @@ public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsC } @Override - public boolean shouldBeSaved() { + public boolean shouldBeSavedOnClose() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 7120fe50adb4..0ce9656a21b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -157,7 +157,7 @@ public class NotificationConversationInfo extends LinearLayout implements mShadeController.animateCollapsePanels(); mPeopleSpaceWidgetManager.requestPinAppWidget(mShortcutInfo, new Bundle()); } - mGutsContainer.closeControls(v, true); + mGutsContainer.closeControls(v, /* save= */ true); }; public NotificationConversationInfo(Context context, AttributeSet attrs) { @@ -186,7 +186,6 @@ public class NotificationConversationInfo extends LinearLayout implements } public void bindNotification( - @Action int selectedAction, ShortcutManager shortcutManager, PackageManager pm, PeopleSpaceWidgetManager peopleSpaceWidgetManager, @@ -205,8 +204,6 @@ public class NotificationConversationInfo extends LinearLayout implements OnConversationSettingsClickListener onConversationSettingsClickListener, Optional<BubblesManager> bubblesManagerOptional, ShadeController shadeController) { - mPressedApply = false; - mSelectedAction = selectedAction; mINotificationManager = iNotificationManager; mPeopleSpaceWidgetManager = peopleSpaceWidgetManager; mOnUserInteractionCallback = onUserInteractionCallback; @@ -417,9 +414,7 @@ public class NotificationConversationInfo extends LinearLayout implements } @Override - public void onFinishedClosing() { - mSelectedAction = -1; - } + public void onFinishedClosing() { } @Override public boolean needsFalsingProtection() { @@ -564,7 +559,7 @@ public class NotificationConversationInfo extends LinearLayout implements } @Override - public boolean shouldBeSaved() { + public boolean shouldBeSavedOnClose() { return mPressedApply; } @@ -578,6 +573,12 @@ public class NotificationConversationInfo extends LinearLayout implements if (save && mSelectedAction > -1) { updateChannel(); } + + // Clear the selected importance when closing, so when when we open again, + // we starts from a clean state. + mSelectedAction = -1; + mPressedApply = false; + return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java index fc296e125794..93f08123ab5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java @@ -76,7 +76,7 @@ public class NotificationGuts extends FrameLayout { switch (action) { case AccessibilityNodeInfo.ACTION_LONG_CLICK: - closeControls(host, false); + closeControls(host, /* save= */ false); return true; } @@ -123,7 +123,7 @@ public class NotificationGuts extends FrameLayout { /** * Return whether something changed and needs to be saved, possibly requiring a bouncer. */ - boolean shouldBeSaved(); + boolean shouldBeSavedOnClose(); /** * Called when the guts view has finished its close animation. @@ -259,7 +259,7 @@ public class NotificationGuts extends FrameLayout { if (mGutsContent != null) { if ((mGutsContent.isLeavebehind() && leavebehinds) || (!mGutsContent.isLeavebehind() && controls)) { - closeControls(x, y, mGutsContent.shouldBeSaved(), force); + closeControls(x, y, mGutsContent.shouldBeSavedOnClose(), force); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 7b0b0ce3a691..ea12b8263fed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -463,7 +463,6 @@ public class NotificationGutsManager implements NotifGutsViewManager { R.dimen.notification_guts_conversation_icon_size)); notificationInfoView.bindNotification( - notificationInfoView.getSelectedAction(), mShortcutManager, pmUser, mPeopleSpaceWidgetManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index 8b01a4790f3c..ea0060a693b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -158,7 +158,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G // used by standard ui private OnClickListener mOnDismissSettings = v -> { mPressedApply = true; - mGutsContainer.closeControls(v, true); + mGutsContainer.closeControls(v, /* save= */ true); }; public NotificationInfo(Context context, AttributeSet attrs) { @@ -541,10 +541,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G @Override public void onFinishedClosing() { - if (mChosenImportance != null) { - mStartingChannelImportance = mChosenImportance; - } - bindInlineControls(); logUiEvent(NotificationControlsEvent.NOTIFICATION_CONTROLS_CLOSE); @@ -604,7 +600,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G } @Override - public boolean shouldBeSaved() { + public boolean shouldBeSavedOnClose() { return mPressedApply; } @@ -627,6 +623,12 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G if (save) { saveImportance(); } + + // Clear the selected importance when closing, so when when we open again, + // we starts from a clean state. + mChosenImportance = null; + mPressedApply = false; + return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java index 512b04968166..adbfa755b63c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java @@ -384,7 +384,7 @@ public class NotificationSnooze extends LinearLayout private void undoSnooze(View v) { mSelectedOption = null; showSnoozeOptions(false); - mGutsContainer.closeControls(v, false); + mGutsContainer.closeControls(v, /* save= */ false); } @Override @@ -433,7 +433,7 @@ public class NotificationSnooze extends LinearLayout } @Override - public boolean shouldBeSaved() { + public boolean shouldBeSavedOnClose() { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java index 186ffa67f046..ac97e77f84a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java @@ -16,22 +16,13 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.app.INotificationManager; -import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.os.Parcelable; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.text.TextUtils; @@ -46,8 +37,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import java.lang.annotation.Retention; -import java.util.List; import java.util.Set; /** @@ -71,8 +60,6 @@ public class PartialConversationInfo extends LinearLayout implements private Set<NotificationChannel> mUniqueChannelsInRow; private Drawable mPkgIcon; - private @Action int mSelectedAction = -1; - private boolean mPressedApply; private boolean mPresentingChannelEditorDialog = false; private NotificationInfo.OnSettingsClickListener mOnSettingsClickListener; @@ -82,14 +69,8 @@ public class PartialConversationInfo extends LinearLayout implements @VisibleForTesting boolean mSkipPost = false; - @Retention(SOURCE) - @IntDef({ACTION_SETTINGS}) - private @interface Action {} - static final int ACTION_SETTINGS = 5; - private OnClickListener mOnDone = v -> { - mPressedApply = true; - mGutsContainer.closeControls(v, true); + mGutsContainer.closeControls(v, /* save= */ false); }; public PartialConversationInfo(Context context, AttributeSet attrs) { @@ -107,7 +88,6 @@ public class PartialConversationInfo extends LinearLayout implements NotificationInfo.OnSettingsClickListener onSettingsClick, boolean isDeviceProvisioned, boolean isNonBlockable) { - mSelectedAction = -1; mINotificationManager = iNotificationManager; mPackageName = pkg; mSbn = entry.getSbn(); @@ -286,8 +266,8 @@ public class PartialConversationInfo extends LinearLayout implements } @Override - public boolean shouldBeSaved() { - return mPressedApply; + public boolean shouldBeSavedOnClose() { + return false; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index dbc5f7c7041e..171d893640d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -241,5 +241,5 @@ private const val PACKAGE_NAME = "com.android.systemui" private val routeInfo = MediaRoute2Info.Builder("id", "Test route name") .addFeature("feature") - .setPackageName(PACKAGE_NAME) + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index cd8ee732e113..1061e3c6b0d5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -686,5 +686,5 @@ private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") - .setPackageName(PACKAGE_NAME) + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java index 541749b49474..d59cc54dfe98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.interruption; +import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_PUBLIC; import static android.app.Notification.VISIBILITY_SECRET; import static android.app.NotificationManager.IMPORTANCE_HIGH; @@ -449,6 +450,54 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test + public void notificationVisibilityPublic() { + // GIVEN a VISIBILITY_PUBLIC notification + NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)); + entryBuilder.modifyNotification(mContext) + .setVisibility(VISIBILITY_PUBLIC); + mEntry = entryBuilder.build(); + + // WHEN we're in an 'unfiltered-keyguard-showing' state + setupUnfilteredState(mEntry); + + // THEN don't hide the entry based on visibility. + assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void notificationVisibilityPrivate() { + // GIVEN a VISIBILITY_PRIVATE notification + NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)); + entryBuilder.modifyNotification(mContext) + .setVisibility(VISIBILITY_PRIVATE); + mEntry = entryBuilder.build(); + + // WHEN we're in an 'unfiltered-keyguard-showing' state + setupUnfilteredState(mEntry); + + // THEN don't hide the entry based on visibility. (Redaction is handled elsewhere.) + assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void notificationVisibilitySecret() { + // GIVEN a VISIBILITY_SECRET notification + NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)); + entryBuilder.modifyNotification(mContext) + .setVisibility(VISIBILITY_SECRET); + mEntry = entryBuilder.build(); + + // WHEN we're in an 'unfiltered-keyguard-showing' state + setupUnfilteredState(mEntry); + + // THEN hide the entry based on visibility. + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test public void summaryExceedsThresholdToShow() { // GIVEN the notification doesn't exceed the threshold to show on the lockscreen // but it's part of a group (has a parent) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 381d72f53d5f..90adabfadd5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -236,7 +236,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Test public void testBindNotification_SetsShortcutIcon() { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -261,7 +260,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { public void testBindNotification_SetsTextApplicationName() { when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name"); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -314,7 +312,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setGroup(group.getId()); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -340,7 +337,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Test public void testBindNotification_GroupNameHiddenIfNoGroup() { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -365,7 +361,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Test public void testBindNotification_noDelegate() { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -401,7 +396,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { .setShortcutInfo(mShortcutInfo) .build(); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -427,7 +421,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { public void testBindNotification_SetsOnClickListenerForSettings() { final CountDownLatch latch = new CountDownLatch(1); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -457,7 +450,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Test public void testBindNotification_SettingsButtonInvisibleWhenNoClickListener() { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -482,7 +474,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { public void testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() { final CountDownLatch latch = new CountDownLatch(1); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -511,7 +502,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportance(IMPORTANCE_LOW); mConversationChannel.setImportantConversation(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -540,7 +530,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mConversationChannel.setAllowBubbles(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -572,7 +561,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mConversationChannel.setAllowBubbles(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -610,7 +598,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mConversationChannel.setAllowBubbles(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -639,7 +626,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mConversationChannel.setAllowBubbles(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -675,7 +661,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mConversationChannel.setAllowBubbles(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -704,7 +689,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -735,7 +719,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { .isEqualTo(GONE); // no changes until hit done - assertFalse(mNotificationInfo.shouldBeSaved()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( anyString(), anyInt(), any()); assertFalse(mConversationChannel.isImportantConversation()); @@ -749,7 +733,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportance(IMPORTANCE_LOW); mConversationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -779,7 +762,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { .isEqualTo(GONE); // no changes until hit done - assertFalse(mNotificationInfo.shouldBeSaved()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( anyString(), anyInt(), any()); assertFalse(mConversationChannel.isImportantConversation()); @@ -793,7 +776,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -825,7 +807,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { .isEqualTo(VISIBLE); // no changes until save - assertFalse(mNotificationInfo.shouldBeSaved()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( anyString(), anyInt(), any()); assertEquals(IMPORTANCE_DEFAULT, mConversationChannel.getImportance()); @@ -838,7 +820,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -868,6 +849,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { assertTrue(captor.getValue().isImportantConversation()); assertTrue(captor.getValue().canBubble()); assertEquals(IMPORTANCE_DEFAULT, captor.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -876,7 +858,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportance(9); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -913,7 +894,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -954,7 +934,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { // WHEN we indicate no selected action mNotificationInfo.bindNotification( - -1, // no action selected by default mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -984,8 +963,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(false); // WHEN we indicate the selected action should be "Favorite" + mNotificationInfo.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE); mNotificationInfo.bindNotification( - NotificationConversationInfo.ACTION_FAVORITE, // "Favorite" selected by default mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1015,7 +994,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setOriginalImportance(IMPORTANCE_HIGH); mConversationChannel.setImportantConversation(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1044,6 +1022,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { assertFalse(captor.getValue().isImportantConversation()); assertFalse(captor.getValue().canBubble()); assertEquals(IMPORTANCE_HIGH, captor.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1052,7 +1031,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setOriginalImportance(IMPORTANCE_HIGH); mConversationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1089,7 +1067,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setOriginalImportance(IMPORTANCE_HIGH); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1125,7 +1102,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setAllowBubbles(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1155,12 +1131,46 @@ public class NotificationConversationInfoTest extends SysuiTestCase { assertFalse(captor.getValue().isImportantConversation()); assertFalse(captor.getValue().canBubble()); assertEquals(IMPORTANCE_LOW, captor.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); + } + + @Test + public void testSilence_closeGutsThenTryToSave() { + mConversationChannel.setImportance(IMPORTANCE_DEFAULT); + mConversationChannel.setImportantConversation(true); + mConversationChannel.setAllowBubbles(true); + + mNotificationInfo.bindNotification( + mShortcutManager, + mMockPackageManager, + mPeopleSpaceWidgetManager, + mMockINotificationManager, + mOnUserInteractionCallback, + TEST_PACKAGE_NAME, + mNotificationChannel, + mEntry, + mBubbleMetadata, + null, + mIconFactory, + mContext, + true, + mTestHandler, + mTestHandler, null, Optional.of(mBubblesManager), + mShadeController); + + mNotificationInfo.findViewById(R.id.silence).performClick(); + mNotificationInfo.handleCloseControls(false, false); + mNotificationInfo.findViewById(R.id.done).performClick(); + + mTestableLooper.processAllMessages(); + + assertEquals(IMPORTANCE_DEFAULT, mConversationChannel.getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test public void testBindNotification_createsNewChannel() throws Exception { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1186,7 +1196,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { public void testBindNotification_doesNotCreateNewChannelIfExists() throws Exception { mNotificationChannel.setConversationId("", CONVERSATION_ID); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1213,7 +1222,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { //WHEN channel is default importance mNotificationChannel.setImportantConversation(false); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1244,7 +1252,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { @Test public void testSelectDefaultDoesNotRequestPinPeopleTile() { mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, @@ -1279,7 +1286,6 @@ public class NotificationConversationInfoTest extends SysuiTestCase { mConversationChannel.setImportantConversation(true); mNotificationInfo.bindNotification( - -1, mShortcutManager, mMockPackageManager, mPeopleSpaceWidgetManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt new file mode 100644 index 000000000000..e696c8738d72 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt @@ -0,0 +1,103 @@ +/* + * 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.statusbar.notification.row + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.testing.ViewUtils +import android.view.LayoutInflater +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationGutsTest : SysuiTestCase() { + + private lateinit var guts: NotificationGuts + private lateinit var gutsContentView: View + + @Mock + private lateinit var gutsContent: NotificationGuts.GutsContent + + @Mock + private lateinit var gutsClosedListener: NotificationGuts.OnGutsClosedListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + val layoutInflater = LayoutInflater.from(mContext) + guts = layoutInflater.inflate(R.layout.notification_guts, null) as NotificationGuts + gutsContentView = View(mContext) + + whenever(gutsContent.contentView).thenReturn(gutsContentView) + + ViewUtils.attachView(guts) + } + + @After + fun tearDown() { + ViewUtils.detachView(guts) + } + + @Test + fun setGutsContent() { + guts.gutsContent = gutsContent + + verify(gutsContent).setGutsParent(guts) + } + + @Test + fun openControls() { + guts.gutsContent = gutsContent + + guts.openControls(true, 0, 0, false, null) + } + + @Test + fun closeControlsWithSave() { + guts.gutsContent = gutsContent + guts.setClosedListener(gutsClosedListener) + + guts.closeControls(gutsContentView, true) + + verify(gutsContent).handleCloseControls(true, false) + verify(gutsClosedListener).onGutsClosed(guts) + } + + @Test + fun closeControlsWithoutSave() { + guts.gutsContent = gutsContent + guts.setClosedListener(gutsClosedListener) + + guts.closeControls(gutsContentView, false) + + verify(gutsContent).handleCloseControls(false, false) + verify(gutsClosedListener).onGutsClosed(guts) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java index b1f10751119e..80a81a592049 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java @@ -50,6 +50,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.telecom.TelecomManager; @@ -1090,6 +1091,7 @@ public class NotificationInfoTest extends SysuiTestCase { mUiEventLogger.eventId(0)); assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(), mUiEventLogger.eventId(1)); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1124,6 +1126,7 @@ public class NotificationInfoTest extends SysuiTestCase { assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1156,6 +1159,7 @@ public class NotificationInfoTest extends SysuiTestCase { verify(mMockINotificationManager, times(1)).unlockNotificationChannel( anyString(), eq(TEST_UID), any()); assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1191,6 +1195,7 @@ public class NotificationInfoTest extends SysuiTestCase { assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1229,6 +1234,37 @@ public class NotificationInfoTest extends SysuiTestCase { anyString(), eq(TEST_UID), updated.capture()); assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); assertEquals(IMPORTANCE_MIN, updated.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); + } + + @Test + public void testSilence_closeGutsThenTryToSave() throws RemoteException { + mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); + mNotificationInfo.bindNotification( + mMockPackageManager, + mMockINotificationManager, + mOnUserInteractionCallback, + mChannelEditorDialogController, + TEST_PACKAGE_NAME, + mNotificationChannel, + mNotificationChannelSet, + mEntry, + null, + null, + mUiEventLogger, + true, + false, + false, + mAssistantFeedbackController); + + mNotificationInfo.findViewById(R.id.silence).performClick(); + mNotificationInfo.handleCloseControls(false, false); + mNotificationInfo.handleCloseControls(true, false); + + mTestableLooper.processAllMessages(); + + assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1267,6 +1303,7 @@ public class NotificationInfoTest extends SysuiTestCase { anyString(), eq(TEST_UID), updated.capture()); assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1294,6 +1331,7 @@ public class NotificationInfoTest extends SysuiTestCase { mNotificationInfo.handleCloseControls(true, false); verify(mOnUserInteractionCallback).onImportanceChanged(mEntry); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1360,6 +1398,7 @@ public class NotificationInfoTest extends SysuiTestCase { assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test @@ -1450,7 +1489,7 @@ public class NotificationInfoTest extends SysuiTestCase { mNotificationInfo.findViewById(R.id.alert).performClick(); - assertFalse(mNotificationInfo.shouldBeSaved()); + assertFalse(mNotificationInfo.shouldBeSavedOnClose()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java index 43aa8fef76a9..12c8fd5db751 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME; import static android.view.View.GONE; @@ -25,7 +24,6 @@ import static android.view.View.VISIBLE; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; @@ -36,8 +34,6 @@ import static org.mockito.Mockito.when; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; -import android.app.Person; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; diff --git a/proto/src/OWNERS b/proto/src/OWNERS index b456ba60d086..abd08deced79 100644 --- a/proto/src/OWNERS +++ b/proto/src/OWNERS @@ -1,3 +1,4 @@ per-file gnss.proto = file:/services/core/java/com/android/server/location/OWNERS per-file wifi.proto = file:/wifi/OWNERS per-file camera.proto = file:/services/core/java/com/android/server/camera/OWNERS +per-file system_messages.proto = file:/core/res/OWNERS diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index b3aff65afb1f..eba9c7a5f14e 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1915,7 +1915,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return null; } - UUID getDeviceSensorUuid(AudioDeviceAttributes device) { + @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) { synchronized (mDeviceStateLock) { return mDeviceInventory.getDeviceSensorUuid(device); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 1312d082a832..25211c862376 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -16,6 +16,7 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; @@ -1511,7 +1512,7 @@ public class AudioDeviceInventory { mDevRoleCapturePresetDispatchers.finishBroadcast(); } - UUID getDeviceSensorUuid(AudioDeviceAttributes device) { + @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) { final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(), device.getAddress()); synchronized (mDevicesLock) { diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index e27fb1141850..8356134bc63b 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -353,6 +353,14 @@ public class SpatializerHelper { mASA.getDevicesForAttributes( DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES); + // check validity of routing information + if (ROUTING_DEVICES[0] == null) { + logloge("onRoutingUpdated: device is null, no Spatial Audio"); + setDispatchAvailableState(false); + // not changing the spatializer level as this is likely a transient state + return; + } + // is media routed to a new device? if (isWireless(ROUTING_DEVICES[0].getType())) { addWirelessDeviceIfNew(ROUTING_DEVICES[0]); @@ -1098,7 +1106,7 @@ public class SpatializerHelper { logDeviceState(deviceState, "setHeadTrackerEnabled"); // check current routing to see if it affects the headtracking mode - if (ROUTING_DEVICES[0].getType() == ada.getType() + if (ROUTING_DEVICES[0] != null && ROUTING_DEVICES[0].getType() == ada.getType() && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) { setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled : Spatializer.HEAD_TRACKING_MODE_DISABLED); @@ -1633,7 +1641,11 @@ public class SpatializerHelper { private int getHeadSensorHandleUpdateTracker() { int headHandle = -1; - UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(ROUTING_DEVICES[0]); + final AudioDeviceAttributes currentDevice = ROUTING_DEVICES[0]; + if (currentDevice == null) { + return headHandle; + } + UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice); // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by @@ -1644,7 +1656,7 @@ public class SpatializerHelper { final UUID uuid = sensor.getUuid(); if (uuid.equals(routingDeviceUuid)) { headHandle = sensor.getHandle(); - if (!setHasHeadTracker(ROUTING_DEVICES[0])) { + if (!setHasHeadTracker(currentDevice)) { headHandle = -1; } break; diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a0335e89ceec..5b4468ff5108 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -4209,9 +4209,9 @@ public class UserManagerService extends IUserManager.Stub { UserManager.USER_OPERATION_ERROR_MAX_USERS); } // Keep logic in sync with getRemainingCreatableUserCount() - if (!isGuest && !isManagedProfile && !isDemo && isUserLimitReached()) { + if (!isGuest && !isProfile && !isDemo && isUserLimitReached()) { // If the user limit has been reached, we cannot add a user (except guest/demo). - // Note that managed profiles can bypass it in certain circumstances (taken + // Note that profiles can bypass it in certain circumstances (taken // into account in the profile check below). throwCheckedUserOperationException( "Cannot add user. Maximum user limit is reached.", diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java index 4b0a8e2778c0..466c4c9a31d6 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java @@ -103,7 +103,6 @@ public class DomainVerificationShell { pw.println(" <DOMAINS>: space separated list of domains to change, or \"all\" to"); pw.println(" change every domain."); pw.println(" set-app-links-allowed --user <USER_ID> [--package <PACKAGE>] <ALLOWED>"); - pw.println(" <ENABLED> <DOMAINS>..."); pw.println(" Toggle the auto verified link handling setting for a package."); pw.println(" --user <USER_ID>: the user to change selections for"); pw.println(" --package <PACKAGE>: the package to set, or \"all\" to set all packages"); diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java index 12e68b10c3df..eebd046b2601 100644 --- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java @@ -96,6 +96,7 @@ abstract class AbstractVibratorStep extends Step { "Turning off vibrator " + getVibratorId()); } controller.off(); + getVibration().stats().reportVibratorOff(); } protected void changeAmplitude(float amplitude) { @@ -104,6 +105,7 @@ abstract class AbstractVibratorStep extends Step { "Amplitude changed on vibrator " + getVibratorId() + " to " + amplitude); } controller.setAmplitude(amplitude); + getVibration().stats().reportSetAmplitude(); } /** @@ -147,6 +149,8 @@ abstract class AbstractVibratorStep extends Step { if (nextSegmentIndex >= effectSize && repeatIndex >= 0) { // Count the loops that were played. int loopSize = effectSize - repeatIndex; + int loopSegmentsPlayed = nextSegmentIndex - repeatIndex; + getVibration().stats().reportRepetition(loopSegmentsPlayed / loopSize); nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize); } Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect, diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java index 3bc11c8f8322..f8b99265246a 100644 --- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java @@ -67,9 +67,10 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { Slog.d(VibrationThread.TAG, "Compose " + primitives + " primitives on vibrator " + controller.getVibratorInfo().getId()); } - mVibratorOnResult = controller.on( - primitives.toArray(new PrimitiveSegment[primitives.size()]), - getVibration().id); + PrimitiveSegment[] primitivesArray = + primitives.toArray(new PrimitiveSegment[primitives.size()]); + mVibratorOnResult = controller.on(primitivesArray, getVibration().id); + getVibration().stats().reportComposePrimitives(mVibratorOnResult, primitivesArray); return nextSteps(/* segmentsPlayed= */ primitives.size()); } finally { diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java index 919f1be27ef9..81f52c912f28 100644 --- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java @@ -68,8 +68,9 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep { Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator " + controller.getVibratorInfo().getId()); } - mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]), - getVibration().id); + RampSegment[] pwlesArray = pwles.toArray(new RampSegment[pwles.size()]); + mVibratorOnResult = controller.on(pwlesArray, getVibration().id); + getVibration().stats().reportComposePwle(mVibratorOnResult, pwlesArray); return nextSteps(/* segmentsPlayed= */ pwles.size()); } finally { diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java index 601ae978f637..41902147838d 100644 --- a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java @@ -62,6 +62,7 @@ final class PerformPrebakedVibratorStep extends AbstractVibratorStep { VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId()); mVibratorOnResult = controller.on(prebaked, getVibration().id); + getVibration().stats().reportPerformEffect(mVibratorOnResult, prebaked); if (mVibratorOnResult == 0 && prebaked.shouldFallback() && (fallback instanceof VibrationEffect.Composed)) { diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java index 1f0d2d71d25c..6fb9111793ea 100644 --- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java @@ -148,7 +148,9 @@ final class SetAmplitudeVibratorStep extends AbstractVibratorStep { "Turning on vibrator " + controller.getVibratorInfo().getId() + " for " + duration + "ms"); } - return controller.on(duration, getVibration().id); + long vibratorOnResult = controller.on(duration, getVibration().id); + getVibration().stats().reportVibratorOn(vibratorOnResult); + return vibratorOnResult; } /** diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java index 080a36cb2a6e..2c6fbbc945fa 100644 --- a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java +++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java @@ -93,10 +93,8 @@ final class StartSequentialEffectStep extends Step { } mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps); - if (mVibratorsOnMaxDuration > 0) { - conductor.vibratorManagerHooks.noteVibratorOn(conductor.getVibration().uid, - mVibratorsOnMaxDuration); - } + conductor.vibratorManagerHooks.noteVibratorOn(conductor.getVibration().uid, + mVibratorsOnMaxDuration); } finally { if (mVibratorsOnMaxDuration >= 0) { // It least one vibrator was started then add a finish step to wait for all diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index d79837be3583..a375d0aceb54 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -16,10 +16,10 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.CombinedVibration; import android.os.IBinder; -import android.os.SystemClock; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.vibrator.PrebakedSegment; @@ -30,48 +30,60 @@ import android.os.vibrator.VibrationEffectSegment; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; +import com.android.internal.util.FrameworkStatsLog; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.function.Function; /** Represents a vibration request to the vibrator service. */ final class Vibration { - private static final String TAG = "Vibration"; private static final SimpleDateFormat DEBUG_DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + /** Vibration status with reference to values from vibratormanagerservice.proto for logging. */ enum Status { - RUNNING, - FINISHED, - FINISHED_UNEXPECTED, // Didn't terminate in the usual way. - FORWARDED_TO_INPUT_DEVICES, - CANCELLED_BINDER_DIED, - CANCELLED_BY_SCREEN_OFF, - CANCELLED_BY_SETTINGS_UPDATE, - CANCELLED_BY_USER, - CANCELLED_BY_UNKNOWN_REASON, - CANCELLED_SUPERSEDED, - IGNORED_ERROR_APP_OPS, - IGNORED_ERROR_CANCELLING, - IGNORED_ERROR_SCHEDULING, - IGNORED_ERROR_TOKEN, - IGNORED_APP_OPS, - IGNORED_BACKGROUND, - IGNORED_UNKNOWN_VIBRATION, - IGNORED_UNSUPPORTED, - IGNORED_FOR_EXTERNAL, - IGNORED_FOR_HIGHER_IMPORTANCE, - IGNORED_FOR_ONGOING, - IGNORED_FOR_POWER, - IGNORED_FOR_RINGER_MODE, - IGNORED_FOR_SETTINGS, - IGNORED_SUPERSEDED, + UNKNOWN(VibrationProto.UNKNOWN), + RUNNING(VibrationProto.RUNNING), + FINISHED(VibrationProto.FINISHED), + FINISHED_UNEXPECTED(VibrationProto.FINISHED_UNEXPECTED), + FORWARDED_TO_INPUT_DEVICES(VibrationProto.FORWARDED_TO_INPUT_DEVICES), + CANCELLED_BINDER_DIED(VibrationProto.CANCELLED_BINDER_DIED), + CANCELLED_BY_SCREEN_OFF(VibrationProto.CANCELLED_BY_SCREEN_OFF), + CANCELLED_BY_SETTINGS_UPDATE(VibrationProto.CANCELLED_BY_SETTINGS_UPDATE), + CANCELLED_BY_USER(VibrationProto.CANCELLED_BY_USER), + CANCELLED_BY_UNKNOWN_REASON(VibrationProto.CANCELLED_BY_UNKNOWN_REASON), + CANCELLED_SUPERSEDED(VibrationProto.CANCELLED_SUPERSEDED), + IGNORED_ERROR_APP_OPS(VibrationProto.IGNORED_ERROR_APP_OPS), + IGNORED_ERROR_CANCELLING(VibrationProto.IGNORED_ERROR_CANCELLING), + IGNORED_ERROR_SCHEDULING(VibrationProto.IGNORED_ERROR_SCHEDULING), + IGNORED_ERROR_TOKEN(VibrationProto.IGNORED_ERROR_TOKEN), + IGNORED_APP_OPS(VibrationProto.IGNORED_APP_OPS), + IGNORED_BACKGROUND(VibrationProto.IGNORED_BACKGROUND), + IGNORED_UNKNOWN_VIBRATION(VibrationProto.IGNORED_UNKNOWN_VIBRATION), + IGNORED_UNSUPPORTED(VibrationProto.IGNORED_UNSUPPORTED), + IGNORED_FOR_EXTERNAL(VibrationProto.IGNORED_FOR_EXTERNAL), + IGNORED_FOR_HIGHER_IMPORTANCE(VibrationProto.IGNORED_FOR_HIGHER_IMPORTANCE), + IGNORED_FOR_ONGOING(VibrationProto.IGNORED_FOR_ONGOING), + IGNORED_FOR_POWER(VibrationProto.IGNORED_FOR_POWER), + IGNORED_FOR_RINGER_MODE(VibrationProto.IGNORED_FOR_RINGER_MODE), + IGNORED_FOR_SETTINGS(VibrationProto.IGNORED_FOR_SETTINGS), + IGNORED_SUPERSEDED(VibrationProto.IGNORED_SUPERSEDED); + + private final int mProtoEnumValue; + + Status(int value) { + mProtoEnumValue = value; + } + + public int getProtoEnumValue() { + return mProtoEnumValue; + } } - /** Start time using {@link SystemClock#uptimeMillis()}, for calculations. */ - public final long startUptimeMillis; public final VibrationAttributes attrs; public final long id; public final int uid; @@ -91,17 +103,11 @@ final class Vibration { @Nullable private CombinedVibration mOriginalEffect; - /** - * Start/end times in unix epoch time. Only to be used for debugging purposes and to correlate - * with other system events, any duration calculations should be done use - * {@link #startUptimeMillis} so as not to be affected by discontinuities created by RTC - * adjustments. - */ - private final long mStartTimeDebug; - private long mEndTimeDebug; - /** End time using {@link SystemClock#uptimeMillis()}, for calculations. */ - private long mEndUptimeMillis; - private Status mStatus; + /** Vibration status. */ + private Vibration.Status mStatus; + + /** Vibration runtime stats. */ + private final VibrationStats mStats = new VibrationStats(); /** A {@link CountDownLatch} to enable waiting for completion. */ private final CountDownLatch mCompletionLatch = new CountDownLatch(1); @@ -111,34 +117,35 @@ final class Vibration { this.token = token; this.mEffect = effect; this.id = id; - this.startUptimeMillis = SystemClock.uptimeMillis(); this.attrs = attrs; this.uid = uid; this.opPkg = opPkg; this.reason = reason; - mStartTimeDebug = System.currentTimeMillis(); - mStatus = Status.RUNNING; + mStatus = Vibration.Status.RUNNING; + } + + VibrationStats stats() { + return mStats; } /** - * Set the {@link Status} of this vibration and the current system time as this + * Set the {@link Status} of this vibration and reports the current system time as this * vibration end time, for debugging purposes. * * <p>This method will only accept given value if the current status is {@link * Status#RUNNING}. */ - public void end(Status status) { + public void end(EndInfo info) { if (hasEnded()) { // Vibration already ended, keep first ending status set and ignore this one. return; } - mStatus = status; - mEndUptimeMillis = SystemClock.uptimeMillis(); - mEndTimeDebug = System.currentTimeMillis(); + mStatus = info.status; + mStats.reportEnded(info.endedByUid, info.endedByUsage); mCompletionLatch.countDown(); } - /** Waits indefinitely until another thread calls {@link #end(Status)} on this vibration. */ + /** Waits indefinitely until another thread calls {@link #end} on this vibration. */ public void waitForEnd() throws InterruptedException { mCompletionLatch.await(); } @@ -228,16 +235,69 @@ final class Vibration { /** Return {@link Vibration.DebugInfo} with read-only debug information about this vibration. */ public Vibration.DebugInfo getDebugInfo() { - long durationMs = hasEnded() ? mEndUptimeMillis - startUptimeMillis : -1; - return new Vibration.DebugInfo( - mStartTimeDebug, mEndTimeDebug, durationMs, mEffect, mOriginalEffect, - /* scale= */ 0, attrs, uid, opPkg, reason, mStatus); + return new Vibration.DebugInfo(mStatus, mStats, mEffect, mOriginalEffect, /* scale= */ 0, + attrs, uid, opPkg, reason); + } + + /** Return {@link VibrationStats.StatsInfo} with read-only metrics about this vibration. */ + public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) { + int vibrationType = isRepeating() + ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED + : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE; + return new VibrationStats.StatsInfo( + uid, vibrationType, attrs.getUsage(), mStatus, mStats, completionUptimeMillis); + } + + /** Immutable info passed as a signal to end a vibration. */ + static final class EndInfo { + /** The {@link Status} to be set to the vibration when it ends with this info. */ + @NonNull + public final Status status; + /** The UID that triggered the vibration that ended this, or -1 if undefined. */ + public final int endedByUid; + /** The VibrationAttributes.USAGE_* of the vibration that ended this, or -1 if undefined. */ + public final int endedByUsage; + + EndInfo(@NonNull Vibration.Status status) { + this(status, /* endedByUid= */ -1, /* endedByUsage= */ -1); + } + + EndInfo(@NonNull Vibration.Status status, int endedByUid, int endedByUsage) { + this.status = status; + this.endedByUid = endedByUid; + this.endedByUsage = endedByUsage; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EndInfo)) return false; + EndInfo that = (EndInfo) o; + return endedByUid == that.endedByUid + && endedByUsage == that.endedByUsage + && status == that.status; + } + + @Override + public int hashCode() { + return Objects.hash(status, endedByUid, endedByUsage); + } + + @Override + public String toString() { + return "EndInfo{" + + "status=" + status + + ", endedByUid=" + endedByUid + + ", endedByUsage=" + endedByUsage + + '}'; + } } /** Debug information about vibrations. */ static final class DebugInfo { - private final long mStartTimeDebug; - private final long mEndTimeDebug; + private final long mCreateTime; + private final long mStartTime; + private final long mEndTime; private final long mDurationMs; private final CombinedVibration mEffect; private final CombinedVibration mOriginalEffect; @@ -248,12 +308,13 @@ final class Vibration { private final String mReason; private final Status mStatus; - DebugInfo(long startTimeDebug, long endTimeDebug, long durationMs, - CombinedVibration effect, CombinedVibration originalEffect, float scale, - VibrationAttributes attrs, int uid, String opPkg, String reason, Status status) { - mStartTimeDebug = startTimeDebug; - mEndTimeDebug = endTimeDebug; - mDurationMs = durationMs; + DebugInfo(Status status, VibrationStats stats, @Nullable CombinedVibration effect, + @Nullable CombinedVibration originalEffect, float scale, VibrationAttributes attrs, + int uid, String opPkg, String reason) { + mCreateTime = stats.getCreateTimeDebug(); + mStartTime = stats.getStartTimeDebug(); + mEndTime = stats.getEndTimeDebug(); + mDurationMs = stats.getDurationDebug(); mEffect = effect; mOriginalEffect = originalEffect; mScale = scale; @@ -267,11 +328,13 @@ final class Vibration { @Override public String toString() { return new StringBuilder() - .append("startTime: ") - .append(DEBUG_DATE_FORMAT.format(new Date(mStartTimeDebug))) + .append("createTime: ") + .append(DEBUG_DATE_FORMAT.format(new Date(mCreateTime))) + .append(", startTime: ") + .append(DEBUG_DATE_FORMAT.format(new Date(mStartTime))) .append(", endTime: ") - .append(mEndTimeDebug == 0 ? null - : DEBUG_DATE_FORMAT.format(new Date(mEndTimeDebug))) + .append(mEndTime == 0 ? null + : DEBUG_DATE_FORMAT.format(new Date(mEndTime))) .append(", durationMs: ") .append(mDurationMs) .append(", status: ") @@ -296,8 +359,8 @@ final class Vibration { /** Write this info into given {@code fieldId} on {@link ProtoOutputStream}. */ public void dumpProto(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); - proto.write(VibrationProto.START_TIME, mStartTimeDebug); - proto.write(VibrationProto.END_TIME, mEndTimeDebug); + proto.write(VibrationProto.START_TIME, mStartTime); + proto.write(VibrationProto.END_TIME, mEndTime); proto.write(VibrationProto.DURATION_MS, mDurationMs); proto.write(VibrationProto.STATUS, mStatus.ordinal()); @@ -421,4 +484,5 @@ final class Vibration { proto.end(token); } } + } diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java new file mode 100644 index 000000000000..931be1d5d711 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VibrationStats.java @@ -0,0 +1,395 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; +import android.util.Slog; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; + +/** Holds basic stats about the vibration playback and interaction with the vibrator HAL. */ +final class VibrationStats { + static final String TAG = "VibrationStats"; + + // Milestone timestamps, using SystemClock.uptimeMillis(), for calculations. + // - Create: time a vibration object was created, which is closer to when the service receives a + // vibrate request. + // - Start: time a vibration started to play, which is closer to the time that the + // VibrationEffect started playing the very first segment. + // - End: time a vibration ended, even if it never started to play. This can be as soon as the + // vibrator HAL reports it has finished the last command, or before it has even started + // when the vibration is ignored or cancelled. + // Create and end times set by VibratorManagerService only, guarded by its lock. + // Start times set by VibrationThread only (single-threaded). + private long mCreateUptimeMillis; + private long mStartUptimeMillis; + private long mEndUptimeMillis; + + // Milestone timestamps, using unix epoch time, only to be used for debugging purposes and + // to correlate with other system events. Any duration calculations should be done with the + // {create/start/end}UptimeMillis counterparts so as not to be affected by discontinuities + // created by RTC adjustments. + // Set together with the *UptimeMillis counterparts. + private long mCreateTimeDebug; + private long mStartTimeDebug; + private long mEndTimeDebug; + + // Vibration interruption tracking. + // Set by VibratorManagerService only, guarded by its lock. + private int mEndedByUid; + private int mEndedByUsage; + private int mInterruptedUsage; + + // All following counters are set by VibrationThread only (single-threaded): + // Counts how many times the VibrationEffect was repeated. + private int mRepeatCount; + // Total duration, in milliseconds, the vibrator was active with non-zero amplitude. + private int mVibratorOnTotalDurationMillis; + // Total number of primitives used in compositions. + private int mVibrationCompositionTotalSize; + private int mVibrationPwleTotalSize; + // Counts how many times each IVibrator method was triggered by this vibration. + private int mVibratorOnCount; + private int mVibratorOffCount; + private int mVibratorSetAmplitudeCount; + private int mVibratorSetExternalControlCount; + private int mVibratorPerformCount; + private int mVibratorComposeCount; + private int mVibratorComposePwleCount; + + // Ids of vibration effects and primitives used by this vibration, with support flag. + // Set by VibrationThread only (single-threaded). + private SparseBooleanArray mVibratorEffectsUsed = new SparseBooleanArray(); + private SparseBooleanArray mVibratorPrimitivesUsed = new SparseBooleanArray(); + + VibrationStats() { + mCreateUptimeMillis = SystemClock.uptimeMillis(); + mCreateTimeDebug = System.currentTimeMillis(); + // Set invalid UID and VibrationAttributes.USAGE values to indicate fields are unset. + mEndedByUid = -1; + mEndedByUsage = -1; + mInterruptedUsage = -1; + } + + long getCreateUptimeMillis() { + return mCreateUptimeMillis; + } + + long getStartUptimeMillis() { + return mStartUptimeMillis; + } + + long getEndUptimeMillis() { + return mEndUptimeMillis; + } + + long getCreateTimeDebug() { + return mCreateTimeDebug; + } + + long getStartTimeDebug() { + return mStartTimeDebug; + } + + long getEndTimeDebug() { + return mEndTimeDebug; + } + + /** + * Duration calculated for debugging purposes, between the creation of a vibration and the + * end time being reported, or -1 if the vibration has not ended. + */ + long getDurationDebug() { + return hasEnded() ? (mEndUptimeMillis - mCreateUptimeMillis) : -1; + } + + /** Return true if vibration reported it has ended. */ + boolean hasEnded() { + return mEndUptimeMillis > 0; + } + + /** Return true if vibration reported it has started triggering the vibrator. */ + boolean hasStarted() { + return mStartUptimeMillis > 0; + } + + /** + * Set the current system time as this vibration start time, for debugging purposes. + * + * <p>This indicates the vibration has started to interact with the vibrator HAL and the + * device may start vibrating after this point. + * + * <p>This method will only accept given value if the start timestamp was never set. + */ + void reportStarted() { + if (hasEnded() || (mStartUptimeMillis != 0)) { + // Vibration already started or ended, keep first time set and ignore this one. + return; + } + mStartUptimeMillis = SystemClock.uptimeMillis(); + mStartTimeDebug = System.currentTimeMillis(); + } + + /** + * Set status and end cause for this vibration to end, and the current system time as this + * vibration end time, for debugging purposes. + * + * <p>This might be triggered before {@link #reportStarted()}, which indicates this + * vibration was cancelled or ignored before it started triggering the vibrator. + * + * @return true if the status was accepted. This method will only accept given values if + * the end timestamp was never set. + */ + boolean reportEnded(int endedByUid, int endedByUsage) { + if (hasEnded()) { + // Vibration already ended, keep first ending stats set and ignore this one. + return false; + } + mEndedByUid = endedByUid; + mEndedByUsage = endedByUsage; + mEndUptimeMillis = SystemClock.uptimeMillis(); + mEndTimeDebug = System.currentTimeMillis(); + return true; + } + + /** + * Report this vibration has interrupted another vibration. + * + * <p>This method will only accept the first value as the one that was interrupted by this + * vibration, and will ignore all successive calls. + */ + void reportInterruptedAnotherVibration(int interruptedUsage) { + if (mInterruptedUsage < 0) { + mInterruptedUsage = interruptedUsage; + } + } + + /** Report the vibration has looped a few more times. */ + void reportRepetition(int loops) { + mRepeatCount += loops; + } + + /** Report a call to vibrator method to turn on for given duration. */ + void reportVibratorOn(long halResult) { + mVibratorOnCount++; + + if (halResult > 0) { + // If HAL result is positive then it represents the actual duration it will be ON. + mVibratorOnTotalDurationMillis += (int) halResult; + } + } + + /** Report a call to vibrator method to turn off. */ + void reportVibratorOff() { + mVibratorOffCount++; + } + + /** Report a call to vibrator method to change the vibration amplitude. */ + void reportSetAmplitude() { + mVibratorSetAmplitudeCount++; + } + + /** Report a call to vibrator method to trigger a vibration effect. */ + void reportPerformEffect(long halResult, PrebakedSegment prebaked) { + mVibratorPerformCount++; + + if (halResult > 0) { + // If HAL result is positive then it represents the actual duration of the vibration. + mVibratorEffectsUsed.put(prebaked.getEffectId(), true); + mVibratorOnTotalDurationMillis += (int) halResult; + } else { + // Effect unsupported or request failed. + mVibratorEffectsUsed.put(prebaked.getEffectId(), false); + } + } + + /** Report a call to vibrator method to trigger a vibration as a composition of primitives. */ + void reportComposePrimitives(long halResult, PrimitiveSegment[] primitives) { + mVibratorComposeCount++; + mVibrationCompositionTotalSize += primitives.length; + + if (halResult > 0) { + // If HAL result is positive then it represents the actual duration of the vibration. + // Remove the requested delays to update the total time the vibrator was ON. + for (PrimitiveSegment primitive : primitives) { + halResult -= primitive.getDelay(); + mVibratorPrimitivesUsed.put(primitive.getPrimitiveId(), true); + } + if (halResult > 0) { + mVibratorOnTotalDurationMillis += (int) halResult; + } + } else { + // One or more primitives were unsupported, or request failed. + for (PrimitiveSegment primitive : primitives) { + mVibratorPrimitivesUsed.put(primitive.getPrimitiveId(), false); + } + } + } + + /** Report a call to vibrator method to trigger a vibration as a PWLE. */ + void reportComposePwle(long halResult, RampSegment[] segments) { + mVibratorComposePwleCount++; + mVibrationPwleTotalSize += segments.length; + + if (halResult > 0) { + // If HAL result is positive then it represents the actual duration of the vibration. + // Remove the zero-amplitude segments to update the total time the vibrator was ON. + for (RampSegment ramp : segments) { + if ((ramp.getStartAmplitude() == 0) && (ramp.getEndAmplitude() == 0)) { + halResult -= ramp.getDuration(); + } + } + if (halResult > 0) { + mVibratorOnTotalDurationMillis += (int) halResult; + } + } + } + + /** + * Increment the stats for total number of times the {@code setExternalControl} method was + * triggered in the vibrator HAL. + */ + void reportSetExternalControl() { + mVibratorSetExternalControlCount++; + } + + /** + * Immutable metrics about this vibration, to be kept in memory until it can be pushed through + * {@link com.android.internal.util.FrameworkStatsLog} as a + * {@link com.android.internal.util.FrameworkStatsLog#VIBRATION_REPORTED}. + */ + static final class StatsInfo { + public final int uid; + public final int vibrationType; + public final int usage; + public final int status; + public final boolean endedBySameUid; + public final int endedByUsage; + public final int interruptedUsage; + public final int repeatCount; + public final int totalDurationMillis; + public final int vibratorOnMillis; + public final int startLatencyMillis; + public final int endLatencyMillis; + public final int halComposeCount; + public final int halComposePwleCount; + public final int halOnCount; + public final int halOffCount; + public final int halPerformCount; + public final int halSetAmplitudeCount; + public final int halSetExternalControlCount; + public final int halCompositionSize; + public final int halPwleSize; + public final int[] halSupportedCompositionPrimitivesUsed; + public final int[] halSupportedEffectsUsed; + public final int[] halUnsupportedCompositionPrimitivesUsed; + public final int[] halUnsupportedEffectsUsed; + private boolean mIsWritten; + + StatsInfo(int uid, int vibrationType, int usage, Vibration.Status status, + VibrationStats stats, long completionUptimeMillis) { + this.uid = uid; + this.vibrationType = vibrationType; + this.usage = usage; + this.status = status.getProtoEnumValue(); + endedBySameUid = (uid == stats.mEndedByUid); + endedByUsage = stats.mEndedByUsage; + interruptedUsage = stats.mInterruptedUsage; + repeatCount = stats.mRepeatCount; + + // This duration goes from the time this object was created until the time it was + // completed. We can use latencies to detect the times between first and last + // interaction with vibrator. + totalDurationMillis = + (int) Math.max(0, completionUptimeMillis - stats.mCreateUptimeMillis); + vibratorOnMillis = stats.mVibratorOnTotalDurationMillis; + + if (stats.hasStarted()) { + // We only measure latencies for vibrations that actually triggered the vibrator. + startLatencyMillis = + (int) Math.max(0, stats.mStartUptimeMillis - stats.mCreateUptimeMillis); + endLatencyMillis = + (int) Math.max(0, completionUptimeMillis - stats.mEndUptimeMillis); + } else { + startLatencyMillis = endLatencyMillis = 0; + } + + halComposeCount = stats.mVibratorComposeCount; + halComposePwleCount = stats.mVibratorComposePwleCount; + halOnCount = stats.mVibratorOnCount; + halOffCount = stats.mVibratorOffCount; + halPerformCount = stats.mVibratorPerformCount; + halSetAmplitudeCount = stats.mVibratorSetAmplitudeCount; + halSetExternalControlCount = stats.mVibratorSetExternalControlCount; + halCompositionSize = stats.mVibrationCompositionTotalSize; + halPwleSize = stats.mVibrationPwleTotalSize; + halSupportedCompositionPrimitivesUsed = + filteredKeys(stats.mVibratorPrimitivesUsed, /* supported= */ true); + halSupportedEffectsUsed = + filteredKeys(stats.mVibratorEffectsUsed, /* supported= */ true); + halUnsupportedCompositionPrimitivesUsed = + filteredKeys(stats.mVibratorPrimitivesUsed, /* supported= */ false); + halUnsupportedEffectsUsed = + filteredKeys(stats.mVibratorEffectsUsed, /* supported= */ false); + } + + @VisibleForTesting + boolean isWritten() { + return mIsWritten; + } + + void writeVibrationReported() { + if (mIsWritten) { + Slog.wtf(TAG, "Writing same vibration stats multiple times for uid=" + uid); + } + mIsWritten = true; + // Mapping from this MetricInfo representation and the atom proto VibrationReported. + FrameworkStatsLog.write_non_chained( + FrameworkStatsLog.VIBRATION_REPORTED, + uid, null, vibrationType, usage, status, endedBySameUid, endedByUsage, + interruptedUsage, repeatCount, totalDurationMillis, vibratorOnMillis, + startLatencyMillis, endLatencyMillis, halComposeCount, halComposePwleCount, + halOnCount, halOffCount, halPerformCount, halSetAmplitudeCount, + halSetExternalControlCount, halSupportedCompositionPrimitivesUsed, + halSupportedEffectsUsed, halUnsupportedCompositionPrimitivesUsed, + halUnsupportedEffectsUsed, halCompositionSize, halPwleSize); + } + + private static int[] filteredKeys(SparseBooleanArray supportArray, boolean supported) { + int count = 0; + for (int i = 0; i < supportArray.size(); i++) { + if (supportArray.valueAt(i) == supported) count++; + } + if (count == 0) { + return null; + } + int pos = 0; + int[] res = new int[count]; + for (int i = 0; i < supportArray.size(); i++) { + if (supportArray.valueAt(i) == supported) { + res[pos++] = supportArray.keyAt(i); + } + } + return res; + } + } +} diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index e3d806755c6e..0799b955b6f1 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -81,12 +81,12 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { private final IntArray mSignalVibratorsComplete; @Nullable @GuardedBy("mLock") - private Vibration.Status mSignalCancelStatus = null; + private Vibration.EndInfo mSignalCancel = null; @GuardedBy("mLock") private boolean mSignalCancelImmediate = false; @Nullable - private Vibration.Status mCancelStatus = null; + private Vibration.EndInfo mCancelledVibrationEndInfo = null; private boolean mCancelledImmediately = false; // hard stop private int mPendingVibrateSteps; private int mRemainingStartSequentialEffectSteps; @@ -153,6 +153,9 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { // This count is decremented at the completion of the step, so we don't subtract one. mRemainingStartSequentialEffectSteps = sequentialEffect.getEffects().size(); mNextSteps.offer(new StartSequentialEffectStep(this, sequentialEffect)); + // Vibration will start playing in the Vibrator, following the effect timings and delays. + // Report current time as the vibration start time, for debugging. + mVibration.stats().reportStarted(); } public Vibration getVibration() { @@ -182,24 +185,25 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { * Calculate the {@link Vibration.Status} based on the current queue state and the expected * number of {@link StartSequentialEffectStep} to be played. */ - public Vibration.Status calculateVibrationStatus() { + @Nullable + public Vibration.EndInfo calculateVibrationEndInfo() { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(true); } - if (mCancelStatus != null) { - return mCancelStatus; + if (mCancelledVibrationEndInfo != null) { + return mCancelledVibrationEndInfo; } - if (mPendingVibrateSteps > 0 - || mRemainingStartSequentialEffectSteps > 0) { - return Vibration.Status.RUNNING; + if (mPendingVibrateSteps > 0 || mRemainingStartSequentialEffectSteps > 0) { + // Vibration still running. + return null; } // No pending steps, and something happened. if (mSuccessfulVibratorOnSteps > 0) { - return Vibration.Status.FINISHED; + return new Vibration.EndInfo(Vibration.Status.FINISHED); } // If no step was able to turn the vibrator ON successfully. - return Vibration.Status.IGNORED_UNSUPPORTED; + return new Vibration.EndInfo(Vibration.Status.IGNORED_UNSUPPORTED); } /** @@ -305,45 +309,50 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { if (DEBUG) { Slog.d(TAG, "Binder died, cancelling vibration..."); } - notifyCancelled(Vibration.Status.CANCELLED_BINDER_DIED, /* immediate= */ false); + notifyCancelled(new Vibration.EndInfo(Vibration.Status.CANCELLED_BINDER_DIED), + /* immediate= */ false); } /** * Notify the execution that cancellation is requested. This will be acted upon * asynchronously in the VibrationThread. * + * <p>Only the first cancel signal will be used to end a cancelled vibration, but subsequent + * calls with {@code immediate} flag set to true can still force the first cancel signal to + * take effect urgently. + * * @param immediate indicates whether cancellation should abort urgently and skip cleanup steps. */ - public void notifyCancelled(@NonNull Vibration.Status cancelStatus, boolean immediate) { + public void notifyCancelled(@NonNull Vibration.EndInfo cancelInfo, boolean immediate) { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(false); } if (DEBUG) { - Slog.d(TAG, "Vibration cancel requested with status=" + cancelStatus + Slog.d(TAG, "Vibration cancel requested with signal=" + cancelInfo + ", immediate=" + immediate); } - if ((cancelStatus == null) || !cancelStatus.name().startsWith("CANCEL")) { - Slog.w(TAG, "Vibration cancel requested with bad status=" + cancelStatus + if ((cancelInfo == null) || !cancelInfo.status.name().startsWith("CANCEL")) { + Slog.w(TAG, "Vibration cancel requested with bad signal=" + cancelInfo + ", using CANCELLED_UNKNOWN_REASON to ensure cancellation."); - cancelStatus = Vibration.Status.CANCELLED_BY_UNKNOWN_REASON; + cancelInfo = new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_UNKNOWN_REASON); } synchronized (mLock) { - if (immediate && mSignalCancelImmediate || (mSignalCancelStatus != null)) { + if ((immediate && mSignalCancelImmediate) || (mSignalCancel != null)) { if (DEBUG) { Slog.d(TAG, "Vibration cancel request ignored as the vibration " - + mVibration.id + "is already being cancelled with status=" - + mSignalCancelStatus + ", immediate=" + mSignalCancelImmediate); + + mVibration.id + "is already being cancelled with signal=" + + mSignalCancel + ", immediate=" + mSignalCancelImmediate); } return; } mSignalCancelImmediate |= immediate; - if (mSignalCancelStatus == null) { - mSignalCancelStatus = cancelStatus; + if (mSignalCancel == null) { + mSignalCancel = cancelInfo; } else { if (DEBUG) { - Slog.d(TAG, "Vibration cancel request new status=" + cancelStatus - + " ignored as the vibration was already cancelled with status=" - + mSignalCancelStatus + ", but immediate flag was updated to " + Slog.d(TAG, "Vibration cancel request new signal=" + cancelInfo + + " ignored as the vibration was already cancelled with signal=" + + mSignalCancel + ", but immediate flag was updated to " + mSignalCancelImmediate); } } @@ -401,9 +410,9 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(true); // Reads VibrationThread variables as well as signals. } - return (mSignalCancelStatus != mCancelStatus) - || (mSignalCancelImmediate && !mCancelledImmediately) - || (mSignalVibratorsComplete.size() > 0); + return (mSignalCancel != null && mCancelledVibrationEndInfo == null) + || (mSignalCancelImmediate && !mCancelledImmediately) + || (mSignalVibratorsComplete.size() > 0); } /** @@ -416,7 +425,7 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } int[] vibratorsToProcess = null; - Vibration.Status doCancelStatus = null; + Vibration.EndInfo doCancelInfo = null; boolean doCancelImmediate = false; // Collect signals to process, but don't keep the lock while processing them. synchronized (mLock) { @@ -426,10 +435,10 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } // This should only happen once. doCancelImmediate = true; - doCancelStatus = mSignalCancelStatus; + doCancelInfo = mSignalCancel; } - if (mSignalCancelStatus != mCancelStatus) { - doCancelStatus = mSignalCancelStatus; + if ((mSignalCancel != null) && (mCancelledVibrationEndInfo == null)) { + doCancelInfo = mSignalCancel; } if (!doCancelImmediate && mSignalVibratorsComplete.size() > 0) { // Swap out the queue of completions to process. @@ -443,11 +452,11 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { // completion signals that were collected in this call, but we won't process them // anyway as all steps are cancelled. if (doCancelImmediate) { - processCancelImmediately(doCancelStatus); + processCancelImmediately(doCancelInfo); return; } - if (doCancelStatus != null) { - processCancel(doCancelStatus); + if (doCancelInfo != null) { + processCancel(doCancelInfo); } if (vibratorsToProcess != null) { processVibratorsComplete(vibratorsToProcess); @@ -460,12 +469,12 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { * <p>This will remove all steps and replace them with respective results of * {@link Step#cancel()}. */ - public void processCancel(Vibration.Status cancelStatus) { + public void processCancel(Vibration.EndInfo cancelInfo) { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(true); } - mCancelStatus = cancelStatus; + mCancelledVibrationEndInfo = cancelInfo; // Vibrator callbacks should wait until all steps from the queue are properly cancelled // and clean up steps are added back to the queue, so they can handle the callback. List<Step> cleanUpSteps = new ArrayList<>(); @@ -483,13 +492,13 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { * * <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order. */ - public void processCancelImmediately(Vibration.Status cancelStatus) { + public void processCancelImmediately(Vibration.EndInfo cancelInfo) { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(true); } mCancelledImmediately = true; - mCancelStatus = cancelStatus; + mCancelledVibrationEndInfo = cancelInfo; Step step; while ((step = pollNext()) != null) { step.cancelImmediately(); diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index cecc5c04dedc..e824db105abc 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -76,7 +76,7 @@ final class VibrationThread extends Thread { * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased} * is called. */ - void onVibrationCompleted(long vibrationId, Vibration.Status status); + void onVibrationCompleted(long vibrationId, @NonNull Vibration.EndInfo vibrationEndInfo); /** * Tells the manager that the VibrationThread is finished with the previous vibration and @@ -237,7 +237,8 @@ final class VibrationThread extends Thread { try { runCurrentVibrationWithWakeLockAndDeathLink(); } finally { - clientVibrationCompleteIfNotAlready(Vibration.Status.FINISHED_UNEXPECTED); + clientVibrationCompleteIfNotAlready( + new Vibration.EndInfo(Vibration.Status.FINISHED_UNEXPECTED)); } } finally { mWakeLock.release(); @@ -255,7 +256,8 @@ final class VibrationThread extends Thread { vibrationBinderToken.linkToDeath(mExecutingConductor, 0); } catch (RemoteException e) { Slog.e(TAG, "Error linking vibration to token death", e); - clientVibrationCompleteIfNotAlready(Vibration.Status.IGNORED_ERROR_TOKEN); + clientVibrationCompleteIfNotAlready( + new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_TOKEN)); return; } // Ensure that the unlink always occurs now. @@ -274,11 +276,11 @@ final class VibrationThread extends Thread { // Indicate that the vibration is complete. This can be called multiple times only for // convenience of handling error conditions - an error after the client is complete won't // affect the status. - private void clientVibrationCompleteIfNotAlready(Vibration.Status completedStatus) { + private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) { if (!mCalledVibrationCompleteCallback) { mCalledVibrationCompleteCallback = true; mVibratorManagerHooks.onVibrationCompleted( - mExecutingConductor.getVibration().id, completedStatus); + mExecutingConductor.getVibration().id, vibrationEndInfo); } } @@ -298,12 +300,15 @@ final class VibrationThread extends Thread { mExecutingConductor.runNextStep(); } - Vibration.Status status = mExecutingConductor.calculateVibrationStatus(); - // This block can only run once due to mCalledVibrationCompleteCallback. - if (status != Vibration.Status.RUNNING && !mCalledVibrationCompleteCallback) { - // First time vibration stopped running, start clean-up tasks and notify - // callback immediately. - clientVibrationCompleteIfNotAlready(status); + if (!mCalledVibrationCompleteCallback) { + // This block can only run once due to mCalledVibrationCompleteCallback. + Vibration.EndInfo vibrationEndInfo = + mExecutingConductor.calculateVibrationEndInfo(); + if (vibrationEndInfo != null) { + // First time vibration stopped running, start clean-up tasks and notify + // callback immediately. + clientVibrationCompleteIfNotAlready(vibrationEndInfo); + } } } } finally { diff --git a/services/core/java/com/android/server/vibrator/VibratorFrameworkStatsLogger.java b/services/core/java/com/android/server/vibrator/VibratorFrameworkStatsLogger.java new file mode 100644 index 000000000000..f600a2964cbc --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VibratorFrameworkStatsLogger.java @@ -0,0 +1,140 @@ +/* + * 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.server.vibrator; + +import android.os.Handler; +import android.os.SystemClock; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.ArrayDeque; +import java.util.Queue; + +/** Helper class for async write of atoms to {@link FrameworkStatsLog} using a given Handler. */ +public class VibratorFrameworkStatsLogger { + private static final String TAG = "VibratorFrameworkStatsLogger"; + + // VibrationReported pushed atom needs to be throttled to at most one every 10ms. + private static final int VIBRATION_REPORTED_MIN_INTERVAL_MILLIS = 10; + // We accumulate events that should take 3s to write and drop excessive metrics. + private static final int VIBRATION_REPORTED_MAX_QUEUE_SIZE = 300; + // Warning about dropping entries after this amount of atoms were dropped by the throttle. + private static final int VIBRATION_REPORTED_WARNING_QUEUE_SIZE = 200; + + private final Object mLock = new Object(); + private final Handler mHandler; + private final long mVibrationReportedLogIntervalMillis; + private final long mVibrationReportedQueueMaxSize; + private final Runnable mConsumeVibrationStatsQueueRunnable = + () -> writeVibrationReportedFromQueue(); + + @GuardedBy("mLock") + private long mLastVibrationReportedLogUptime; + @GuardedBy("mLock") + private Queue<VibrationStats.StatsInfo> mVibrationStatsQueue = new ArrayDeque<>(); + + VibratorFrameworkStatsLogger(Handler handler) { + this(handler, VIBRATION_REPORTED_MIN_INTERVAL_MILLIS, VIBRATION_REPORTED_MAX_QUEUE_SIZE); + } + + @VisibleForTesting + VibratorFrameworkStatsLogger(Handler handler, int vibrationReportedLogIntervalMillis, + int vibrationReportedQueueMaxSize) { + mHandler = handler; + mVibrationReportedLogIntervalMillis = vibrationReportedLogIntervalMillis; + mVibrationReportedQueueMaxSize = vibrationReportedQueueMaxSize; + } + + /** Writes {@link FrameworkStatsLog#VIBRATOR_STATE_CHANGED} for state ON. */ + public void writeVibratorStateOnAsync(int uid, long duration) { + mHandler.post( + () -> FrameworkStatsLog.write_non_chained( + FrameworkStatsLog.VIBRATOR_STATE_CHANGED, uid, null, + FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, duration)); + } + + /** Writes {@link FrameworkStatsLog#VIBRATOR_STATE_CHANGED} for state OFF. */ + public void writeVibratorStateOffAsync(int uid) { + mHandler.post( + () -> FrameworkStatsLog.write_non_chained( + FrameworkStatsLog.VIBRATOR_STATE_CHANGED, uid, null, + FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, + /* duration= */ 0)); + } + + /** + * Writes {@link FrameworkStatsLog#VIBRATION_REPORTED} for given vibration. + * + * <p>This atom is throttled to be pushed once every 10ms, so this logger can keep a queue of + * {@link VibrationStats.StatsInfo} entries to slowly write to statsd. + */ + public void writeVibrationReportedAsync(VibrationStats.StatsInfo metrics) { + boolean needsScheduling; + long scheduleDelayMs; + int queueSize; + + synchronized (mLock) { + queueSize = mVibrationStatsQueue.size(); + needsScheduling = (queueSize == 0); + + if (queueSize < mVibrationReportedQueueMaxSize) { + mVibrationStatsQueue.offer(metrics); + } + + long nextLogUptime = + mLastVibrationReportedLogUptime + mVibrationReportedLogIntervalMillis; + scheduleDelayMs = Math.max(0, nextLogUptime - SystemClock.uptimeMillis()); + } + + if ((queueSize + 1) == VIBRATION_REPORTED_WARNING_QUEUE_SIZE) { + Slog.w(TAG, " Approaching vibration metrics queue limit, events might be dropped."); + } + + if (needsScheduling) { + mHandler.postDelayed(mConsumeVibrationStatsQueueRunnable, scheduleDelayMs); + } + } + + /** Writes next {@link FrameworkStatsLog#VIBRATION_REPORTED} from the queue. */ + private void writeVibrationReportedFromQueue() { + boolean needsScheduling; + VibrationStats.StatsInfo stats; + + synchronized (mLock) { + stats = mVibrationStatsQueue.poll(); + needsScheduling = !mVibrationStatsQueue.isEmpty(); + + if (stats != null) { + mLastVibrationReportedLogUptime = SystemClock.uptimeMillis(); + } + } + + if (stats == null) { + Slog.w(TAG, "Unexpected vibration metric flush with empty queue. Ignoring."); + } else { + stats.writeVibrationReported(); + } + + if (needsScheduling) { + mHandler.postDelayed(mConsumeVibrationStatsQueueRunnable, + mVibrationReportedLogIntervalMillis); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 5ac2f4f27452..2f12a820eb81 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -129,6 +129,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private final Context mContext; private final PowerManager.WakeLock mWakeLock; private final IBatteryStats mBatteryStatsService; + private final VibratorFrameworkStatsLogger mFrameworkStatsLogger; private final Handler mHandler; private final VibrationThread mVibrationThread; private final AppOpsManager mAppOps; @@ -163,10 +164,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // When the system is entering a non-interactive state, we want to cancel // vibrations in case a misbehaving app has abandoned them. if (shouldCancelOnScreenOffLocked(mNextVibration)) { - clearNextVibrationLocked(Vibration.Status.CANCELLED_BY_SCREEN_OFF); + clearNextVibrationLocked( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF)); } if (shouldCancelOnScreenOffLocked(mCurrentVibration)) { - mCurrentVibration.notifyCancelled(Vibration.Status.CANCELLED_BY_SCREEN_OFF, + mCurrentVibration.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false); } } @@ -207,6 +210,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mVibratorManagerRecords = new VibratorManagerRecords(dumpLimit); mBatteryStatsService = injector.getBatteryStatsService(); + mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler); mAppOps = mContext.getSystemService(AppOpsManager.class); @@ -384,7 +388,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * The Vibration is only returned if it is ongoing after this method returns. */ @Nullable - private Vibration vibrateInternal(int uid, String opPkg, @NonNull CombinedVibration effect, + @VisibleForTesting + Vibration vibrateInternal(int uid, String opPkg, @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs, String reason, IBinder token) { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason); try { @@ -399,6 +404,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return null; } attrs = fixupVibrationAttributes(attrs, effect); + // Create Vibration.Stats as close to the received request as possible, for tracking. Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(), effect, attrs, uid, opPkg, reason); fillVibrationFallbacks(vib, effect); @@ -413,32 +419,56 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "Starting vibrate for vibration " + vib.id); } - Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked( - vib.uid, vib.opPkg, vib.attrs); - - if (ignoreStatus == null) { - ignoreStatus = shouldIgnoreVibrationForOngoingLocked(vib); + int ignoredByUid = -1; + int ignoredByUsage = -1; + Vibration.Status status = null; + + // Check if user settings or DnD is set to ignore this vibration. + status = shouldIgnoreVibrationLocked(vib.uid, vib.opPkg, vib.attrs); + + // Check if something has external control, assume it's more important. + if ((status == null) && (mCurrentExternalVibration != null)) { + status = Vibration.Status.IGNORED_FOR_EXTERNAL; + ignoredByUid = mCurrentExternalVibration.externalVibration.getUid(); + ignoredByUsage = mCurrentExternalVibration.externalVibration + .getVibrationAttributes().getUsage(); } - if (ignoreStatus != null) { - endVibrationLocked(vib, ignoreStatus); - return vib; + // Check if ongoing vibration is more important than this vibration. + if (status == null) { + status = shouldIgnoreVibrationForOngoingLocked(vib); + if (status != null) { + ignoredByUid = mCurrentVibration.getVibration().uid; + ignoredByUsage = mCurrentVibration.getVibration().attrs.getUsage(); + } } - final long ident = Binder.clearCallingIdentity(); - try { - if (mCurrentVibration != null) { - mCurrentVibration.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, - /* immediate= */ false); - } - Vibration.Status status = startVibrationLocked(vib); - if (status != Vibration.Status.RUNNING) { - endVibrationLocked(vib, status); + // If not ignored so far then try to start this vibration. + if (status == null) { + final long ident = Binder.clearCallingIdentity(); + try { + if (mCurrentVibration != null) { + vib.stats().reportInterruptedAnotherVibration( + mCurrentVibration.getVibration().attrs.getUsage()); + mCurrentVibration.notifyCancelled( + new Vibration.EndInfo( + Vibration.Status.CANCELLED_SUPERSEDED, vib.uid, + vib.attrs.getUsage()), + /* immediate= */ false); + } + status = startVibrationLocked(vib); + } finally { + Binder.restoreCallingIdentity(ident); } - return vib; - } finally { - Binder.restoreCallingIdentity(ident); } + + // Ignored or failed to start the vibration, end it and report metrics right away. + if (status != Vibration.Status.RUNNING) { + endVibrationLocked(vib, + new Vibration.EndInfo(status, ignoredByUid, ignoredByUsage), + /* shouldWriteStats= */ true); + } + return vib; } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); @@ -457,26 +487,28 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "Canceling vibration"); } + Vibration.EndInfo cancelledByUserInfo = + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER); final long ident = Binder.clearCallingIdentity(); try { if (mNextVibration != null && shouldCancelVibration(mNextVibration.getVibration(), usageFilter, token)) { - clearNextVibrationLocked(Vibration.Status.CANCELLED_BY_USER); + clearNextVibrationLocked(cancelledByUserInfo); } if (mCurrentVibration != null && shouldCancelVibration(mCurrentVibration.getVibration(), usageFilter, token)) { - mCurrentVibration.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, - /* immediate= */false); + mCurrentVibration.notifyCancelled( + cancelledByUserInfo, /* immediate= */false); } if (mCurrentExternalVibration != null && shouldCancelVibration( mCurrentExternalVibration.externalVibration.getVibrationAttributes(), usageFilter)) { - mCurrentExternalVibration.externalVibration.mute(); - endExternalVibrateLocked(Vibration.Status.CANCELLED_BY_USER, - /* continueExternalControl= */ false); + mCurrentExternalVibration.mute(); + endExternalVibrateLocked( + cancelledByUserInfo, /* continueExternalControl= */ false); } } finally { Binder.restoreCallingIdentity(ident); @@ -604,15 +636,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.d(TAG, "Canceling vibration because settings changed: " + (inputDevicesChanged ? "input devices changed" : ignoreStatus)); } - mCurrentVibration.notifyCancelled(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE, + mCurrentVibration.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), /* immediate= */ false); } } } - private void setExternalControl(boolean externalControl) { + private void setExternalControl(boolean externalControl, VibrationStats vibrationStats) { for (int i = 0; i < mVibrators.size(); i++) { mVibrators.valueAt(i).setExternalControl(externalControl); + vibrationStats.reportSetExternalControl(); } } @@ -654,7 +688,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } // If there's already a vibration queued (waiting for the previous one to finish // cancelling), end it cleanly and replace it with the new one. - clearNextVibrationLocked(Vibration.Status.IGNORED_SUPERSEDED); + clearNextVibrationLocked( + new Vibration.EndInfo(Vibration.Status.IGNORED_SUPERSEDED, + vib.uid, vib.attrs.getUsage())); mNextVibration = conductor; return Vibration.Status.RUNNING; } finally { @@ -671,6 +707,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { switch (mode) { case AppOpsManager.MODE_ALLOWED: Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + // Make sure mCurrentVibration is set while triggering the VibrationThread. mCurrentVibration = conductor; if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) { // Shouldn't happen. The method call already logs a wtf. @@ -690,18 +727,26 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void endVibrationLocked(Vibration vib, Vibration.Status status) { - vib.end(status); - logVibrationStatus(vib.uid, vib.attrs, status); + private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo, + boolean shouldWriteStats) { + vib.end(vibrationEndInfo); + logVibrationStatus(vib.uid, vib.attrs, vibrationEndInfo.status); mVibratorManagerRecords.record(vib); + if (shouldWriteStats) { + mFrameworkStatsLogger.writeVibrationReportedAsync( + vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis())); + } } @GuardedBy("mLock") - private void endVibrationLocked(ExternalVibrationHolder vib, Vibration.Status status) { - vib.end(status); + private void endVibrationAndWriteStatsLocked(ExternalVibrationHolder vib, + Vibration.EndInfo vibrationEndInfo) { + vib.end(vibrationEndInfo); logVibrationStatus(vib.externalVibration.getUid(), - vib.externalVibration.getVibrationAttributes(), status); + vib.externalVibration.getVibrationAttributes(), vibrationEndInfo.status); mVibratorManagerRecords.record(vib); + mFrameworkStatsLogger.writeVibrationReportedAsync( + vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis())); } private void logVibrationStatus(int uid, VibrationAttributes attrs, Vibration.Status status) { @@ -744,15 +789,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void reportFinishedVibrationLocked(Vibration.Status status) { + private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked"); Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); try { Vibration vib = mCurrentVibration.getVibration(); if (DEBUG) { - Slog.d(TAG, "Reporting vibration " + vib.id + " finished with status " + status); + Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vibrationEndInfo); } - endVibrationLocked(vib, status); + // DO NOT write metrics at this point, wait for the VibrationThread to report the + // vibration was released, after all cleanup. The metrics will be reported then. + endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false); finishAppOpModeLocked(vib.uid, vib.opPkg); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); @@ -791,11 +838,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable private Vibration.Status shouldIgnoreVibrationForOngoingLocked(Vibration vib) { - if (mCurrentExternalVibration != null) { - // If something has external control of the vibrator, assume that it's more important. - return Vibration.Status.IGNORED_FOR_EXTERNAL; - } - if (mCurrentVibration == null || vib.isRepeating()) { // Incoming repeating vibrations always take precedence over ongoing vibrations. return null; @@ -1122,7 +1164,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } Vibration vib = conductor.getVibration(); return mVibrationSettings.shouldCancelVibrationOnScreenOff( - vib.uid, vib.opPkg, vib.attrs.getUsage(), vib.startUptimeMillis); + vib.uid, vib.opPkg, vib.attrs.getUsage(), vib.stats().getCreateUptimeMillis()); } @GuardedBy("mLock") @@ -1158,6 +1200,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { BatteryStats.SERVICE_NAME)); } + VibratorFrameworkStatsLogger getFrameworkStatsLogger(Handler handler) { + return new VibratorFrameworkStatsLogger(handler); + } + VibratorController createVibratorController(int vibratorId, VibratorController.OnVibrationCompleteListener listener) { return new VibratorController(vibratorId, listener); @@ -1197,6 +1243,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public void noteVibratorOn(int uid, long duration) { try { if (duration <= 0) { + // Tried to turn vibrator ON and got: + // duration == 0: Unsupported effect/method or zero-amplitude segment. + // duration < 0: Unexpected error triggering the vibrator. + // Skip battery stats and atom metric for VibratorStageChanged to ON. return; } if (duration == Long.MAX_VALUE) { @@ -1205,10 +1255,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION; } mBatteryStatsService.noteVibratorOn(uid, duration); - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, - uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, - duration); + mFrameworkStatsLogger.writeVibratorStateOnAsync(uid, duration); } catch (RemoteException e) { + Slog.e(TAG, "Error logging VibratorStateChanged to ON", e); } } @@ -1216,22 +1265,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public void noteVibratorOff(int uid) { try { mBatteryStatsService.noteVibratorOff(uid); - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, - uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, - /* duration= */ 0); + mFrameworkStatsLogger.writeVibratorStateOffAsync(uid); } catch (RemoteException e) { + Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e); } } @Override - public void onVibrationCompleted(long vibrationId, Vibration.Status status) { + public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) { if (DEBUG) { - Slog.d(TAG, "Vibration " + vibrationId + " finished with status " + status); + Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo); } synchronized (mLock) { if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) { - reportFinishedVibrationLocked(status); + reportFinishedVibrationLocked(vibrationEndInfo); } } } @@ -1251,13 +1299,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { "VibrationId mismatch on release. expected=%d, released=%d", mCurrentVibration.getVibration().id, vibrationId)); } - mCurrentVibration = null; + if (mCurrentVibration != null) { + // This is when we consider the current vibration complete, so report metrics. + mFrameworkStatsLogger.writeVibrationReportedAsync( + mCurrentVibration.getVibration().getStatsInfo( + /* completionUptimeMillis= */ SystemClock.uptimeMillis())); + mCurrentVibration = null; + } if (mNextVibration != null) { VibrationStepConductor nextConductor = mNextVibration; mNextVibration = null; Vibration.Status status = startVibrationOnThreadLocked(nextConductor); if (status != Vibration.Status.RUNNING) { - endVibrationLocked(nextConductor.getVibration(), status); + // Failed to start the vibration, end it and report metrics right away. + endVibrationLocked(nextConductor.getVibration(), + new Vibration.EndInfo(status), /* shouldWriteStats= */ true); } } } @@ -1325,31 +1381,48 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private final class ExternalVibrationHolder implements IBinder.DeathRecipient { public final ExternalVibration externalVibration; + public final VibrationStats stats = new VibrationStats(); public int scale; - private final long mStartUptimeMillis; - private final long mStartTimeDebug; - - private long mEndUptimeMillis; - private long mEndTimeDebug; private Vibration.Status mStatus; private ExternalVibrationHolder(ExternalVibration externalVibration) { this.externalVibration = externalVibration; this.scale = IExternalVibratorService.SCALE_NONE; - mStartUptimeMillis = SystemClock.uptimeMillis(); - mStartTimeDebug = System.currentTimeMillis(); mStatus = Vibration.Status.RUNNING; } - public void end(Vibration.Status status) { + public void mute() { + externalVibration.mute(); + } + + public void linkToDeath() { + externalVibration.linkToDeath(this); + } + + public void unlinkToDeath() { + externalVibration.unlinkToDeath(this); + } + + public boolean isHoldingSameVibration(ExternalVibration externalVibration) { + return this.externalVibration.equals(externalVibration); + } + + public void end(Vibration.EndInfo info) { if (mStatus != Vibration.Status.RUNNING) { - // Vibration already ended, keep first ending status set and ignore this one. + // Already ended, ignore this call return; } - mStatus = status; - mEndUptimeMillis = SystemClock.uptimeMillis(); - mEndTimeDebug = System.currentTimeMillis(); + mStatus = info.status; + stats.reportEnded(info.endedByUid, info.endedByUsage); + + if (stats.hasStarted()) { + // External vibration doesn't have feedback from total time the vibrator was playing + // with non-zero amplitude, so we use the duration between start and end times of + // the vibration as the time the vibrator was ON, since the haptic channels are + // open for this duration and can receive vibration waveform data. + stats.reportVibratorOn(stats.getEndUptimeMillis() - stats.getStartUptimeMillis()); + } } public void binderDied() { @@ -1358,19 +1431,26 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "External vibration finished because binder died"); } - endExternalVibrateLocked(Vibration.Status.CANCELLED_BINDER_DIED, + endExternalVibrateLocked( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BINDER_DIED), /* continueExternalControl= */ false); } } } public Vibration.DebugInfo getDebugInfo() { - long durationMs = mEndUptimeMillis == 0 ? -1 : mEndUptimeMillis - mStartUptimeMillis; return new Vibration.DebugInfo( - mStartTimeDebug, mEndTimeDebug, durationMs, - /* effect= */ null, /* originalEffect= */ null, scale, + mStatus, stats, /* effect= */ null, /* originalEffect= */ null, scale, externalVibration.getVibrationAttributes(), externalVibration.getUid(), - externalVibration.getPackage(), /* reason= */ null, mStatus); + externalVibration.getPackage(), /* reason= */ null); + } + + public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) { + return new VibrationStats.StatsInfo( + externalVibration.getUid(), + FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__EXTERNAL, + externalVibration.getVibrationAttributes().getUsage(), mStatus, stats, + completionUptimeMillis); } } @@ -1500,9 +1580,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** Clears mNextVibration if set, ending it cleanly */ @GuardedBy("mLock") - private void clearNextVibrationLocked(Vibration.Status endStatus) { + private void clearNextVibrationLocked(Vibration.EndInfo vibrationEndInfo) { if (mNextVibration != null) { - endVibrationLocked(mNextVibration.getVibration(), endStatus); + // Clearing next vibration before playing it, end it and report metrics right away. + endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo, + /* shouldWriteStats= */ true); mNextVibration = null; } } @@ -1510,25 +1592,25 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** * Ends the external vibration, and clears related service state. * - * @param status the status to end the associated Vibration with + * @param vibrationEndInfo the status and related info to end the associated Vibration with * @param continueExternalControl indicates whether external control will continue. If not, the * HAL will have external control turned off. */ @GuardedBy("mLock") - private void endExternalVibrateLocked(Vibration.Status status, + private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo, boolean continueExternalControl) { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "endExternalVibrateLocked"); try { if (mCurrentExternalVibration == null) { return; } - endVibrationLocked(mCurrentExternalVibration, status); - mCurrentExternalVibration.externalVibration.unlinkToDeath( - mCurrentExternalVibration); - mCurrentExternalVibration = null; + mCurrentExternalVibration.unlinkToDeath(); if (!continueExternalControl) { - setExternalControl(false); + setExternalControl(false, mCurrentExternalVibration.stats); } + // The external control was turned off, end it and report metrics right away. + endVibrationAndWriteStatsLocked(mCurrentExternalVibration, vibrationEndInfo); + mCurrentExternalVibration = null; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } @@ -1552,6 +1634,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return IExternalVibratorService.SCALE_MUTE; } + // Create Vibration.Stats as close to the received request as possible, for tracking. + ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib); VibrationAttributes attrs = fixupVibrationAttributes(vib.getVibrationAttributes(), /* effect= */ null); if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { @@ -1562,18 +1646,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { boolean alreadyUnderExternalControl = false; boolean waitForCompletion = false; - int scale; synchronized (mLock) { Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked( vib.getUid(), vib.getPackage(), attrs); if (ignoreStatus != null) { - ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib); vibHolder.scale = IExternalVibratorService.SCALE_MUTE; - endVibrationLocked(vibHolder, ignoreStatus); + // Failed to start the vibration, end it and report metrics right away. + endVibrationAndWriteStatsLocked(vibHolder, new Vibration.EndInfo(ignoreStatus)); return vibHolder.scale; } if (mCurrentExternalVibration != null - && mCurrentExternalVibration.externalVibration.equals(vib)) { + && mCurrentExternalVibration.isHoldingSameVibration(vib)) { // We are already playing this external vibration, so we can return the same // scale calculated in the previous call to this method. return mCurrentExternalVibration.scale; @@ -1582,8 +1665,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // If we're not under external control right now, then cancel any normal // vibration that may be playing and ready the vibrator for external control. if (mCurrentVibration != null) { - clearNextVibrationLocked(Vibration.Status.IGNORED_FOR_EXTERNAL); - mCurrentVibration.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, + vibHolder.stats.reportInterruptedAnotherVibration( + mCurrentVibration.getVibration().attrs.getUsage()); + clearNextVibrationLocked( + new Vibration.EndInfo(Vibration.Status.IGNORED_FOR_EXTERNAL, + vib.getUid(), attrs.getUsage())); + mCurrentVibration.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED, + vib.getUid(), attrs.getUsage()), /* immediate= */ true); waitForCompletion = true; } @@ -1597,22 +1686,27 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Note that this doesn't support multiple concurrent external controls, as we // would need to mute the old one still if it came from a different controller. alreadyUnderExternalControl = true; - mCurrentExternalVibration.externalVibration.mute(); - endExternalVibrateLocked(Vibration.Status.CANCELLED_SUPERSEDED, + mCurrentExternalVibration.mute(); + vibHolder.stats.reportInterruptedAnotherVibration( + mCurrentExternalVibration.externalVibration + .getVibrationAttributes().getUsage()); + endExternalVibrateLocked( + new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED, + vib.getUid(), attrs.getUsage()), /* continueExternalControl= */ true); } - mCurrentExternalVibration = new ExternalVibrationHolder(vib); - vib.linkToDeath(mCurrentExternalVibration); - mCurrentExternalVibration.scale = mVibrationScaler.getExternalVibrationScale( - attrs.getUsage()); - scale = mCurrentExternalVibration.scale; + mCurrentExternalVibration = vibHolder; + vibHolder.linkToDeath(); + vibHolder.scale = mVibrationScaler.getExternalVibrationScale(attrs.getUsage()); } if (waitForCompletion) { if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) { Slog.e(TAG, "Timed out waiting for vibration to cancel"); synchronized (mLock) { - endExternalVibrateLocked(Vibration.Status.IGNORED_ERROR_CANCELLING, + // Trigger endExternalVibrateLocked to unlink to death recipient. + endExternalVibrateLocked( + new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING), /* continueExternalControl= */ false); } return IExternalVibratorService.SCALE_MUTE; @@ -1622,23 +1716,27 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "Vibrator going under external control."); } - setExternalControl(true); + setExternalControl(true, vibHolder.stats); } if (DEBUG) { Slog.e(TAG, "Playing external vibration: " + vib); } - return scale; + // Vibrator will start receiving data from external channels after this point. + // Report current time as the vibration start time, for debugging. + vibHolder.stats.reportStarted(); + return vibHolder.scale; } @Override public void onExternalVibrationStop(ExternalVibration vib) { synchronized (mLock) { if (mCurrentExternalVibration != null - && mCurrentExternalVibration.externalVibration.equals(vib)) { + && mCurrentExternalVibration.isHoldingSameVibration(vib)) { if (DEBUG) { Slog.e(TAG, "Stopping external vibration" + vib); } - endExternalVibrateLocked(Vibration.Status.FINISHED, + endExternalVibrateLocked( + new Vibration.EndInfo(Vibration.Status.FINISHED), /* continueExternalControl= */ false); } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index be995a887fc2..5a1afc49c62b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2654,12 +2654,12 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { @Override public void accept(ActivityRecord r) { - if (r.finishing) { - return; - } if (r.mLaunchCookie != null) { mInfo.addLaunchCookie(r.mLaunchCookie); } + if (r.finishing) { + return; + } mInfo.numActivities++; mInfo.baseActivity = r.mActivityComponent; if (mTopRunning == null) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 3d91921e3ab7..8dd58506ef0b 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -399,9 +399,8 @@ public class DisplayRotation { return false; } - final ScreenRotationAnimation screenRotationAnimation = - mDisplayContent.getRotationAnimation(); - if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()) { + if (mDisplayContent.inTransition() + && !mDisplayContent.mTransitionController.useShellTransitionsRotation()) { // Rotation updates cannot be performed while the previous rotation change animation // is still in progress. Skip this update. We will try updating again after the // animation is finished and the display is unfrozen. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index bbc95a1dd70f..584a40e04700 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -622,9 +622,11 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId); } + boolean hasParticipatedDisplay = false; // Commit all going-invisible containers for (int i = 0; i < mParticipants.size(); ++i) { - final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); + final WindowContainer<?> participant = mParticipants.valueAt(i); + final ActivityRecord ar = participant.asActivityRecord(); if (ar != null) { boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar); // We need both the expected visibility AND current requested-visibility to be @@ -656,8 +658,13 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Legacy dispatch relies on this (for now). ar.mEnteringAnimation = visibleAtTransitionEnd; } + continue; } - final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken(); + if (participant.asDisplayContent() != null) { + hasParticipatedDisplay = true; + continue; + } + final WallpaperWindowToken wt = participant.asWallpaperToken(); if (wt != null) { final boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(wt); if (!visibleAtTransitionEnd && !wt.isVisibleRequested()) { @@ -737,6 +744,12 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mState = STATE_FINISHED; mController.mTransitionTracer.logState(this); + // Rotation change may be deferred while there is a display change transition, so check + // again in case there is a new pending change. + if (hasParticipatedDisplay && !mController.useShellTransitionsRotation()) { + mController.mAtm.mWindowManager.updateRotation(false /* alwaysSendConfiguration */, + false /* forceRelayout */); + } } void abort() { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 6544f821f13c..cae722ece7c7 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4302,7 +4302,9 @@ public class WindowManagerService extends IWindowManager.Stub // Even if alwaysSend, we are waiting for a transition or remote to provide // updated configuration, so we can't update configuration yet. if (!pendingRemoteDisplayChange) { - if (!rotationChanged || forceRelayout) { + // The layout-needed flag will be set if there is a rotation change, so + // only set it if the caller requests to force relayout. + if (forceRelayout) { displayContent.setLayoutNeeded(); layoutNeeded = true; } diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java index f9f6fe919c3b..831a69a8d890 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java @@ -20,6 +20,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import android.app.AlarmManager; @@ -92,7 +94,7 @@ public class AgentTest { Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); - doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString()); Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0); agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); @@ -121,7 +123,7 @@ public class AgentTest { Ledger ledger = new Ledger(); doReturn(1000L).when(mIrs).getConsumptionLimitLocked(); - doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString()); Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0); agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); @@ -168,7 +170,7 @@ public class AgentTest { Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); - doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance(); + doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString()); Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0); agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); @@ -187,7 +189,7 @@ public class AgentTest { assertEquals(1_000, ledger.getCurrentBalance()); // Shouldn't change in normal operation, but adding test case in case it does. - doReturn(900L).when(mEconomicPolicy).getMaxSatiatedBalance(); + doReturn(900L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString()); transaction = new Ledger.Transaction(0, 0, 0, null, 500, 0); agent.recordTransactionLocked(0, "com.test", ledger, transaction, false); diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java index da7664b82294..2fac31e9e6fd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java @@ -63,7 +63,7 @@ public class AgentTrendCalculatorTest { } @Override - long getMaxSatiatedBalance() { + long getMaxSatiatedBalance(int userId, String pkgName) { return 0; } diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java index 2e200c395e9d..fb3e8f298424 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java @@ -134,8 +134,11 @@ public class AlarmManagerEconomicPolicyTest { mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getHardSatiatedConsumptionLimit()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES, - mEconomicPolicy.getMaxSatiatedBalance()); + mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES, @@ -154,7 +157,10 @@ public class AlarmManagerEconomicPolicyTest { assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(10), mEconomicPolicy.getMaxSatiatedBalance()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(10), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(9), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); @@ -172,7 +178,10 @@ public class AlarmManagerEconomicPolicyTest { assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedBalance()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); @@ -187,7 +196,8 @@ public class AlarmManagerEconomicPolicyTest { assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance()); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); } diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java index 45c97e4c5d80..47155a1eadd3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java @@ -142,9 +142,12 @@ public class CompleteEconomicPolicyTest { assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES + EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getHardSatiatedConsumptionLimit()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES + EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES, - mEconomicPolicy.getMaxSatiatedBalance()); + mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES @@ -170,7 +173,10 @@ public class CompleteEconomicPolicyTest { assertEquals(arcToCake(10), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(50), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(20), mEconomicPolicy.getMaxSatiatedBalance()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(20), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java index 03ce91aea58b..19b798da5aab 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java @@ -134,8 +134,11 @@ public class JobSchedulerEconomicPolicyTest { mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getHardSatiatedConsumptionLimit()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, - mEconomicPolicy.getMaxSatiatedBalance()); + mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES, @@ -154,7 +157,10 @@ public class JobSchedulerEconomicPolicyTest { assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(10), mEconomicPolicy.getMaxSatiatedBalance()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(10), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(9), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); @@ -172,7 +178,10 @@ public class JobSchedulerEconomicPolicyTest { assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedBalance()); + final String pkgRestricted = "com.pkg.restricted"; + when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); @@ -187,7 +196,8 @@ public class JobSchedulerEconomicPolicyTest { assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit()); - assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance()); + assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); + assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index fa3fcd9e9475..235849c1cd8b 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -127,9 +127,17 @@ final class FakeVibratorControllerProvider { } @Override - public long compose(PrimitiveSegment[] effects, long vibrationId) { + public long compose(PrimitiveSegment[] primitives, long vibrationId) { + if (mSupportedPrimitives == null) { + return 0; + } + for (PrimitiveSegment primitive : primitives) { + if (Arrays.binarySearch(mSupportedPrimitives, primitive.getPrimitiveId()) < 0) { + return 0; + } + } long duration = 0; - for (PrimitiveSegment primitive : effects) { + for (PrimitiveSegment primitive : primitives) { duration += EFFECT_DURATION + primitive.getDelay(); recordEffectSegment(vibrationId, primitive); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationTest.java new file mode 100644 index 000000000000..b46929947fe4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationTest.java @@ -0,0 +1,46 @@ +/* + * 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.server.vibrator; + +import static com.google.common.truth.Truth.assertThat; + +import static java.util.stream.Collectors.toList; + +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; + +import java.util.Arrays; + +/** + * Tests for {@link Vibration}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:VibrationTest + */ +@Presubmit +public class VibrationTest { + + @Test + public void status_hasUniqueProtoEnumValues() { + assertThat( + Arrays.stream(Vibration.Status.values()) + .map(Vibration.Status::getProtoEnumValue) + .collect(toList())) + .containsNoDuplicates(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index de5f6ed2ae5d..ca162efe0f6e 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -257,13 +257,18 @@ public class VibrationThreadTest { assertTrue(mThread.isRunningVibrationId(vibrationId)); assertTrue(mControllers.get(VIBRATOR_ID).isVibrating()); - conductor.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ false); + Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo( + Vibration.Status.CANCELLED_SUPERSEDED, /* endedByUid= */ 1, + /* endedByUsage= */ VibrationAttributes.USAGE_ALARM); + conductor.notifyCancelled( + cancelVibrationInfo, + /* immediate= */ false); waitForCompletion(); assertFalse(mThread.isRunningVibrationId(vibrationId)); verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong()); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_SUPERSEDED); + verifyCallbacksTriggered(vibrationId, cancelVibrationInfo); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); List<Float> playedAmplitudes = fakeVibrator.getAmplitudes(); @@ -288,7 +293,9 @@ public class VibrationThreadTest { VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, effect); assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER); @@ -319,7 +326,9 @@ public class VibrationThreadTest { assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(), TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); // PWLE size max was used to generate a single vibrate call with 10 segments. @@ -348,11 +357,13 @@ public class VibrationThreadTest { assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(), TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ false); waitForCompletion(); // Composition size max was used to generate a single vibrate call with 10 primitives. - verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_SUPERSEDED); + verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size()); } @@ -370,7 +381,9 @@ public class VibrationThreadTest { VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, effect); assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER); @@ -394,7 +407,9 @@ public class VibrationThreadTest { assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1, 5000 + TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER); @@ -414,6 +429,8 @@ public class VibrationThreadTest { public void vibrate_singleVibratorPredefinedCancel_cancelsVibrationImmediately() throws Exception { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); long vibrationId = 1; VibrationEffect effect = VibrationEffect.startComposition() @@ -431,7 +448,9 @@ public class VibrationThreadTest { // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread(() -> conductor.notifyCancelled( - Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE, /* immediate= */ false)); + new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), + /* immediate= */ false)); cancellingThread.start(); waitForCompletion(/* timeout= */ 50); @@ -458,7 +477,9 @@ public class VibrationThreadTest { // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread(() -> conductor.notifyCancelled( - Vibration.Status.CANCELLED_BY_SCREEN_OFF, /* immediate= */ false)); + new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ false)); cancellingThread.start(); waitForCompletion(/* timeout= */ 50); @@ -519,7 +540,7 @@ public class VibrationThreadTest { startThreadAndDispatcher(vibrationId, effect); waitForCompletion(); - verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong()); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED); @@ -530,6 +551,8 @@ public class VibrationThreadTest { public void vibrate_singleVibratorComposed_runsVibration() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK, + VibrationEffect.Composition.PRIMITIVE_TICK); long vibrationId = 1; VibrationEffect effect = VibrationEffect.startComposition() @@ -559,7 +582,7 @@ public class VibrationThreadTest { startThreadAndDispatcher(vibrationId, effect); waitForCompletion(); - verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong()); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED); @@ -570,6 +593,10 @@ public class VibrationThreadTest { public void vibrate_singleVibratorLargeComposition_splitsVibratorComposeCalls() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + fakeVibrator.setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK, + VibrationEffect.Composition.PRIMITIVE_TICK, + VibrationEffect.Composition.PRIMITIVE_SPIN); fakeVibrator.setCompositionSizeMax(2); long vibrationId = 1; @@ -809,6 +836,8 @@ public class VibrationThreadTest { mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(4).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); long vibrationId = 1; VibrationEffect composed = VibrationEffect.startComposition() @@ -854,6 +883,8 @@ public class VibrationThreadTest { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK); long vibrationId = 1; @@ -902,7 +933,11 @@ public class VibrationThreadTest { long vibrationId = 1; mockVibrators(vibratorIds); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(1).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); when(mManagerHooks.prepareSyncedVibration(anyLong(), eq(vibratorIds))).thenReturn(true); when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true); @@ -939,6 +974,8 @@ public class VibrationThreadTest { mockVibrators(vibratorIds); mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(4).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); when(mManagerHooks.prepareSyncedVibration(anyLong(), any())).thenReturn(true); when(mManagerHooks.triggerSyncedVibration(anyLong())).thenReturn(true); @@ -1125,7 +1162,9 @@ public class VibrationThreadTest { // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread( () -> conductor.notifyCancelled( - Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false)); + new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false)); cancellingThread.start(); // Cancelling the vibration should be fast and return right away, even if the thread is @@ -1143,6 +1182,8 @@ public class VibrationThreadTest { mockVibrators(1, 2); mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); long vibrationId = 1; CombinedVibration effect = CombinedVibration.startParallel() @@ -1163,13 +1204,15 @@ public class VibrationThreadTest { // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread( () -> conductor.notifyCancelled( - Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ false)); + new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ false)); cancellingThread.start(); waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_SUPERSEDED); + verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); } @@ -1195,9 +1238,11 @@ public class VibrationThreadTest { // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. - Thread cancellingThread = - new Thread(() -> conductor.notifyCancelled( - Vibration.Status.CANCELLED_BY_SCREEN_OFF, /* immediate= */ false)); + Thread cancellingThread = new Thread( + () -> conductor.notifyCancelled( + new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ false)); cancellingThread.start(); waitForCompletion(/* timeout= */ 50); @@ -1266,7 +1311,7 @@ public class VibrationThreadTest { // Vibration completed but vibrator not yet released. verify(mManagerHooks, timeout(TEST_TIMEOUT_MILLIS)).onVibrationCompleted(eq(vibrationId), - eq(Vibration.Status.FINISHED)); + eq(new Vibration.EndInfo(Vibration.Status.FINISHED))); verify(mManagerHooks, never()).onVibrationThreadReleased(anyLong()); // Thread still running ramp down. @@ -1278,12 +1323,13 @@ public class VibrationThreadTest { // Will stop the ramp down right away. conductor.notifyCancelled( - Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE, /* immediate= */ true); + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), + /* immediate= */ true); waitForCompletion(); // Does not cancel already finished vibration, but releases vibrator. verify(mManagerHooks, never()).onVibrationCompleted(eq(vibrationId), - eq(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE)); + eq(new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE))); verify(mManagerHooks).onVibrationThreadReleased(vibrationId); } @@ -1299,7 +1345,9 @@ public class VibrationThreadTest { VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); - conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER); @@ -1422,7 +1470,9 @@ public class VibrationThreadTest { VibrationStepConductor conductor2 = startThreadAndDispatcher(vibrationId2, effect2); // Effect2 won't complete on its own. Cancel it after a couple of repeats. Thread.sleep(150); // More than two TICKs. - conductor2.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false); + conductor2.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), + /* immediate= */ false); waitForCompletion(); startThreadAndDispatcher(vibrationId3, effect3); @@ -1431,7 +1481,9 @@ public class VibrationThreadTest { // Effect4 is a long oneshot, but it gets cancelled as fast as possible. long start4 = System.currentTimeMillis(); VibrationStepConductor conductor4 = startThreadAndDispatcher(vibrationId4, effect4); - conductor4.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ true); + conductor4.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ true); waitForCompletion(); long duration4 = System.currentTimeMillis() - start4; @@ -1469,7 +1521,7 @@ public class VibrationThreadTest { fakeVibrator.getEffectSegments(vibrationId3)); // Effect4: cancelled quickly. - verifyCallbacksTriggered(vibrationId4, Vibration.Status.CANCELLED_SUPERSEDED); + verifyCallbacksTriggered(vibrationId4, Vibration.Status.CANCELLED_BY_SCREEN_OFF); assertTrue("Tested duration=" + duration4, duration4 < 2000); // Effect5: normal oneshot. Don't worry about amplitude, as effect4 may or may not have @@ -1580,7 +1632,11 @@ public class VibrationThreadTest { } private void verifyCallbacksTriggered(long vibrationId, Vibration.Status expectedStatus) { - verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedStatus)); + verifyCallbacksTriggered(vibrationId, new Vibration.EndInfo(expectedStatus)); + } + + private void verifyCallbacksTriggered(long vibrationId, Vibration.EndInfo expectedEndInfo) { + verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedEndInfo)); verify(mManagerHooks).onVibrationThreadReleased(vibrationId); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java new file mode 100644 index 000000000000..c1ab1db2732e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorFrameworkStatsLoggerTest.java @@ -0,0 +1,127 @@ +/* + * 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.server.vibrator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Handler; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Tests for {@link VibratorFrameworkStatsLogger}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:VibratorFrameworkStatsLoggerTest + */ +@Presubmit +public class VibratorFrameworkStatsLoggerTest { + + @Rule public MockitoRule rule = MockitoJUnit.rule(); + + private TestLooper mTestLooper; + private VibratorFrameworkStatsLogger mLogger; + + @Before + public void setUp() { + mTestLooper = new TestLooper(); + } + + @Test + public void writeVibrationReportedAsync_afterMinInterval_writesRightAway() { + setUpLogger(/* minIntervalMillis= */ 10, /* queueMaxSize= */ 10); + + VibrationStats.StatsInfo firstStats = newEmptyStatsInfo(); + assertFalse(firstStats.isWritten()); + + mLogger.writeVibrationReportedAsync(firstStats); + mTestLooper.dispatchAll(); + assertTrue(firstStats.isWritten()); + } + + @Test + public void writeVibrationReportedAsync_rightAfterLogging_schedulesToRunAfterRemainingDelay() { + setUpLogger(/* minIntervalMillis= */ 100, /* queueMaxSize= */ 10); + + VibrationStats.StatsInfo firstStats = newEmptyStatsInfo(); + VibrationStats.StatsInfo secondStats = newEmptyStatsInfo(); + assertFalse(firstStats.isWritten()); + assertFalse(secondStats.isWritten()); + + // Write first message at current SystemClock.uptimeMillis + mLogger.writeVibrationReportedAsync(firstStats); + mTestLooper.dispatchAll(); + assertTrue(firstStats.isWritten()); + + // Second message is not written right away, it needs to wait the configured interval. + mLogger.writeVibrationReportedAsync(secondStats); + mTestLooper.dispatchAll(); + assertFalse(secondStats.isWritten()); + + // Second message is written after delay passes. + mTestLooper.moveTimeForward(100); + mTestLooper.dispatchAll(); + assertTrue(secondStats.isWritten()); + } + + @Test + public void writeVibrationReportedAsync_tooFast_logsUsingIntervalAndDropsMessagesFromQueue() { + setUpLogger(/* minIntervalMillis= */ 100, /* queueMaxSize= */ 2); + + VibrationStats.StatsInfo firstStats = newEmptyStatsInfo(); + VibrationStats.StatsInfo secondStats = newEmptyStatsInfo(); + VibrationStats.StatsInfo thirdStats = newEmptyStatsInfo(); + + mLogger.writeVibrationReportedAsync(firstStats); + mLogger.writeVibrationReportedAsync(secondStats); + mLogger.writeVibrationReportedAsync(thirdStats); + + // Only first message is logged. + mTestLooper.dispatchAll(); + assertTrue(firstStats.isWritten()); + assertFalse(secondStats.isWritten()); + assertFalse(thirdStats.isWritten()); + + // Wait one interval to check only the second one is logged. + mTestLooper.moveTimeForward(100); + mTestLooper.dispatchAll(); + assertTrue(secondStats.isWritten()); + assertFalse(thirdStats.isWritten()); + + // Wait a long interval to check the third one was dropped and will never be logged. + mTestLooper.moveTimeForward(1_000); + mTestLooper.dispatchAll(); + assertFalse(thirdStats.isWritten()); + } + + private void setUpLogger(int minIntervalMillis, int queueMaxSize) { + mLogger = new VibratorFrameworkStatsLogger(new Handler(mTestLooper.getLooper()), + minIntervalMillis, queueMaxSize); + } + + private static VibrationStats.StatsInfo newEmptyStatsInfo() { + return new VibrationStats.StatsInfo( + 0, 0, 0, Vibration.Status.FINISHED, new VibrationStats(), 0L); + } +} diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 8a96febcd1e9..36bec750e3bc 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -75,11 +76,13 @@ import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.platform.test.annotations.Presubmit; import android.provider.Settings; +import android.util.SparseBooleanArray; import android.view.InputDevice; import androidx.test.InstrumentationRegistry; import com.android.internal.app.IBatteryStats; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.LocalServices; @@ -148,6 +151,8 @@ public class VibratorManagerServiceTest { private IInputManager mIInputManagerMock; @Mock private IBatteryStats mBatteryStatsMock; + @Mock + private VibratorFrameworkStatsLogger mVibratorFrameworkStatsLoggerMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); @@ -233,6 +238,11 @@ public class VibratorManagerServiceTest { } @Override + VibratorFrameworkStatsLogger getFrameworkStatsLogger(Handler handler) { + return mVibratorFrameworkStatsLoggerMock; + } + + @Override VibratorController createVibratorController(int vibratorId, VibratorController.OnVibrationCompleteListener listener) { return mVibratorProviders.get(vibratorId) @@ -806,11 +816,11 @@ public class VibratorManagerServiceTest { service, TEST_TIMEOUT_MILLIS)); VibrationEffect repeatingEffect = VibrationEffect.createWaveform( - new long[]{10_000, 10_000}, new int[]{128, 255}, 1); + new long[]{10, 10}, new int[]{128, 255}, 1); vibrate(service, repeatingEffect, NOTIFICATION_ATTRS); // VibrationThread will start this vibration async, so wait before checking it started. - assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, + assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 2, service, TEST_TIMEOUT_MILLIS)); // The second vibration should have recorded that the vibrators were turned on. @@ -916,7 +926,11 @@ public class VibratorManagerServiceTest { mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_COMPOSE); mockVibrators(1, 2); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(1).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK); // Mock alarm intensity equals to default value to avoid scaling in this test. setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY, mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_ALARM)); @@ -1078,6 +1092,8 @@ public class VibratorManagerServiceTest { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL, IVibrator.CAP_COMPOSE_EFFECTS); + fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK, + VibrationEffect.Composition.PRIMITIVE_TICK); VibratorManagerService service = createSystemReadyService(); vibrate(service, VibrationEffect.startComposition() @@ -1380,6 +1396,373 @@ public class VibratorManagerServiceTest { assertEquals(IExternalVibratorService.SCALE_MUTE, scale); } + @Test + public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + createSystemReadyService(); + + AudioAttributes audioAttrs = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .build(); + + ExternalVibration vib = new ExternalVibration(UID, PACKAGE_NAME, audioAttrs, + mock(IExternalVibrationController.class)); + mExternalVibratorService.onExternalVibrationStart(vib); + + Thread.sleep(10); + mExternalVibratorService.onExternalVibrationStop(vib); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo statsInfo = argumentCaptor.getValue(); + assertEquals(UID, statsInfo.uid); + assertEquals(FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__EXTERNAL, + statsInfo.vibrationType); + assertEquals(VibrationAttributes.USAGE_ALARM, statsInfo.usage); + assertEquals(Vibration.Status.FINISHED.getProtoEnumValue(), statsInfo.status); + assertTrue(statsInfo.totalDurationMillis > 0); + assertTrue( + "Expected vibrator ON for at least 10ms, got " + statsInfo.vibratorOnMillis + "ms", + statsInfo.vibratorOnMillis >= 10); + assertEquals(2, statsInfo.halSetExternalControlCount); + } + + @Test + public void frameworkStats_waveformVibration_reportsAllMetrics() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + + VibratorManagerService service = createSystemReadyService(); + vibrateAndWaitUntilFinished(service, + VibrationEffect.createWaveform(new long[] {0, 10, 20, 10}, -1), RINGTONE_ATTRS); + + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOnAsync(eq(UID), anyLong()); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOffAsync(eq(UID)); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo metrics = argumentCaptor.getValue(); + assertEquals(UID, metrics.uid); + assertEquals(FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE, + metrics.vibrationType); + assertEquals(VibrationAttributes.USAGE_RINGTONE, metrics.usage); + assertEquals(Vibration.Status.FINISHED.getProtoEnumValue(), metrics.status); + assertTrue("Total duration was too low, " + metrics.totalDurationMillis + "ms", + metrics.totalDurationMillis >= 20); + assertTrue("Vibrator ON duration was too low, " + metrics.vibratorOnMillis + "ms", + metrics.vibratorOnMillis >= 20); + + // All unrelated metrics are empty. + assertEquals(0, metrics.repeatCount); + assertEquals(0, metrics.halComposeCount); + assertEquals(0, metrics.halComposePwleCount); + assertEquals(0, metrics.halPerformCount); + assertEquals(0, metrics.halSetExternalControlCount); + assertEquals(0, metrics.halCompositionSize); + assertEquals(0, metrics.halPwleSize); + assertNull(metrics.halSupportedCompositionPrimitivesUsed); + assertNull(metrics.halSupportedEffectsUsed); + assertNull(metrics.halUnsupportedCompositionPrimitivesUsed); + assertNull(metrics.halUnsupportedEffectsUsed); + + // Accommodate for ramping off config that might add extra setAmplitudes. + assertEquals(2, metrics.halOnCount); + assertTrue(metrics.halOffCount > 0); + assertTrue(metrics.halSetAmplitudeCount >= 2); + } + + @Test + public void frameworkStats_repeatingVibration_reportsAllMetrics() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + + VibratorManagerService service = createSystemReadyService(); + vibrate(service, VibrationEffect.createWaveform(new long[] {10, 100}, 1), RINGTONE_ATTRS); + + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOnAsync(eq(UID), anyLong()); + + // Wait for at least one loop before cancelling it. + Thread.sleep(100); + service.cancelVibrate(VibrationAttributes.USAGE_RINGTONE, service); + + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOffAsync(eq(UID)); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo metrics = argumentCaptor.getValue(); + assertEquals(UID, metrics.uid); + assertEquals(FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED, + metrics.vibrationType); + assertEquals(VibrationAttributes.USAGE_RINGTONE, metrics.usage); + assertEquals(Vibration.Status.CANCELLED_BY_USER.getProtoEnumValue(), metrics.status); + assertTrue("Total duration was too low, " + metrics.totalDurationMillis + "ms", + metrics.totalDurationMillis >= 100); + assertTrue("Vibrator ON duration was too low, " + metrics.vibratorOnMillis + "ms", + metrics.vibratorOnMillis >= 100); + + // All unrelated metrics are empty. + assertTrue(metrics.repeatCount > 0); + assertEquals(0, metrics.halComposeCount); + assertEquals(0, metrics.halComposePwleCount); + assertEquals(0, metrics.halPerformCount); + assertEquals(0, metrics.halSetExternalControlCount); + assertEquals(0, metrics.halCompositionSize); + assertEquals(0, metrics.halPwleSize); + assertNull(metrics.halSupportedCompositionPrimitivesUsed); + assertNull(metrics.halSupportedEffectsUsed); + assertNull(metrics.halUnsupportedCompositionPrimitivesUsed); + assertNull(metrics.halUnsupportedEffectsUsed); + + // Accommodate for ramping off config that might add extra setAmplitudes. + assertTrue(metrics.halOnCount > 0); + assertTrue(metrics.halOffCount > 0); + assertTrue(metrics.halSetAmplitudeCount > 0); + } + + @Test + public void frameworkStats_prebakedAndComposedVibrations_reportsAllMetrics() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + mVibratorProviders.get(1).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_TICK); + + VibratorManagerService service = createSystemReadyService(); + vibrateAndWaitUntilFinished(service, + VibrationEffect.startComposition() + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(), + ALARM_ATTRS); + + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOnAsync(eq(UID), anyLong()); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOffAsync(eq(UID)); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo metrics = argumentCaptor.getValue(); + assertEquals(UID, metrics.uid); + assertEquals(FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE, + metrics.vibrationType); + assertEquals(VibrationAttributes.USAGE_ALARM, metrics.usage); + assertEquals(Vibration.Status.FINISHED.getProtoEnumValue(), metrics.status); + + // At least 4 effect/primitive played, 20ms each, plus configured fallback. + assertTrue("Total duration was too low, " + metrics.totalDurationMillis + "ms", + metrics.totalDurationMillis >= 80); + assertTrue("Vibrator ON duration was too low, " + metrics.vibratorOnMillis + "ms", + metrics.vibratorOnMillis >= 80); + + // Related metrics were collected. + assertEquals(2, metrics.halComposeCount); // TICK+TICK, then CLICK+CLICK + assertEquals(3, metrics.halPerformCount); // CLICK, TICK, then CLICK + assertEquals(4, metrics.halCompositionSize); // 2*TICK + 2*CLICK + // No repetitions in reported effect/primitive IDs. + assertArrayEquals(new int[] {VibrationEffect.Composition.PRIMITIVE_TICK}, + metrics.halSupportedCompositionPrimitivesUsed); + assertArrayEquals(new int[] {VibrationEffect.Composition.PRIMITIVE_CLICK}, + metrics.halUnsupportedCompositionPrimitivesUsed); + assertArrayEquals(new int[] {VibrationEffect.EFFECT_CLICK}, + metrics.halSupportedEffectsUsed); + assertArrayEquals(new int[] {VibrationEffect.EFFECT_TICK}, + metrics.halUnsupportedEffectsUsed); + + // All unrelated metrics are empty. + assertEquals(0, metrics.repeatCount); + assertEquals(0, metrics.halComposePwleCount); + assertEquals(0, metrics.halSetExternalControlCount); + assertEquals(0, metrics.halPwleSize); + + // Accommodate for ramping off config that might add extra setAmplitudes + // for the effect that plays the fallback instead of "perform". + assertTrue(metrics.halOnCount > 0); + assertTrue(metrics.halOffCount > 0); + assertTrue(metrics.halSetAmplitudeCount > 0); + } + + @Test + public void frameworkStats_interruptingVibrations_reportsAllMetrics() throws Exception { + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + + vibrate(service, VibrationEffect.createOneShot(1_000, 128), HAPTIC_FEEDBACK_ATTRS); + + // VibrationThread will start this vibration async, so wait until vibration is triggered. + assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(), + service, TEST_TIMEOUT_MILLIS)); + + vibrateAndWaitUntilFinished(service, VibrationEffect.createOneShot(10, 255), ALARM_ATTRS); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS).times(2)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo touchMetrics = argumentCaptor.getAllValues().get(0); + assertEquals(UID, touchMetrics.uid); + assertEquals(VibrationAttributes.USAGE_TOUCH, touchMetrics.usage); + assertEquals(Vibration.Status.CANCELLED_SUPERSEDED.getProtoEnumValue(), + touchMetrics.status); + assertTrue(touchMetrics.endedBySameUid); + assertEquals(VibrationAttributes.USAGE_ALARM, touchMetrics.endedByUsage); + assertEquals(-1, touchMetrics.interruptedUsage); + + VibrationStats.StatsInfo alarmMetrics = argumentCaptor.getAllValues().get(1); + assertEquals(UID, alarmMetrics.uid); + assertEquals(VibrationAttributes.USAGE_ALARM, alarmMetrics.usage); + assertEquals(Vibration.Status.FINISHED.getProtoEnumValue(), alarmMetrics.status); + assertFalse(alarmMetrics.endedBySameUid); + assertEquals(-1, alarmMetrics.endedByUsage); + assertEquals(VibrationAttributes.USAGE_TOUCH, alarmMetrics.interruptedUsage); + } + + @Test + public void frameworkStats_ignoredVibration_reportsStatus() throws Exception { + setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_OFF); + + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + + // Haptic feedback ignored in low power state + vibrateAndWaitUntilFinished(service, VibrationEffect.createOneShot(100, 128), + HAPTIC_FEEDBACK_ATTRS); + // Ringtone vibration user settings are off + vibrateAndWaitUntilFinished(service, VibrationEffect.createOneShot(200, 128), + RINGTONE_ATTRS); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS).times(2)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo touchMetrics = argumentCaptor.getAllValues().get(0); + assertEquals(UID, touchMetrics.uid); + assertEquals(VibrationAttributes.USAGE_TOUCH, touchMetrics.usage); + assertEquals(Vibration.Status.IGNORED_FOR_POWER.getProtoEnumValue(), touchMetrics.status); + + VibrationStats.StatsInfo ringtoneMetrics = argumentCaptor.getAllValues().get(1); + assertEquals(UID, ringtoneMetrics.uid); + assertEquals(VibrationAttributes.USAGE_RINGTONE, ringtoneMetrics.usage); + assertEquals(Vibration.Status.IGNORED_FOR_SETTINGS.getProtoEnumValue(), + ringtoneMetrics.status); + + for (VibrationStats.StatsInfo metrics : argumentCaptor.getAllValues()) { + // Latencies are empty since vibrations never started + assertEquals(0, metrics.startLatencyMillis); + assertEquals(0, metrics.endLatencyMillis); + assertEquals(0, metrics.vibratorOnMillis); + + // All unrelated metrics are empty. + assertEquals(0, metrics.repeatCount); + assertEquals(0, metrics.halComposeCount); + assertEquals(0, metrics.halComposePwleCount); + assertEquals(0, metrics.halOffCount); + assertEquals(0, metrics.halOnCount); + assertEquals(0, metrics.halPerformCount); + assertEquals(0, metrics.halSetExternalControlCount); + assertEquals(0, metrics.halCompositionSize); + assertEquals(0, metrics.halPwleSize); + assertNull(metrics.halSupportedCompositionPrimitivesUsed); + assertNull(metrics.halSupportedEffectsUsed); + assertNull(metrics.halUnsupportedCompositionPrimitivesUsed); + assertNull(metrics.halUnsupportedEffectsUsed); + } + } + + @Test + public void frameworkStats_multiVibrators_reportsAllMetrics() throws Exception { + mockVibrators(1, 2); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(1).setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_TICK); + mVibratorProviders.get(2).setSupportedEffects(VibrationEffect.EFFECT_TICK); + + VibratorManagerService service = createSystemReadyService(); + vibrateAndWaitUntilFinished(service, + CombinedVibration.startParallel() + .addVibrator(1, + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) + .compose()) + .addVibrator(2, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)) + .combine(), + NOTIFICATION_ATTRS); + + SparseBooleanArray expectedEffectsUsed = new SparseBooleanArray(); + expectedEffectsUsed.put(VibrationEffect.EFFECT_TICK, true); + + SparseBooleanArray expectedPrimitivesUsed = new SparseBooleanArray(); + expectedPrimitivesUsed.put(VibrationEffect.Composition.PRIMITIVE_TICK, true); + + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOnAsync(eq(UID), anyLong()); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibratorStateOffAsync(eq(UID)); + + ArgumentCaptor<VibrationStats.StatsInfo> argumentCaptor = + ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(argumentCaptor.capture()); + + VibrationStats.StatsInfo metrics = argumentCaptor.getValue(); + assertEquals(UID, metrics.uid); + assertEquals(FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE, + metrics.vibrationType); + assertEquals(VibrationAttributes.USAGE_NOTIFICATION, metrics.usage); + assertEquals(Vibration.Status.FINISHED.getProtoEnumValue(), metrics.status); + assertTrue(metrics.totalDurationMillis >= 20); + + // vibratorOnMillis accumulates both vibrators, it's 20 for each constant. + assertEquals(40, metrics.vibratorOnMillis); + + // Related metrics were collected. + assertEquals(1, metrics.halComposeCount); + assertEquals(1, metrics.halPerformCount); + assertEquals(1, metrics.halCompositionSize); + assertEquals(2, metrics.halOffCount); + assertArrayEquals(new int[] {VibrationEffect.Composition.PRIMITIVE_TICK}, + metrics.halSupportedCompositionPrimitivesUsed); + assertArrayEquals(new int[] {VibrationEffect.EFFECT_TICK}, + metrics.halSupportedEffectsUsed); + + // All unrelated metrics are empty. + assertEquals(0, metrics.repeatCount); + assertEquals(0, metrics.halComposePwleCount); + assertEquals(0, metrics.halOnCount); + assertEquals(0, metrics.halSetAmplitudeCount); + assertEquals(0, metrics.halSetExternalControlCount); + assertEquals(0, metrics.halPwleSize); + assertNull(metrics.halUnsupportedCompositionPrimitivesUsed); + assertNull(metrics.halUnsupportedEffectsUsed); + } + private VibrationEffectSegment expectedPrebaked(int effectId) { return expectedPrebaked(effectId, VibrationEffect.EFFECT_STRENGTH_MEDIUM); } @@ -1429,6 +1812,20 @@ public class VibratorManagerServiceTest { mContextSpy.getContentResolver(), settingName, value, UserHandle.USER_CURRENT); } + private void vibrateAndWaitUntilFinished(VibratorManagerService service, VibrationEffect effect, + VibrationAttributes attrs) throws InterruptedException { + vibrateAndWaitUntilFinished(service, CombinedVibration.createParallel(effect), attrs); + } + + private void vibrateAndWaitUntilFinished(VibratorManagerService service, + CombinedVibration effect, VibrationAttributes attrs) throws InterruptedException { + Vibration vib = + service.vibrateInternal(UID, PACKAGE_NAME, effect, attrs, "some reason", service); + if (vib != null) { + vib.waitForEnd(); + } + } + private void vibrate(VibratorManagerService service, VibrationEffect effect, VibrationAttributes attrs) { vibrate(service, CombinedVibration.createParallel(effect), attrs); 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 5146616a5cab..15d1a3c48ccd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2314,6 +2314,8 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(launchCookie, activity2.mLaunchCookie); assertNull(activity1.mLaunchCookie); + activity2.makeFinishingLocked(); + assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie)); } private void verifyProcessInfoUpdate(ActivityRecord activity, State state, @@ -2464,6 +2466,7 @@ public class ActivityRecordTests extends WindowTestsBase { activity.addWindow(appWindow); spyOn(appWindow); doNothing().when(appWindow).onStartFreezingScreen(); + doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt()); // Set initial orientation and update. performRotation(displayRotation, Surface.ROTATION_90); @@ -2472,8 +2475,6 @@ public class ActivityRecordTests extends WindowTestsBase { // Update the rotation to perform 180 degree rotation and check that resize was reported. performRotation(displayRotation, Surface.ROTATION_270); assertTrue(appWindow.mResizeReported); - - appWindow.removeImmediately(); } private void performRotation(DisplayRotation spiedRotation, int rotationToReport) { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 28d2aa157e0a..7a73f082c25e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1832,19 +1832,18 @@ public class DisplayContentTests extends WindowTestsBase { @Test public void testRemoteRotation() { - DisplayContent dc = createNewDisplay(); - + final DisplayContent dc = mDisplayContent; final DisplayRotation dr = dc.getDisplayRotation(); - doCallRealMethod().when(dr).updateRotationUnchecked(anyBoolean()); + spyOn(dr); // Rotate 180 degree so the display doesn't have configuration change. This condition is // used for the later verification of stop-freezing (without setting mWaitingForConfig). doReturn((dr.getRotation() + 2) % 4).when(dr).rotationForOrientation(anyInt(), anyInt()); final boolean[] continued = new boolean[1]; - doAnswer( - invocation -> { - continued[0] = true; - return true; - }).when(dc).updateDisplayOverrideConfigurationLocked(); + doAnswer(invocation -> { + continued[0] = true; + mAtm.addWindowLayoutReasons(ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED); + return true; + }).when(dc).updateDisplayOverrideConfigurationLocked(); final boolean[] called = new boolean[1]; mWm.mDisplayChangeController = new IDisplayChangeWindowController.Stub() { diff --git a/telephony/OWNERS b/telephony/OWNERS index 9681ee8b6c75..e0c5f8fa214e 100644 --- a/telephony/OWNERS +++ b/telephony/OWNERS @@ -10,3 +10,10 @@ jayachandranc@google.com chinmayd@google.com amruthr@google.com sasindran@google.com + +# Temporarily reduced the owner during refactoring +per-file SubscriptionManager.java=set noparent +per-file SubscriptionManager.java=jackyu@google.com,amruthr@google.com +per-file SubscriptionInfo.java=set noparent +per-file SubscriptionInfo.java=jackyu@google.com,amruthr@google.com + diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 2453f3cafaaa..d5b4434a136c 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -9088,7 +9088,7 @@ public class TelephonyManager { * @param executor The executor through which the callback should be invoked. Since the scan * request may trigger multiple callbacks and they must be invoked in the same order as * they are received by the platform, the user should provide an executor which executes - * tasks one at a time in serial order. For example AsyncTask.SERIAL_EXECUTOR. + * tasks one at a time in serial order. * @param callback Returns network scan results or errors. * @return A NetworkScan obj which contains a callback which can be used to stop the scan. */ @@ -9132,7 +9132,7 @@ public class TelephonyManager { * @param executor The executor through which the callback should be invoked. Since the scan * request may trigger multiple callbacks and they must be invoked in the same order as * they are received by the platform, the user should provide an executor which executes - * tasks one at a time in serial order. For example AsyncTask.SERIAL_EXECUTOR. + * tasks one at a time in serial order. * @param callback Returns network scan results or errors. * @return A NetworkScan obj which contains a callback which can be used to stop the scan. */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index 5e21252f3ebd..472a0fa376bf 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -43,6 +43,19 @@ fun FlickerTestParameter.navBarWindowIsAlwaysVisible() { } /** + * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the start + * and end of the WM trace + */ +fun FlickerTestParameter.navBarWindowIsVisibleAtStartAndEnd() { + assertWmStart { + this.isAboveAppWindowVisible(ComponentMatcher.NAV_BAR) + } + assertWmEnd { + this.isAboveAppWindowVisible(ComponentMatcher.NAV_BAR) + } +} + +/** * Checks that [ComponentMatcher.TASK_BAR] window is visible and above the app windows in * all WM trace entries */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt index 457e973392f7..a8c0a0b55009 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt @@ -17,14 +17,18 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd +import com.android.server.wm.traces.common.ComponentMatcher import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -65,4 +69,20 @@ class SwitchImeWindowsFromGestureNavTest_ShellTransit( @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Ignore("Nav bar window becomes invisible during quick switch") + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** + * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the start + * and end of the WM trace + */ + @Presubmit + @Test + fun navBarWindowIsVisibleAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarWindowIsVisibleAtStartAndEnd() + } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt index e007fe354994..2607ee5bb0ef 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt @@ -17,14 +17,18 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd +import com.android.server.wm.traces.common.ComponentMatcher import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -76,4 +80,20 @@ open class QuickSwitchBetweenTwoAppsBackTest_ShellTransit( @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Ignore("Nav bar window becomes invisible during quick switch") + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** + * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the start + * and end of the WM trace + */ + @Presubmit + @Test + fun navBarWindowIsVisibleAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarWindowIsVisibleAtStartAndEnd() + } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt index 6f78ba8dc0f6..27ae12566e94 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt @@ -17,14 +17,18 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd +import com.android.server.wm.traces.common.ComponentMatcher import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -66,4 +70,20 @@ open class QuickSwitchBetweenTwoAppsForwardTest_ShellTransit( @FlakyTest(bugId = 228009808) @Test override fun endsWithApp2BeingOnTop() = super.endsWithApp2BeingOnTop() + + /** {@inheritDoc} */ + @Ignore("Nav bar window becomes invisible during quick switch") + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** + * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the start + * and end of the WM trace + */ + @Presubmit + @Test + fun navBarWindowIsVisibleAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarWindowIsVisibleAtStartAndEnd() + } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index 510043b680e5..c79b55251c74 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -29,9 +29,12 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd import com.android.server.wm.traces.common.ComponentMatcher import com.android.server.wm.traces.common.Rect +import org.junit.Assume import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -296,6 +299,22 @@ class QuickSwitchFromLauncherTest(testSpec: FlickerTestParameter) : BaseTest(tes override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() + /** {@inheritDoc} */ + @Ignore("Nav bar window becomes invisible during quick switch") + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** + * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the start + * and end of the WM trace + */ + @Presubmit + @Test + fun navBarWindowIsVisibleAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarWindowIsVisibleAtStartAndEnd() + } + companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect.EMPTY |