Messaging: Meet Play memory/R8 rules, ship Q icon, and polish home chrome

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-28 23:44:47 +09:00
co-authored by Cursor
parent 74fb80f783
commit 47d735be2c
19 changed files with 209 additions and 107 deletions
@@ -128,6 +128,18 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
Trace.endSection();
}
@Override
public void onTrimMemory(final int level) {
super.onTrimMemory(level);
if (level < TRIM_MEMORY_UI_HIDDEN) {
return;
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "BugleApplication.onTrimMemory " + level);
}
reclaimCachedBitmaps();
}
@Override
public void onLowMemory() {
super.onLowMemory();
@@ -135,7 +147,14 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "BugleApplication.onLowMemory");
}
Factory.get().reclaimMemory();
reclaimCachedBitmaps();
}
private static void reclaimCachedBitmaps() {
final Factory factory = Factory.get();
if (factory != null) {
factory.reclaimMemory();
}
}
@Override
@@ -211,7 +211,16 @@ public class DecodedImageResource extends ImageResource {
}
}
byte[] encodedBytes = ImageUtils.bitmapToBytes(bitmap, COMPRESS_QUALITY);
return new EncodedImageResource(getKey(), encodedBytes, getOrientation());
int desiredWidth = ImageRequest.UNSPECIFIED_SIZE;
int desiredHeight = ImageRequest.UNSPECIFIED_SIZE;
if (mOriginalImageRequest instanceof ImageRequest<?>) {
final ImageRequestDescriptor descriptor =
((ImageRequest<?>) mOriginalImageRequest).getDescriptor();
desiredWidth = descriptor.desiredWidth;
desiredHeight = descriptor.desiredHeight;
}
return new EncodedImageResource(getKey(), encodedBytes, getOrientation(),
desiredWidth, desiredHeight);
} catch (Exception ex) {
// Something went wrong during bitmap compression, fall back to just using the
// original bitmap.
@@ -17,11 +17,11 @@ package com.android.messaging.datamodel.media;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.ImageUtils;
import java.util.Arrays;
import java.util.List;
@@ -41,10 +41,20 @@ import java.util.List;
*/
public class EncodedImageResource extends ImageResource {
private final byte[] mImageBytes;
private final int mDesiredWidth;
private final int mDesiredHeight;
public EncodedImageResource(String key, byte[] imageBytes, int orientation) {
this(key, imageBytes, orientation, ImageRequest.UNSPECIFIED_SIZE,
ImageRequest.UNSPECIFIED_SIZE);
}
public EncodedImageResource(String key, byte[] imageBytes, int orientation,
final int desiredWidth, final int desiredHeight) {
super(key, orientation);
mImageBytes = imageBytes;
mDesiredWidth = desiredWidth;
mDesiredHeight = desiredHeight;
}
@Override
@@ -54,7 +64,7 @@ public class EncodedImageResource extends ImageResource {
try {
// This should only be called during the decode request.
Assert.isNotMainThread();
return BitmapFactory.decodeByteArray(mImageBytes, 0, mImageBytes.length);
return decodeSampledBitmap();
} finally {
releaseLock();
}
@@ -99,11 +109,15 @@ public class EncodedImageResource extends ImageResource {
return true;
}
private Bitmap decodeSampledBitmap() {
return ImageUtils.decodeByteArraySampled(mImageBytes, mDesiredWidth, mDesiredHeight);
}
@Override
MediaRequest<? extends RefCountedMediaResource> getMediaDecodingRequest(
final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
Assert.isTrue(isEncoded());
return new DecodeImageRequest();
return new DecodeImageRequest(originalRequest);
}
/**
@@ -112,7 +126,22 @@ public class EncodedImageResource extends ImageResource {
* properties such as binding.
*/
private class DecodeImageRequest implements MediaRequest<ImageResource> {
public DecodeImageRequest() {
private final int mRequestDesiredWidth;
private final int mRequestDesiredHeight;
public DecodeImageRequest(
final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
int desiredWidth = mDesiredWidth;
int desiredHeight = mDesiredHeight;
if (originalRequest != null) {
final MediaRequestDescriptor<?> descriptor = originalRequest.getDescriptor();
if (descriptor instanceof ImageRequestDescriptor) {
desiredWidth = ((ImageRequestDescriptor) descriptor).desiredWidth;
desiredHeight = ((ImageRequestDescriptor) descriptor).desiredHeight;
}
}
mRequestDesiredWidth = desiredWidth;
mRequestDesiredHeight = desiredHeight;
// Hold a ref onto the encoded resource before the request finishes.
addRef();
}
@@ -129,8 +158,11 @@ public class EncodedImageResource extends ImageResource {
Assert.isNotMainThread();
acquireLock();
try {
final Bitmap decodedBitmap = BitmapFactory.decodeByteArray(mImageBytes, 0,
mImageBytes.length);
final Bitmap decodedBitmap = ImageUtils.decodeByteArraySampled(mImageBytes,
mRequestDesiredWidth, mRequestDesiredHeight);
if (decodedBitmap == null) {
throw new RuntimeException("failed decoding encoded bitmap");
}
return new DecodedImageResource(getKey(), decodedBitmap, getOrientation());
} finally {
releaseLock();
@@ -18,11 +18,11 @@ package com.android.messaging.datamodel.media;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.mms.pdu.ContentType;
import com.android.messaging.Factory;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.LogUtil;
import java.io.FileNotFoundException;
@@ -91,7 +91,11 @@ public class NetworkUriImageRequest<D extends UriImageRequestDescriptor> extends
connection.setDoInput(true);
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
try (InputStream inputStream = connection.getInputStream()) {
final byte[] bytes = ImageUtils.readStreamToByteArray(inputStream);
bitmap = ImageUtils.decodeByteArraySampled(bytes,
mDescriptor.desiredWidth, mDescriptor.desiredHeight);
}
}
} catch (MalformedURLException e) {
LogUtil.e(LogUtil.BUGLE_TAG,
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversationlist;
import android.os.Bundle;
import android.view.ActionMode;
import android.window.SplashScreenView;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
@@ -34,6 +35,7 @@ public class ConversationListActivity extends AbstractConversationListActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
Trace.beginSection("ConversationListActivity.onCreate");
getSplashScreen().setOnExitAnimationListener(SplashScreenView::remove);
setTheme(R.style.BugleTheme_ConversationListActivity);
super.onCreate(savedInstanceState);
mConversationListFragment = ConversationListFragment.createConversationListFragment(null);
@@ -20,10 +20,12 @@ import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.support.v7.mms.pdu.ContentType;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.StaticLayout;
@@ -221,6 +223,9 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mListDividerView = findViewById(R.id.conversation_list_divider);
mConversationNameView.addOnLayoutChangeListener(this);
mSnippetTextView.addOnLayoutChangeListener(this);
if (mCategoryChipView != null) {
mCategoryChipView.addOnLayoutChangeListener(this);
}
final Resources resources = getContext().getResources();
final Resources.Theme theme = getContext().getTheme();
@@ -243,6 +248,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
setSnippet();
} else if (v == mSubjectTextView) {
setSubject();
} else if (v == mCategoryChipView) {
alignSimBadgeToFirstSnippetLine();
}
}
@@ -258,10 +265,12 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
if (!SimNameUtil.shouldShowSimName(getContext())) {
mCategoryChipView.setText(null);
mCategoryChipView.setVisibility(GONE);
mCategoryChipView.setTranslationY(0f);
return;
}
mCategoryChipView.setText(SimNameUtil.getSimDisplayName(getContext(), mData.getSelfId()));
mCategoryChipView.setVisibility(VISIBLE);
alignSimBadgeToFirstSnippetLine();
}
private void setConversationName() {
@@ -344,12 +353,41 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
final int indent = getSnippetChipIndent();
if (indent <= 0) {
mSnippetTextView.setText(snippetText);
} else {
final SpannableString spanned = new SpannableString(snippetText == null ? "" : snippetText);
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mSnippetTextView.setText(spanned);
}
alignSimBadgeToFirstSnippetLine();
}
private void alignSimBadgeToFirstSnippetLine() {
if (mCategoryChipView == null) {
return;
}
final SpannableString spanned = new SpannableString(snippetText == null ? "" : snippetText);
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mSnippetTextView.setText(spanned);
if (mCategoryChipView.getVisibility() != VISIBLE || mSnippetTextView == null) {
mCategoryChipView.setTranslationY(0f);
return;
}
int chipHeight = mCategoryChipView.getHeight();
if (chipHeight <= 0) {
mCategoryChipView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
chipHeight = mCategoryChipView.getMeasuredHeight();
}
if (chipHeight <= 0) {
return;
}
final Paint.FontMetricsInt fm = mSnippetTextView.getPaint().getFontMetricsInt();
final int textHeight = fm.descent - fm.ascent;
int textTop = mSnippetTextView.getPaddingTop();
final Layout layout = mSnippetTextView.getLayout();
if (layout != null && layout.getLineCount() > 0) {
textTop = mSnippetTextView.getPaddingTop() + layout.getLineBaseline(0) + fm.ascent;
}
mCategoryChipView.setTranslationY(textTop + (textHeight - chipHeight) / 2f);
}
private int getSnippetChipIndent() {
@@ -22,14 +22,20 @@ public final class HomeCategoryAddModal {
void onCategoryCreated(HomeCategoryStore.Category created);
}
private static boolean sShowing;
private HomeCategoryAddModal() {
}
public static void show(@NonNull final Context context, @Nullable final Listener listener) {
if (sShowing) {
return;
}
sShowing = true;
final View content = LayoutInflater.from(context)
.inflate(R.layout.home_category_name_dialog, null, false);
final EditText field = content.findViewById(R.id.home_category_name_field);
HomeModal.builder(context)
final HomeModal.Handle handle = HomeModal.builder(context)
.icon(R.drawable.ic_home_category)
.title(R.string.home_category_add)
.message(R.string.home_category_name_message)
@@ -44,6 +50,11 @@ public final class HomeCategoryAddModal {
}
})
.show();
if (handle == null || handle.dialog == null) {
sShowing = false;
return;
}
handle.dialog.setOnDismissListener(dialog -> sShowing = false);
if (field != null) {
field.requestFocus();
ImeUtil.get().showImeKeyboard(context, field);
@@ -202,6 +202,46 @@ public class ImageUtils {
return inSampleSize;
}
/**
* Cap for full-resolution decodes when the caller did not give a target size.
* 2048px keeps a 12 MP photo from becoming a ~48 MB ARGB_8888 bitmap.
*/
public static final int MAX_DECODE_EDGE = 2048;
public static Bitmap decodeByteArraySampled(final byte[] bytes, final int reqWidth,
final int reqHeight) {
if (bytes == null || bytes.length == 0) {
return null;
}
final BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, bounds);
int targetWidth = reqWidth;
int targetHeight = reqHeight;
if (targetWidth == ImageRequest.UNSPECIFIED_SIZE
&& targetHeight == ImageRequest.UNSPECIFIED_SIZE) {
targetWidth = MAX_DECODE_EDGE;
targetHeight = MAX_DECODE_EDGE;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.outWidth = bounds.outWidth;
options.outHeight = bounds.outHeight;
options.inSampleSize = get().calculateInSampleSize(options, targetWidth, targetHeight);
options.inJustDecodeBounds = false;
options.inMutable = true;
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}
public static byte[] readStreamToByteArray(final InputStream inputStream) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final byte[] buffer = new byte[16 * 1024];
int read;
while ((read = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
return output.toByteArray();
}
private static final String[] MEDIA_CONTENT_PROJECTION = new String[] {
MediaStore.MediaColumns.MIME_TYPE
};