diff --git a/app/build.gradle b/app/build.gradle index ece4bc9..68ae539 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ android { buildTypes { release { minifyEnabled true + shrinkResources true signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), rootProject.file('proguard-release.flags'), diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d40ae4b..557b420 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,6 +91,7 @@ android:allowBackup="false" android:appCategory="social" android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher" android:label="@string/app_name" android:localeConfig="@xml/locales_config" android:requestLegacyExternalStorage="true" diff --git a/app/src/main/java/com/android/messaging/BugleApplication.java b/app/src/main/java/com/android/messaging/BugleApplication.java index 4c2d796..83d1a1c 100644 --- a/app/src/main/java/com/android/messaging/BugleApplication.java +++ b/app/src/main/java/com/android/messaging/BugleApplication.java @@ -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 diff --git a/app/src/main/java/com/android/messaging/datamodel/media/DecodedImageResource.java b/app/src/main/java/com/android/messaging/datamodel/media/DecodedImageResource.java index b79a815..4b477bb 100644 --- a/app/src/main/java/com/android/messaging/datamodel/media/DecodedImageResource.java +++ b/app/src/main/java/com/android/messaging/datamodel/media/DecodedImageResource.java @@ -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. diff --git a/app/src/main/java/com/android/messaging/datamodel/media/EncodedImageResource.java b/app/src/main/java/com/android/messaging/datamodel/media/EncodedImageResource.java index 0bc94e5..98d1906 100644 --- a/app/src/main/java/com/android/messaging/datamodel/media/EncodedImageResource.java +++ b/app/src/main/java/com/android/messaging/datamodel/media/EncodedImageResource.java @@ -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 getMediaDecodingRequest( final MediaRequest 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 { - public DecodeImageRequest() { + private final int mRequestDesiredWidth; + private final int mRequestDesiredHeight; + + public DecodeImageRequest( + final MediaRequest 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(); diff --git a/app/src/main/java/com/android/messaging/datamodel/media/NetworkUriImageRequest.java b/app/src/main/java/com/android/messaging/datamodel/media/NetworkUriImageRequest.java index ff7d800..c7faf82 100644 --- a/app/src/main/java/com/android/messaging/datamodel/media/NetworkUriImageRequest.java +++ b/app/src/main/java/com/android/messaging/datamodel/media/NetworkUriImageRequest.java @@ -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 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, diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java index 51e446b..a9efd65 100644 --- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java +++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java @@ -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); diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java index d7bc8df..4f92458 100644 --- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java +++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java @@ -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() { diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java index 014367a..5435221 100644 --- a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java @@ -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); diff --git a/app/src/main/java/com/android/messaging/util/ImageUtils.java b/app/src/main/java/com/android/messaging/util/ImageUtils.java index 278bad9..51e1864 100644 --- a/app/src/main/java/com/android/messaging/util/ImageUtils.java +++ b/app/src/main/java/com/android/messaging/util/ImageUtils.java @@ -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 }; diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..425d797 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index fb36b7e..9b579af 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,18 +1,21 @@ - - - + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index d0dcbf4..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml index 9cf5e2c..aceadf9 100644 --- a/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,19 +1,4 @@ - - - - - + diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index e3be160..e596bd4 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -275,8 +275,8 @@ + android:layout_width="24dp" + android:layout_height="24dp" /> diff --git a/app/src/main/res/layout/home_category_add_tab.xml b/app/src/main/res/layout/home_category_add_tab.xml index c999ed6..0acf76b 100644 --- a/app/src/main/res/layout/home_category_add_tab.xml +++ b/app/src/main/res/layout/home_category_add_tab.xml @@ -2,9 +2,12 @@ diff --git a/proguard-release.flags b/proguard-release.flags index 67ae150..36b6720 100644 --- a/proguard-release.flags +++ b/proguard-release.flags @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. --keep class com.android.messaging.ui.contact.* { *; } - -assumenosideeffects public class com.android.messaging.util.Trace { public void beginSection(...); public void endSection(...); diff --git a/proguard.flags b/proguard.flags index f7b3dbc..e35b925 100644 --- a/proguard.flags +++ b/proguard.flags @@ -22,54 +22,45 @@ RuntimeVisibleTypeAnnotations, AnnotationDefault --keep class com.android.messaging.* { *; } --keep class com.android.messaging.*.* { *; } --keep class com.android.messaging.*.*.* { *; } - -# TODO(b/373579455): Evaluate if needs to be kept. +# Manifest / XML entry points. Do not keep the whole app or androidx packages — +# Play requires 25% shrink / optimize / obfuscate when uncompressed DEX > 10 MB. -keep public class * extends android.app.Activity { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.app.Application { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.app.Service { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.content.BroadcastReceiver { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.content.ContentProvider { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.app.backup.BackupAgentHelper { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. -keep public class * extends android.preference.Preference { void (); } -# TODO(b/373579455): Evaluate if needs to be kept. +-keep public class * extends androidx.preference.Preference { + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); +} -keep public class * extends androidx.fragment.app.Fragment { void (); } --keep public class com.android.vcard.* { *; } --keep class androidx.collection.* { *; } --keep class androidx.core.* { *; } --keep class androidx.core.*.* { *; } --keep class androidx.appcompat.* { *; } --keep class androidx.appcompat.*.* { *; } --keep class androidx.recyclerview.widget.* { *; } --keep class androidx.viewpager.widget.* { *; } - -# Keep rastermill classes that need to be accessed from native code (JNI) --keep class android.support.rastermill.** { *; } +# Actions cross the ActionService / BackgroundWorker via Parcelable extras. +-keep class com.android.messaging.datamodel.action.Action { + void (); +} +-keep class * extends com.android.messaging.datamodel.action.Action { + public static android.os.Parcelable$Creator CREATOR; + (...); +} # Preserve the name of the getCaller method so it can find itself in the stack trace it generates -keepclassmembers public class com.android.messaging.util.DebugUtils { @@ -77,7 +68,7 @@ } # Keep the static fields of referenced inner classes of auto-generated R classes, in case we -# access those fields by reflection (e.g. EmojiMarkup) +# access those fields by reflection -keepclassmembers class **.R$* { public static ; }