summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Willie Koomson <wvk@google.com> 2025-01-03 22:03:03 +0000
committer Willie Koomson <wvk@google.com> 2025-01-25 00:32:47 +0000
commit9ef0692a504a73b989208bf2a49ddcb82fbde549 (patch)
treefc18f311f1a4a1e3ebb13de78822ec230f48a22a
parent2fb05f5a565bb47caa5de6076de278c673def147 (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
-rw-r--r--core/java/android/widget/RemoteViews.java101
-rw-r--r--core/proto/android/service/appwidget.proto1
-rw-r--r--core/tests/coretests/src/android/widget/RemoteViewsTest.java131
-rw-r--r--services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java8
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java2
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java3
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);