blob: 8072bcbd730abbc27eed7aa63745b03d9698326b [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.webapps;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.metrics.WebApkUma;
import org.chromium.chrome.browser.metrics.WebappUma;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.ColorUtils;
import org.chromium.chrome.browser.webapps.WebappActivity.ActivityType;
import org.chromium.net.NetError;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.webapk.lib.common.splash.SplashLayout;
/** Shows and hides splash screen. */
class WebappSplashScreenController extends EmptyTabObserver {
// No error.
public static final int ERROR_OK = 0;
/** Used to schedule splash screen hiding. */
private CompositorViewHolder mCompositorViewHolder;
/** View to which the splash screen is added. */
private ViewGroup mParentView;
/** Whether native was loaded. Native must be loaded in order to record metrics. */
private boolean mNativeLoaded;
/** Whether the splash screen layout was initialized. */
private boolean mInitializedLayout;
private ViewGroup mSplashScreen;
private WebappUma mWebappUma;
/** The error code of the navigation. */
private int mErrorCode;
private WebappOfflineDialog mOfflineDialog;
/** Indicates whether reloading is allowed. */
private boolean mAllowReloads;
private String mAppName;
private @ActivityType int mActivityType;
private ObserverList<SplashscreenObserver> mObservers;
public WebappSplashScreenController() {
mWebappUma = new WebappUma();
mObservers = new ObserverList<>();
addObserver(mWebappUma);
}
/** Shows the splash screen. */
public void showSplashScreen(
@ActivityType int activityType, ViewGroup parentView, final WebappInfo webappInfo) {
mActivityType = activityType;
mParentView = parentView;
mAppName = webappInfo.name();
Context context = ContextUtils.getApplicationContext();
final int backgroundColor = ColorUtils.getOpaqueColor(webappInfo.backgroundColor(
ApiCompatibilityUtils.getColor(context.getResources(), R.color.webapp_default_bg)));
mSplashScreen = new FrameLayout(context);
mSplashScreen.setBackgroundColor(backgroundColor);
mParentView.addView(mSplashScreen);
startSplashscreenTraceEvents();
notifySplashscreenVisible();
mWebappUma.recordSplashscreenBackgroundColor(webappInfo.hasValidBackgroundColor()
? WebappUma.SplashScreenColorStatus.CUSTOM
: WebappUma.SplashScreenColorStatus.DEFAULT);
mWebappUma.recordSplashscreenThemeColor(webappInfo.hasValidThemeColor()
? WebappUma.SplashScreenColorStatus.CUSTOM
: WebappUma.SplashScreenColorStatus.DEFAULT);
if (activityType == WebappActivity.ActivityType.WEBAPK) {
initializeLayout(webappInfo, backgroundColor, ((WebApkInfo) webappInfo).splashIcon());
return;
}
WebappDataStorage storage =
WebappRegistry.getInstance().getWebappDataStorage(webappInfo.id());
if (storage == null) {
initializeLayout(webappInfo, backgroundColor, null);
return;
}
storage.getSplashScreenImage(new WebappDataStorage.FetchCallback<Bitmap>() {
@Override
public void onDataRetrieved(Bitmap splashImage) {
initializeLayout(webappInfo, backgroundColor, splashImage);
}
});
}
/**
* Transfers a {@param viewHierarchy} to the splashscreen's parent view while keeping the
* splashscreen on top.
*/
public void setViewHierarchyBelowSplashscreen(ViewGroup viewHierarchy) {
WarmupManager.transferViewHeirarchy(viewHierarchy, mParentView);
mParentView.bringChildToFront(mSplashScreen);
}
/** Should be called once native has loaded. */
public void onFinishedNativeInit(Tab tab, CompositorViewHolder compositorViewHolder) {
mNativeLoaded = true;
mCompositorViewHolder = compositorViewHolder;
tab.addObserver(this);
if (mInitializedLayout) mWebappUma.commitMetrics();
}
@VisibleForTesting
ViewGroup getSplashScreenForTests() {
return mSplashScreen;
}
@Override
public void didFirstVisuallyNonEmptyPaint(Tab tab) {
if (canHideSplashScreen()) {
hideSplashScreenOnDrawingFinished(tab, WebappUma.SplashScreenHidesReason.PAINT);
}
}
@Override
public void onPageLoadFinished(Tab tab, String url) {
if (canHideSplashScreen()) {
hideSplashScreenOnDrawingFinished(tab, WebappUma.SplashScreenHidesReason.LOAD_FINISHED);
}
}
@Override
public void onPageLoadFailed(Tab tab, int errorCode) {
if (canHideSplashScreen()) {
animateHidingSplashScreen(tab, WebappUma.SplashScreenHidesReason.LOAD_FAILED);
}
}
@Override
public void onCrash(Tab tab) {
animateHidingSplashScreen(tab, WebappUma.SplashScreenHidesReason.CRASH);
}
@Override
public void onDidFinishNavigation(final Tab tab, final String url, boolean isInMainFrame,
boolean isErrorPage, boolean hasCommitted, boolean isSameDocument,
boolean isFragmentNavigation, Integer pageTransition, int errorCode,
int httpStatusCode) {
if (mActivityType == WebappActivity.ActivityType.WEBAPP || !isInMainFrame) return;
mErrorCode = errorCode;
switch (mErrorCode) {
case ERROR_OK:
if (mOfflineDialog != null) {
mOfflineDialog.cancel();
mOfflineDialog = null;
}
break;
case NetError.ERR_NETWORK_CHANGED:
onNetworkChanged(tab);
break;
default:
onNetworkError(tab, errorCode);
break;
}
WebApkUma.recordNetworkErrorWhenLaunch(-errorCode);
}
protected boolean canHideSplashScreen() {
if (mActivityType == WebappActivity.ActivityType.WEBAPP) return true;
return mErrorCode != NetError.ERR_INTERNET_DISCONNECTED
&& mErrorCode != NetError.ERR_NETWORK_CHANGED;
}
private void onNetworkChanged(Tab tab) {
if (!mAllowReloads) return;
// It is possible that we get {@link NetError.ERR_NETWORK_CHANGED} during the first
// reload after the device is online. The navigation will fail until the next auto
// reload fired by {@link NetErrorHelperCore}. We call reload explicitly to reduce the
// waiting time.
tab.reloadIgnoringCache();
mAllowReloads = false;
}
private void onNetworkError(final Tab tab, int errorCode) {
if (mOfflineDialog != null || tab.getActivity() == null) return;
final NetworkChangeNotifier.ConnectionTypeObserver observer =
new NetworkChangeNotifier.ConnectionTypeObserver() {
@Override
public void onConnectionTypeChanged(int connectionType) {
if (!NetworkChangeNotifier.isOnline()) return;
NetworkChangeNotifier.removeConnectionTypeObserver(this);
tab.reloadIgnoringCache();
// One more reload is allowed after the network connection is back.
mAllowReloads = true;
}
};
NetworkChangeNotifier.addConnectionTypeObserver(observer);
mOfflineDialog = new WebappOfflineDialog();
mOfflineDialog.show(tab.getActivity(), mAppName,
mActivityType == WebappActivity.ActivityType.WEBAPK, errorCode);
}
/** Sets the splash screen layout and sets the splash screen's title and icon. */
private void initializeLayout(WebappInfo webappInfo, int backgroundColor, Bitmap splashImage) {
mInitializedLayout = true;
Context context = ContextUtils.getApplicationContext();
Resources resources = context.getResources();
Bitmap displayIcon = splashImage;
boolean displayIconGenerated = false;
if (displayIcon == null) {
displayIcon = webappInfo.icon();
displayIconGenerated = webappInfo.isIconGenerated();
}
@SplashLayout.IconClassification
int displayIconClassification =
SplashLayout.classifyIcon(resources, displayIcon, displayIconGenerated);
if (displayIconClassification == SplashLayout.IconClassification.INVALID) {
mWebappUma.recordSplashscreenIconType(WebappUma.SplashScreenIconType.NONE);
} else {
// Record stats about the splash screen.
@WebappUma.SplashScreenIconType
int splashScreenIconType;
if (splashImage == null) {
splashScreenIconType = WebappUma.SplashScreenIconType.FALLBACK;
} else if (displayIconClassification == SplashLayout.IconClassification.SMALL) {
splashScreenIconType = WebappUma.SplashScreenIconType.CUSTOM_SMALL;
} else {
splashScreenIconType = WebappUma.SplashScreenIconType.CUSTOM;
}
mWebappUma.recordSplashscreenIconType(splashScreenIconType);
mWebappUma.recordSplashscreenIconSize(
Math.round(displayIcon.getScaledWidth(resources.getDisplayMetrics())
/ resources.getDisplayMetrics().density));
}
SplashLayout.createLayout(context, mSplashScreen, displayIcon, displayIconClassification,
webappInfo.name(),
ColorUtils.shouldUseLightForegroundOnBackground(backgroundColor));
if (mNativeLoaded) mWebappUma.commitMetrics();
}
/**
* Schedules the splash screen hiding once the compositor has finished drawing a frame.
*
* Without this callback we were seeing a short flash of white between the splash screen and
* the web content (crbug.com/734500).
* */
private void hideSplashScreenOnDrawingFinished(final Tab tab, final int reason) {
if (mSplashScreen == null) return;
if (mCompositorViewHolder == null) {
animateHidingSplashScreen(tab, reason);
return;
}
mCompositorViewHolder.getCompositorView().surfaceRedrawNeededAsync(
() -> { animateHidingSplashScreen(tab, reason); });
}
/** Performs the splash screen hiding animation. */
private void animateHidingSplashScreen(final Tab tab, final int reason) {
if (mSplashScreen == null) return;
mSplashScreen.animate().alpha(0f).withEndAction(new Runnable() {
@Override
public void run() {
if (mSplashScreen == null) return;
mParentView.removeView(mSplashScreen);
finishSplashscreenTraceEvents();
tab.removeObserver(WebappSplashScreenController.this);
mSplashScreen = null;
mCompositorViewHolder = null;
notifySplashscreenHidden(reason);
}
});
}
private static class SingleShotOnDrawListener implements ViewTreeObserver.OnDrawListener {
private final View mView;
private final Runnable mAction;
private boolean mHasRun;
public static void install(View view, Runnable action) {
SingleShotOnDrawListener listener = new SingleShotOnDrawListener(view, action);
view.getViewTreeObserver().addOnDrawListener(listener);
}
private SingleShotOnDrawListener(View view, Runnable action) {
mView = view;
mAction = action;
}
@Override
public void onDraw() {
if (mHasRun) return;
mHasRun = true;
mAction.run();
// Cannot call removeOnDrawListener within OnDraw, so do on next tick.
mView.post(() -> mView.getViewTreeObserver().removeOnDrawListener(this));
}
};
private void startSplashscreenTraceEvents() {
TraceEvent.startAsync("WebappSplashScreen", hashCode());
SingleShotOnDrawListener.install(mParentView,
() -> { TraceEvent.startAsync("WebappSplashScreen.visible", hashCode()); });
}
private void finishSplashscreenTraceEvents() {
TraceEvent.finishAsync("WebappSplashScreen", hashCode());
SingleShotOnDrawListener.install(mParentView,
() -> { TraceEvent.finishAsync("WebappSplashScreen.visible", hashCode()); });
}
/**
* Register an observer for the splashscreen hidden/visible events.
*/
public void addObserver(SplashscreenObserver observer) {
mObservers.addObserver(observer);
}
/**
* Deegister an observer for the splashscreen hidden/visible events.
*/
public void removeObserver(SplashscreenObserver observer) {
mObservers.removeObserver(observer);
}
private void notifySplashscreenVisible() {
for (SplashscreenObserver observer : mObservers) {
observer.onSplashscreenShown();
}
}
private void notifySplashscreenHidden(int reason) {
for (SplashscreenObserver observer : mObservers) {
observer.onSplashscreenHidden(reason);
}
mObservers.clear();
}
}