11package com .termux .terminal ;
22
3+ import android .app .WallpaperManager ;
34import android .graphics .Bitmap ;
45import android .graphics .BitmapFactory ;
6+ import android .graphics .Canvas ;
7+ import android .graphics .Paint ;
8+ import android .graphics .RecordingCanvas ;
9+ import android .graphics .Rect ;
10+ import android .graphics .RectF ;
11+ import android .os .Build ;
12+
13+ import java .util .Properties ;
514
615/**
716 * A terminal bitmap for images.
@@ -10,6 +19,110 @@ public class TerminalBitmap {
1019
1120 public static final String LOG_TAG = "TerminalBitmap" ;
1221
22+ private static int initMaxBitmapSize () {
23+ int defaultSize =
24+ Build .VERSION .SDK_INT >= Build .VERSION_CODES .VANILLA_ICE_CREAM ?
25+ 150 * 1024 * 1024 : // 150 MB
26+ 100 * 1024 * 1024 ; // 100 MB
27+
28+ Properties systemProperties = AndroidUtils .getSystemProperties (LOG_TAG );
29+ String maxTextureSizeString = systemProperties .getProperty ("ro.hwui.max_texture_allocation_size" );
30+
31+ if (maxTextureSizeString == null ) return defaultSize ;
32+
33+ try {
34+ int maxTextureSize = Integer .parseInt (maxTextureSizeString );
35+ return maxTextureSize > 0 ? maxTextureSize : defaultSize ;
36+ }
37+ catch (Exception e ) {
38+ return defaultSize ;
39+ }
40+ }
41+
42+ /**
43+ * The max size of a Terminal {@link Bitmap} for its pixels. The limit is defined as per how
44+ * `RecordingCanvas.MAX_BITMAP_SIZE` value is defined, check below for details. The value should
45+ * normally be between `100-200MB` depending on device and Android version.
46+ *
47+ * Each pixel is stored on 4 bytes for a {@link Bitmap.Config#ARGB_8888} bitmap color config.
48+ * The bitmap will have following memory usage for its respective resolution (`width x height x 4`).
49+ * - 1280x720 (HD): 3,686,400 bytes/3.6MB.
50+ * - 1920x1080 (FHD): 8,294,400 bytes/8MB.
51+ * - 2560x1440 (QHD): 14,745,600 bytes/14.7MB.
52+ * - 3840x2160 (4K UHD): 33,177,600 bytes/33MB.
53+ * - 7680x4320 (8K UHD): 132,710,400 bytes/132MB.
54+ * .
55+ * - https://en.wikipedia.org/wiki/Display_resolution_standards#High-definition
56+ *
57+ * The terminal uses {@link Canvas#drawBitmap(Bitmap, Rect, RectF, Paint)} to draw the bitmap
58+ * when `TerminalRenderer.render()` is called.
59+ *
60+ * The {@link Canvas} class defines `Canvas.MAXIMUM_BITMAP_SIZE` for the maximum dimension
61+ * for a bitmap which is returned by {@link Canvas#getMaximumBitmapWidth()} and
62+ * {@link Canvas#getMaximumBitmapHeight()}. It is hardcoded with the value `32766` as defined by
63+ * Skia (2D graphics library), which technically has the limit `32767` as it requires supporting
64+ * math on 16-bit buffers.
65+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/f61970fc79e9c5cf340fa942597628242361864a
66+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/graphics/java/android/graphics/Canvas.java;l=76-78
67+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:external/skia/src/shaders/SkImageShader.cpp;l=254-267
68+ *
69+ * The {@link RecordingCanvas} class defines `RecordingCanvas.MAX_BITMAP_SIZE` for the
70+ * maximum size (not dimension) for a bitmap, which is checked by
71+ * `RecordingCanvas.throwIfCannotDraw()` when `BaseRecordingCanvas.drawBitmap()` is called.
72+ * The `RecordingCanvas` is a specialized implementation of the `Canvas` class that is designed
73+ * to record draw commands for deferred rendering instead of executing draw commands instantly.
74+ * By recording draw commands, they can be cached so that complex views can be efficiently
75+ * re-drawn without recalculating them again for every frame. The caching part is similar to
76+ * how a terminal behaves, where it stores all the bitmaps for rendering depending on scroll
77+ * position. So both {@link RecordingCanvas} and a terminal require similar limits on bitmap
78+ * sizes considering memory consumption limits of apps, and multiple bitmaps being loaded
79+ * instead of a single one like for wallpapers, hence why `TerminalBitmap.MAX_BITMAP_SIZE` is
80+ * synced with {@link RecordingCanvas}.
81+ * The `RecordingCanvas.MAX_BITMAP_SIZE` is set from `ro.hwui.max_texture_allocation_size`
82+ * system property if set for Android `>= 12`, otherwise `150MB` (`100MB` for Android `10-14`).
83+ * The values `>= 150MB` are enough to support `7680x4320` (8K UHD) bitmaps.
84+ * Some devices like larger xiaomi devices have `ro.hwui.max_texture_allocation_size` set to `209715200` (`200MB`).
85+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/e4d011201cea40d46cb2b2eef401db8fddc5c9c6
86+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/0e717a9d06ded980908649393bd73e46ffafcd54
87+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/97396260ed06cc9d1834d4d8e4e649a3ef09f1f3
88+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/graphics/java/android/graphics/RecordingCanvas.java;l=42-50
89+ *
90+ * The Android wallpaper manager service also checks if dimensions of cropped wallpaper exceeds
91+ * max texture size that the GPU can support, otherwise it will cause System UI to keep crashing
92+ * because it can not initialize EGL with an appropriate surface. The `GLHelper.getMaxTextureSize()`
93+ * returns the max texture size, which is defined by `sys.max_texture_size` system property if set,
94+ * otherwise by value for `GL_MAX_TEXTURE_SIZE`. The `sys.max_texture_size` defines the maximum
95+ * width or height of a texture, not total size. Its value can be low like `2048` or high like
96+ * `16384` for 16K support.
97+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/32c6a7c691b0d91085c1ed13fe6f1c473c94b4c8
98+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperCropper.java;l=461
99+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/GLHelper.java;l=145
100+ * - https://developer.android.com/reference/android/opengl/GLES10#GL_MAX_TEXTURE_SIZE
101+ *
102+ * The {@link WallpaperManager#getDesiredMinimumWidth()} and {@link WallpaperManager#getDesiredMinimumHeight()}
103+ * can also be called to get minimum suggested width and height of the wallpaper that an app
104+ * should use when setting the wallpaper. This normally is equal to the width and height of the
105+ * current device display, but the width can be higher than display width if the homescreen is
106+ * scrollable horizontally with multiple pages, in which case the width returned is equal to
107+ * entire workspace width. The launcher apps can provide Android their desired width and height
108+ * dimensions depending on the homescreen pages config by calling
109+ * {@link WallpaperManager#suggestDesiredDimensions(int, int)}, which also ensures that values
110+ * passed are scaled down to `sys.max_texture_size` system property if its set.
111+ * - https://cs.android.com/android/_/android/platform/frameworks/base/+/289c273ec49462c7bfdbf6238e9016936da7307c
112+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/core/java/android/app/WallpaperManager.java;l=2737-2794
113+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java;l=2330-2366
114+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java;l=108-115
115+ * - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/core/java/android/view/Display.java;l=1052-1063
116+ *
117+ * If an app specifies `largeHeap=true` in its `AndroidManifest.xml`, then it can be allocated
118+ * larger heap memory to load larger bitmaps maps instead of resulting in an OOM. The Termux app
119+ * does not have it enabled, and hence is more likely to have OOMs when loading larger bitmaps.
120+ * - https://developer.android.com/guide/topics/manifest/application-element#largeHeap
121+ * - https://developer.android.com/topic/performance/memory
122+ */
123+ public static final int MAX_BITMAP_SIZE = initMaxBitmapSize ();
124+
125+
13126
14127 protected final TerminalSessionClient mClient ;
15128
@@ -304,8 +417,15 @@ public static Bitmap resizeBitmap(String logTag, String label, TerminalSessionCl
304417
305418 Bitmap newBitmap ;
306419 try {
307- int [] pixels = new int [bitmap .getAllocationByteCount ()];
420+ int newBitmapSize = bitmapWidth * bitmapHeight * 4 ;
421+ if (newBitmapSize < 0 || newBitmapSize > MAX_BITMAP_SIZE ) {
422+ Logger .logError (client , logTag , "The new " + label + " bitmap after resize with" +
423+ " width " + bitmapWidth + " and height " + bitmapHeight +
424+ " has size " + newBitmapSize + " greater than max bitmap size " + MAX_BITMAP_SIZE );
425+ return null ;
426+ }
308427
428+ int [] pixels = new int [bitmap .getAllocationByteCount ()];
309429 bitmap .getPixels (pixels , 0 , bitmap .getWidth (), 0 , 0 , bitmap .getWidth (), bitmap .getHeight ());
310430
311431 newBitmap = Bitmap .createBitmap (bitmapWidth , bitmapHeight , Bitmap .Config .ARGB_8888 );
0 commit comments