diff options
| author | 2025-01-03 22:03:03 +0000 | |
|---|---|---|
| committer | 2025-01-25 00:32:47 +0000 | |
| commit | 9ef0692a504a73b989208bf2a49ddcb82fbde549 (patch) | |
| tree | fc18f311f1a4a1e3ebb13de78822ec230f48a22a | |
| parent | 2fb05f5a565bb47caa5de6076de278c673def147 (diff) | |
Include bitmap memory estimate in AppWidgetServiceImpl dumpsys output
This change adds a new field "view_bitmap_memory" to the
AppWidgetServiceImpl dumpsys output, which holds the estimate of bitmap
memory used by a widget held in memory by the service.
To support this, a new method RemoteViews.estimateTotalBitmapMemoryUsage
is added, which includes memory from both bitmap and Icon actions.
Also updates RemoteViews.estimateMemoryUsage to return a long. The
int sum can be overflowed to beat the memory check in
AppWidgetServiceImpl.
Bug: 377356756
Test: RemoteViewsTest#estimateMemoryUsage_*
Flag: EXEMPT bugfix
Change-Id: Id37d88bad93d154826e132dc243e24daf27d3905
6 files changed, 238 insertions, 8 deletions
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7c75d7b30037..2deb685f9411 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -160,6 +160,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -720,6 +721,11 @@ public class RemoteViews implements Parcelable, Filter { // Nothing to visit by default. } + /** See {@link RemoteViews#visitIcons(Consumer)}. **/ + public void visitIcons(@NonNull Consumer<Icon> visitor) { + // Nothing to visit by default. + } + public abstract void writeToParcel(Parcel dest, int flags); /** @@ -850,6 +856,29 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Note all {@link Icon} that are referenced internally. + * @hide + */ + public void visitIcons(@NonNull Consumer<Icon> visitor) { + if (mActions != null) { + for (int i = 0; i < mActions.size(); i++) { + mActions.get(i).visitIcons(visitor); + } + } + if (mSizedRemoteViews != null) { + for (int i = 0; i < mSizedRemoteViews.size(); i++) { + mSizedRemoteViews.get(i).visitIcons(visitor); + } + } + if (mLandscape != null) { + mLandscape.visitIcons(visitor); + } + if (mPortrait != null) { + mPortrait.visitIcons(visitor); + } + } + + /** * @hide * @return True if there is a change */ @@ -1313,6 +1342,19 @@ public class RemoteViews implements Parcelable, Filter { } @Override + public void visitIcons(Consumer<Icon> visitor) { + if (mItems == null) { + RemoteCollectionItems cachedItems = mCollectionCache.getItemsForId(mIntentId); + if (cachedItems != null) { + cachedItems.visitIcons(visitor); + } + return; + } + + mItems.visitIcons(visitor); + } + + @Override public boolean canWriteToProto() { // Skip actions that do not contain items (intent only actions) return mItems != null; @@ -2386,7 +2428,7 @@ public class RemoteViews implements Parcelable, Filter { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) ArrayList<Bitmap> mBitmaps; SparseIntArray mBitmapHashes; - int mBitmapMemory = -1; + long mBitmapMemory = -1; public BitmapCache() { mBitmaps = new ArrayList<>(); @@ -2450,7 +2492,7 @@ public class RemoteViews implements Parcelable, Filter { } } - public int getBitmapMemory() { + public long getBitmapMemory() { if (mBitmapMemory < 0) { mBitmapMemory = 0; int count = mBitmaps.size(); @@ -2736,6 +2778,13 @@ public class RemoteViews implements Parcelable, Filter { // TODO(b/281044385): Should we do anything about type BUNDLE? } } + + @Override + public void visitIcons(@NonNull Consumer<Icon> visitor) { + if (mType == ICON && getParameterValue(null) instanceof Icon icon) { + visitor.accept(icon); + } + } } /** Class for the reflection actions. */ @@ -4140,6 +4189,11 @@ public class RemoteViews implements Parcelable, Filter { } @Override + public void visitIcons(@NonNull Consumer<Icon> visitor) { + mNestedViews.visitIcons(visitor); + } + + @Override public boolean canWriteToProto() { return true; } @@ -6393,15 +6447,43 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Returns an estimate of the bitmap heap memory usage for this RemoteViews. + * Returns an estimate of the bitmap heap memory usage by setBitmap and setImageViewBitmap in + * this RemoteViews. + * + * @hide */ - /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public int estimateMemoryUsage() { + public long estimateMemoryUsage() { return mBitmapCache.getBitmapMemory(); } /** + * Returns an estimate of bitmap heap memory usage by setIcon and setImageViewIcon in this + * RemoteViews. Note that this function will count duplicate Icons in its estimate. + * + * @hide + */ + public long estimateIconMemoryUsage() { + AtomicLong total = new AtomicLong(0); + visitIcons(icon -> { + if (icon.getType() == Icon.TYPE_BITMAP || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { + total.addAndGet(icon.getBitmap().getAllocationByteCount()); + } + }); + return total.get(); + } + + /** + * Returns an estimate of the bitmap heap memory usage for all Icon and Bitmap actions in this + * RemoteViews. + * + * @hide + */ + public long estimateTotalBitmapMemoryUsage() { + return estimateMemoryUsage() + estimateIconMemoryUsage(); + } + + /** * Add an action to be executed on the remote side when apply is called. * * @param a The action to add @@ -9763,6 +9845,15 @@ public class RemoteViews implements Parcelable, Filter { view.visitUris(visitor); } } + + /** + * See {@link RemoteViews#visitIcons(Consumer)}. + */ + private void visitIcons(@NonNull Consumer<Icon> visitor) { + for (RemoteViews view : mViews) { + view.visitIcons(visitor); + } + } } /** diff --git a/core/proto/android/service/appwidget.proto b/core/proto/android/service/appwidget.proto index fb907196bfc7..5dc90e02faf5 100644 --- a/core/proto/android/service/appwidget.proto +++ b/core/proto/android/service/appwidget.proto @@ -39,6 +39,7 @@ message WidgetProto { optional int32 maxWidth = 8; optional int32 maxHeight = 9; optional bool restoreCompleted = 10; + optional int32 views_bitmap_memory = 11; } // represents a set of widget previews for a particular provider diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 2880ecf835c4..8b0d3158e9e7 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -36,6 +36,7 @@ import android.app.PendingIntent; import android.appwidget.AppWidgetHostView; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.AsyncTask; @@ -934,6 +935,136 @@ public class RemoteViewsTest { assertEquals(testText, replacedTextView.getText()); } + @Test + public void estimateMemoryUsage() { + Bitmap b1 = Bitmap.createBitmap(1024, 768, Bitmap.Config.ARGB_8888); + int b1Memory = b1.getAllocationByteCount(); + Bitmap b2 = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888); + int b2Memory = b2.getAllocationByteCount(); + Bitmap b3 = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888); + int b3Memory = b3.getAllocationByteCount(); + + final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test); + assertEquals(0, rv.estimateMemoryUsage()); + assertEquals(0, rv.estimateIconMemoryUsage()); + assertEquals(0, rv.estimateTotalBitmapMemoryUsage()); + + rv.setBitmap(R.id.view, "", b1); + rv.setImageViewBitmap(R.id.view, b1); // second instance of b1 is cached + rv.setBitmap(R.id.view, "", b2); + assertEquals(b1Memory + b2Memory, rv.estimateMemoryUsage()); + assertEquals(0, rv.estimateIconMemoryUsage()); + assertEquals(b1Memory + b2Memory, rv.estimateTotalBitmapMemoryUsage()); + + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b2)); + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b3)); + rv.setImageViewIcon(R.id.view, Icon.createWithBitmap(b3)); + assertEquals(b1Memory + b2Memory, rv.estimateMemoryUsage()); + assertEquals(b2Memory + (2 * b3Memory), rv.estimateIconMemoryUsage()); + assertEquals(b1Memory + (2 * b2Memory) + (2 * b3Memory), + rv.estimateTotalBitmapMemoryUsage()); + } + + @Test + public void estimateMemoryUsage_landscapePortrait() { + Bitmap b1 = Bitmap.createBitmap(1024, 768, Bitmap.Config.ARGB_8888); + int b1Memory = b1.getAllocationByteCount(); + Bitmap b2 = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888); + int b2Memory = b2.getAllocationByteCount(); + Bitmap b3 = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888); + int b3Memory = b3.getAllocationByteCount(); + Bitmap b4 = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888); + int b4Memory = b4.getAllocationByteCount(); + + // Landscape and portrait using same bitmaps get counted twice. + final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.setBitmap(R.id.view, "", b1); + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b2)); + RemoteViews landscapePortraitViews = new RemoteViews(rv, rv); + assertEquals(b1Memory, landscapePortraitViews.estimateMemoryUsage()); + assertEquals(2 * b2Memory, landscapePortraitViews.estimateIconMemoryUsage()); + assertEquals(b1Memory + (2 * b2Memory), + landscapePortraitViews.estimateTotalBitmapMemoryUsage()); + + final RemoteViews rv2 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.setBitmap(R.id.view, "", b3); + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b4)); + landscapePortraitViews = new RemoteViews(rv, rv2); + assertEquals(b1Memory + b3Memory, landscapePortraitViews.estimateMemoryUsage()); + assertEquals(b2Memory + b4Memory, landscapePortraitViews.estimateIconMemoryUsage()); + assertEquals(b1Memory + b2Memory + b3Memory + b4Memory, + landscapePortraitViews.estimateTotalBitmapMemoryUsage()); + } + + @Test + public void estimateMemoryUsage_sizedViews() { + Bitmap b1 = Bitmap.createBitmap(1024, 768, Bitmap.Config.ARGB_8888); + int b1Memory = b1.getAllocationByteCount(); + Bitmap b2 = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888); + int b2Memory = b2.getAllocationByteCount(); + Bitmap b3 = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888); + int b3Memory = b3.getAllocationByteCount(); + Bitmap b4 = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888); + int b4Memory = b4.getAllocationByteCount(); + + // Sized views using same bitmaps do not get counted twice. + final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.setBitmap(R.id.view, "", b1); + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b2)); + RemoteViews sizedViews = new RemoteViews( + Map.of(new SizeF(0f, 0f), rv, new SizeF(1f, 1f), rv)); + assertEquals(b1Memory, sizedViews.estimateMemoryUsage()); + assertEquals(2 * b2Memory, sizedViews.estimateIconMemoryUsage()); + assertEquals(b1Memory + (2 * b2Memory), sizedViews.estimateTotalBitmapMemoryUsage()); + + final RemoteViews rv2 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.setBitmap(R.id.view, "", b3); + rv.setIcon(R.id.view, "", Icon.createWithBitmap(b4)); + sizedViews = new RemoteViews(Map.of(new SizeF(0f, 0f), rv, new SizeF(1f, 1f), rv2)); + assertEquals(b1Memory + b3Memory, sizedViews.estimateMemoryUsage()); + assertEquals(b2Memory + b4Memory, sizedViews.estimateIconMemoryUsage()); + assertEquals(b1Memory + b2Memory + b3Memory + b4Memory, + sizedViews.estimateTotalBitmapMemoryUsage()); + } + + @Test + public void estimateMemoryUsage_nestedViews() { + Bitmap b1 = Bitmap.createBitmap(1024, 768, Bitmap.Config.ARGB_8888); + int b1Memory = b1.getAllocationByteCount(); + Bitmap b2 = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888); + int b2Memory = b2.getAllocationByteCount(); + + final RemoteViews rv1 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv1.setBitmap(R.id.view, "", b1); + final RemoteViews rv2 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv2.setIcon(R.id.view, "", Icon.createWithBitmap(b2)); + final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.addView(R.id.view, rv1); + rv.addView(R.id.view, rv2); + assertEquals(b1Memory, rv.estimateMemoryUsage()); + assertEquals(b2Memory, rv.estimateIconMemoryUsage()); + assertEquals(b1Memory + b2Memory, rv.estimateTotalBitmapMemoryUsage()); + } + + @Test + public void estimateMemoryUsage_remoteCollectionItems() { + Bitmap b1 = Bitmap.createBitmap(1024, 768, Bitmap.Config.ARGB_8888); + int b1Memory = b1.getAllocationByteCount(); + Bitmap b2 = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888); + int b2Memory = b2.getAllocationByteCount(); + + final RemoteViews rv1 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv1.setBitmap(R.id.view, "", b1); + final RemoteViews rv2 = new RemoteViews(mPackage, R.layout.remote_views_test); + rv2.setIcon(R.id.view, "", Icon.createWithBitmap(b2)); + final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test); + rv.setRemoteAdapter(R.id.view, new RemoteViews.RemoteCollectionItems.Builder().addItem(0L, + rv1).addItem(1L, rv2).build()); + assertEquals(b1Memory, rv.estimateMemoryUsage()); + assertEquals(b2Memory, rv.estimateIconMemoryUsage()); + assertEquals(b1Memory + b2Memory, rv.estimateTotalBitmapMemoryUsage()); + } + private static LayoutInflater.Factory2 createLayoutInflaterFactory(String viewTypeToReplace, View replacementView) { return new LayoutInflater.Factory2() { diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index cffdfbd36532..536d53c07f11 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -1095,6 +1095,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku proto.write(WidgetProto.MAX_HEIGHT, widget.options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)); } + if (widget.views != null) { + proto.write(WidgetProto.VIEWS_BITMAP_MEMORY, + widget.views.estimateTotalBitmapMemoryUsage()); + } proto.end(token); } @@ -2846,7 +2850,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku // For a full update we replace the RemoteViews completely. widget.views = views; } - int memoryUsage; + long memoryUsage; if ((UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID) && (widget.views != null) && ((memoryUsage = widget.views.estimateMemoryUsage()) > mMaxWidgetBitmapMemory)) { @@ -3503,6 +3507,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } if (widget.views != null) { pw.print(" views="); pw.println(widget.views); + pw.print(" views_bitmap_memory="); + pw.println(widget.views.estimateTotalBitmapMemoryUsage()); } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 05aa4134cbd5..c16b7886181d 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -8683,7 +8683,7 @@ public class NotificationManagerService extends SystemService { if (contentView == null) { return false; } - final int contentViewSize = contentView.estimateMemoryUsage(); + final long contentViewSize = contentView.estimateMemoryUsage(); if (contentViewSize > mWarnRemoteViewsSizeBytes && contentViewSize < mStripRemoteViewsSizeBytes) { Slog.w(TAG, "RemoteViews too large on pkg: " + pkg + " tag: " + tag + " id: " + id diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index e43b28bb9404..44d9786e81a4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -11977,7 +11977,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @DisableFlags(android.app.Flags.FLAG_REMOVE_REMOTE_VIEWS) public void testRemoveLargeRemoteViews() throws Exception { - int removeSize = mContext.getResources().getInteger( + // Cast to long to mock RemoteViews.estimateMemoryUsage which returns long. + long removeSize = mContext.getResources().getInteger( com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes); RemoteViews rv = mock(RemoteViews.class); |