Commit 5e65dc64 authored by Peter Kotwicz's avatar Peter Kotwicz Committed by Commit Bot

Fix splash screen pixelation for new-style WebAPKs

This CL:
- Increases the maximum screenshot size (prior to PNG/JPEG encoding) that
  we transfer to Chrome. The new max will avoid downscaling on most phones
  (e.g. no downscaling on Pixel 3)
- Uses JPEG encoding for large screenshots because JPEG encoding is
  faster than PNG encoding on Android. Although JPEG encoding is not
  lossless it is preferable to downscaling

BUG=1001154

Change-Id: Ib9d61cee565b6d9a392272300bac71d6da46a861
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1787162
Commit-Queue: Peter Kotwicz <pkotwicz@chromium.org>
Reviewed-by: default avatarGlenn Hartmann <hartmanng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#696990}
parent ff9ee2b6
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
# //chrome/android/webapk/shell_apk:webapk is changed. This includes # //chrome/android/webapk/shell_apk:webapk is changed. This includes
# Java files, Android resource files and AndroidManifest.xml. Does not affect # Java files, Android resource files and AndroidManifest.xml. Does not affect
# Chrome.apk # Chrome.apk
current_shell_apk_version = 105 current_shell_apk_version = 106
...@@ -13,6 +13,7 @@ import android.graphics.Bitmap; ...@@ -13,6 +13,7 @@ import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
...@@ -213,44 +214,52 @@ public class SplashActivity extends Activity { ...@@ -213,44 +214,52 @@ public class SplashActivity extends Activity {
/** /**
* Launches the host browser on top of {@link SplashActivity}. * Launches the host browser on top of {@link SplashActivity}.
* @param splashPngEncoded PNG-encoded screenshot of {@link mSplashView}. * @param splashEncoded Encoded screenshot of {@link mSplashView}.
* @param encodingFormat The screenshot's encoding format.
*/ */
private void launch(byte[] splashPngEncoded) { private void launch(byte[] splashEncoded, Bitmap.CompressFormat encodingFormat) {
SplashContentProvider.cache( SplashContentProvider.cache(this, splashEncoded, encodingFormat, mSplashView.getWidth(),
this, splashPngEncoded, mSplashView.getWidth(), mSplashView.getHeight()); mSplashView.getHeight());
H2OLauncher.launch(this, mParams); H2OLauncher.launch(this, mParams);
mParams = null; mParams = null;
} }
/** /**
* Screenshots and PNG-encodes {@link mSplashView} on a background thread. * Screenshots and encodes {@link mSplashView} on a background thread.
*/ */
@SuppressWarnings("NoAndroidAsyncTaskCheck") @SuppressWarnings("NoAndroidAsyncTaskCheck")
private void screenshotAndEncodeSplashInBackground() { private void screenshotAndEncodeSplashInBackground() {
final Bitmap bitmap = SplashUtils.screenshotView( final Bitmap bitmap = SplashUtils.screenshotView(
mSplashView, SplashContentProvider.MAX_TRANSFER_SIZE_BYTES); mSplashView, SplashContentProvider.MAX_TRANSFER_SIZE_BYTES);
if (bitmap == null) { if (bitmap == null) {
launch(null); launch(null, Bitmap.CompressFormat.PNG);
return; return;
} }
mScreenshotSplashTask = mScreenshotSplashTask =
new android.os new android.os
.AsyncTask<Void, Void, byte[]>() { .AsyncTask<Void, Void, Pair<byte[], Bitmap.CompressFormat>>() {
@Override @Override
protected byte[] doInBackground(Void... args) { protected Pair<byte[], Bitmap.CompressFormat> doInBackground(
Void... args) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); Bitmap.CompressFormat encodingFormat =
return out.toByteArray(); SplashUtils.selectBitmapEncoding(
bitmap.getWidth(), bitmap.getHeight());
bitmap.compress(encodingFormat, 100, out);
return Pair.create(out.toByteArray(), encodingFormat);
} catch (IOException e) { } catch (IOException e) {
} }
return null; return null;
} }
@Override @Override
protected void onPostExecute(byte[] splashPngEncoded) { protected void onPostExecute(
Pair<byte[], Bitmap.CompressFormat> splashEncoded) {
mScreenshotSplashTask = null; mScreenshotSplashTask = null;
launch(splashPngEncoded); launch((splashEncoded == null) ? null : splashEncoded.first,
(splashEncoded == null) ? Bitmap.CompressFormat.PNG
: splashEncoded.second);
} }
// Do nothing if task was cancelled. // Do nothing if task was cancelled.
......
...@@ -54,12 +54,14 @@ public class SplashContentProvider ...@@ -54,12 +54,14 @@ public class SplashContentProvider
* downsampled to fit. Capping the maximum size of the screenshot decreases bitmap encoding * downsampled to fit. Capping the maximum size of the screenshot decreases bitmap encoding
* time and image transfer time. * time and image transfer time.
*/ */
public static final int MAX_TRANSFER_SIZE_BYTES = 1024 * 1024 * 4; public static final int MAX_TRANSFER_SIZE_BYTES = 1024 * 1024 * 12;
/** Mime type of transferred screenshot. */ /**
private static final String SPLASH_MIME_TYPE = "image/png"; * The encoding type of the last image vended by the ContentProvider.
*/
private static Bitmap.CompressFormat sEncodingFormat;
private static AtomicReference<ExpiringData> sCachedSplashPngBytes = new AtomicReference<>(); private static AtomicReference<ExpiringData> sCachedSplashBytes = new AtomicReference<>();
/** The URI handled by this content provider. */ /** The URI handled by this content provider. */
private String mContentProviderUri; private String mContentProviderUri;
...@@ -68,14 +70,15 @@ public class SplashContentProvider ...@@ -68,14 +70,15 @@ public class SplashContentProvider
* Temporarily caches the passed-in splash screen screenshot. To preserve memory, the cached * Temporarily caches the passed-in splash screen screenshot. To preserve memory, the cached
* data is cleared after a delay. * data is cleared after a delay.
*/ */
public static void cache( public static void cache(Context context, byte[] splashBytes,
Context context, byte[] splashPngBytes, int splashWidth, int splashHeight) { Bitmap.CompressFormat encodingFormat, int splashWidth, int splashHeight) {
SharedPreferences.Editor editor = WebApkSharedPreferences.getPrefs(context).edit(); SharedPreferences.Editor editor = WebApkSharedPreferences.getPrefs(context).edit();
editor.putInt(WebApkSharedPreferences.PREF_SPLASH_WIDTH, splashWidth); editor.putInt(WebApkSharedPreferences.PREF_SPLASH_WIDTH, splashWidth);
editor.putInt(WebApkSharedPreferences.PREF_SPLASH_HEIGHT, splashHeight); editor.putInt(WebApkSharedPreferences.PREF_SPLASH_HEIGHT, splashHeight);
editor.apply(); editor.apply();
getAndSetCachedData(splashPngBytes); sEncodingFormat = encodingFormat;
getAndSetCachedData(splashBytes);
} }
public static void clearCache() { public static void clearCache() {
...@@ -86,12 +89,12 @@ public class SplashContentProvider ...@@ -86,12 +89,12 @@ public class SplashContentProvider
* Sets the cached splash screen screenshot and returns the old one. * Sets the cached splash screen screenshot and returns the old one.
* Thread safety: Can be called from any thread. * Thread safety: Can be called from any thread.
*/ */
private static byte[] getAndSetCachedData(byte[] newSplashPngBytes) { private static byte[] getAndSetCachedData(byte[] newSplashBytes) {
ExpiringData newData = null; ExpiringData newData = null;
if (newSplashPngBytes != null) { if (newSplashBytes != null) {
newData = new ExpiringData(newSplashPngBytes, SplashContentProvider::clearCache); newData = new ExpiringData(newSplashBytes, SplashContentProvider::clearCache);
} }
ExpiringData oldCachedData = sCachedSplashPngBytes.getAndSet(newData); ExpiringData oldCachedData = sCachedSplashBytes.getAndSet(newData);
if (oldCachedData == null) return null; if (oldCachedData == null) return null;
oldCachedData.removeCallbacks(); oldCachedData.removeCallbacks();
...@@ -117,16 +120,18 @@ public class SplashContentProvider ...@@ -117,16 +120,18 @@ public class SplashContentProvider
public void writeDataToPipe( public void writeDataToPipe(
ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Void unused) { ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Void unused) {
try (OutputStream out = new FileOutputStream(output.getFileDescriptor())) { try (OutputStream out = new FileOutputStream(output.getFileDescriptor())) {
byte[] cachedSplashPngBytes = getAndSetCachedData(null); byte[] cachedSplashBytes = getAndSetCachedData(null);
if (cachedSplashPngBytes != null) { if (cachedSplashBytes != null) {
out.write(cachedSplashPngBytes); out.write(cachedSplashBytes);
} else { } else {
// One way that this case gets hit is when the WebAPK is brought to the foreground // One way that this case gets hit is when the WebAPK is brought to the foreground
// via Android Recents after the Android OOM killer has killed the host browser but // via Android Recents after the Android OOM killer has killed the host browser but
// not SplashActivity. // not SplashActivity.
Bitmap splashScreenshot = recreateAndScreenshotSplash(); Bitmap splashScreenshot = recreateAndScreenshotSplash();
if (splashScreenshot != null) { if (splashScreenshot != null) {
splashScreenshot.compress(Bitmap.CompressFormat.PNG, 100, out); sEncodingFormat = SplashUtils.selectBitmapEncoding(
splashScreenshot.getWidth(), splashScreenshot.getHeight());
splashScreenshot.compress(sEncodingFormat, 100, out);
} }
} }
out.flush(); out.flush();
...@@ -137,7 +142,18 @@ public class SplashContentProvider ...@@ -137,7 +142,18 @@ public class SplashContentProvider
@Override @Override
public String getType(Uri uri) { public String getType(Uri uri) {
if (uri != null && uri.toString().equals(mContentProviderUri)) { if (uri != null && uri.toString().equals(mContentProviderUri)) {
return SPLASH_MIME_TYPE; if (sEncodingFormat == null) {
Context context = getContext().getApplicationContext();
SharedPreferences prefs = WebApkSharedPreferences.getPrefs(context);
int splashWidth = prefs.getInt(WebApkSharedPreferences.PREF_SPLASH_WIDTH, -1);
int splashHeight = prefs.getInt(WebApkSharedPreferences.PREF_SPLASH_HEIGHT, -1);
sEncodingFormat = SplashUtils.selectBitmapEncoding(splashWidth, splashHeight);
}
if (sEncodingFormat == Bitmap.CompressFormat.PNG) {
return "image/png";
} else if (sEncodingFormat == Bitmap.CompressFormat.JPEG) {
return "image/jpeg";
}
} }
return null; return null;
} }
......
...@@ -18,6 +18,12 @@ import org.chromium.webapk.shell_apk.WebApkUtils; ...@@ -18,6 +18,12 @@ import org.chromium.webapk.shell_apk.WebApkUtils;
/** Contains splash screen related utility methods. */ /** Contains splash screen related utility methods. */
public class SplashUtils { public class SplashUtils {
/**
* The maximum image size to PNG-encode. JPEG encoding is > 2 times faster on large bitmaps.
* JPEG encoding is preferable to downscaling the screenshot.
*/
private static final int MAX_SIZE_ENCODE_PNG = 1024 * 1024;
/** Creates view with splash screen. */ /** Creates view with splash screen. */
public static View createSplashView(Context context) { public static View createSplashView(Context context) {
Resources resources = context.getResources(); Resources resources = context.getResources();
...@@ -28,6 +34,7 @@ public class SplashUtils { ...@@ -28,6 +34,7 @@ public class SplashUtils {
SplashLayout.createLayout(context, layout, icon, false /* isIconAdaptive */, SplashLayout.createLayout(context, layout, icon, false /* isIconAdaptive */,
false /* isIconGenerated */, resources.getString(R.string.name), false /* isIconGenerated */, resources.getString(R.string.name),
WebApkUtils.shouldUseLightForegroundOnBackground(backgroundColor)); WebApkUtils.shouldUseLightForegroundOnBackground(backgroundColor));
layout.setBackgroundColor(backgroundColor);
return layout; return layout;
} }
...@@ -57,6 +64,12 @@ public class SplashUtils { ...@@ -57,6 +64,12 @@ public class SplashUtils {
return bitmap; return bitmap;
} }
/** Selects encoding for the bitmap based on its size. */
public static Bitmap.CompressFormat selectBitmapEncoding(int width, int height) {
return (width * height <= MAX_SIZE_ENCODE_PNG) ? Bitmap.CompressFormat.PNG
: Bitmap.CompressFormat.JPEG;
}
/** Creates splash view with the passed-in dimensions and screenshots it. */ /** Creates splash view with the passed-in dimensions and screenshots it. */
public static Bitmap createAndImmediatelyScreenshotSplashView( public static Bitmap createAndImmediatelyScreenshotSplashView(
Context context, int splashWidth, int splashHeight, int maxSizeBytes) { Context context, int splashWidth, int splashHeight, int maxSizeBytes) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment