chromium/chrome/android/webapk/shell_apk/src/org/chromium/webapk/shell_apk/h2o/SplashActivity.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.webapk.shell_apk.h2o;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Pair;
import android.view.View;
import android.view.ViewTreeObserver;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys;
import org.chromium.webapk.lib.common.WebApkMetaDataUtils;
import org.chromium.webapk.shell_apk.HostBrowserLauncher;
import org.chromium.webapk.shell_apk.HostBrowserLauncherParams;
import org.chromium.webapk.shell_apk.HostBrowserUtils;
import org.chromium.webapk.shell_apk.LaunchHostBrowserSelector;
import org.chromium.webapk.shell_apk.WebApkUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** Displays splash screen. */
public class SplashActivity extends Activity {
    /** Task to screenshot and encode splash. */
    @SuppressWarnings("NoAndroidAsyncTaskCheck")
    @Nullable
    private android.os.AsyncTask mScreenshotSplashTask;

    @IntDef({ActivityResult.NONE, ActivityResult.CANCELED, ActivityResult.IGNORE})
    @Retention(RetentionPolicy.SOURCE)
    private @interface ActivityResult {
        int NONE = 0;
        int CANCELED = 1;
        int IGNORE = 2;
    }

    private View mSplashView;
    private Bitmap mBitmap;
    private HostBrowserLauncherParams mParams;
    private @ActivityResult int mResult;

    private final LaunchTrigger mLaunchTrigger = new LaunchTrigger(this::encodeSplashInBackground);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        boolean androidSSplashSuccess = false;
        if (androidSSplashScreenEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // When launched with a data Intent, the splash screen is created, but
            // SplashScreen.OnExitAnimationListener#onSplashScreenExit is not called.
            // Fall back to manually creating our own splash screen in that case.
            androidSSplashSuccess =
                    SplashUtilsForS.listenForSplashScreen(
                            this,
                            getWindow(),
                            (view, bitmap) -> {
                                mSplashView = view;
                                mBitmap = bitmap;
                                mLaunchTrigger.onSplashScreenReady();
                            });
        }
        if (!androidSSplashSuccess) {
            // Fall back to the old behaviour if our reflection based method to launch the Android S
            // splash screen fails.
            showPreSSplashScreen();
        }
        final long splashAddedToLayoutTimeMs = SystemClock.elapsedRealtime();

        // On Android O+, if:
        // - Chrome is translucent
        // AND
        // - Both the WebAPK and Chrome have been killed by the Android out-of-memory killer
        // both the SplashActivity and the browser activity are created when the user selects the
        // WebAPK in Android Recents.
        if (!new ComponentName(this, SplashActivity.class)
                .equals(WebApkUtils.fetchTopActivityComponent(this, getTaskId()))) {
            return;
        }

        selectHostBrowser(splashAddedToLayoutTimeMs);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (mResult != ActivityResult.IGNORE && resultCode == Activity.RESULT_CANCELED) {
            mResult = ActivityResult.CANCELED;
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);

        // Clear flag set by SplashActivity#onActivityResult()
        // The host browser activity is killed - triggering SplashActivity#onActivityResult()
        // - when SplashActivity gets a new intent because SplashActivity has launchMode
        // "singleTask".
        mResult = ActivityResult.IGNORE;

        mLaunchTrigger.reset();

        selectHostBrowser(/* splashShownTimeMs= */ -1);
    }

    @Override
    public void onResume() {
        super.onResume();

        // If Activity#onActivityResult() will be called, it will be called prior to the
        // activity being resumed.
        if (mResult == ActivityResult.CANCELED) {
            finish();
            return;
        }

        mResult = ActivityResult.NONE;
        mLaunchTrigger.onWillLaunch();
    }

    @Override
    public void onDestroy() {
        SplashContentProvider.clearCache();
        if (mScreenshotSplashTask != null) {
            mScreenshotSplashTask.cancel(false);
            mScreenshotSplashTask = null;
        }
        super.onDestroy();
    }

    private void selectHostBrowser(final long splashShownTimeMs) {
        new LaunchHostBrowserSelector(this)
                .selectHostBrowser(
                        new LaunchHostBrowserSelector.Callback() {
                            @Override
                            public void onBrowserSelected(
                                    String hostBrowserPackageName, boolean dialogShown) {
                                if (hostBrowserPackageName == null) {
                                    finish();
                                    return;
                                }
                                HostBrowserLauncherParams params =
                                        HostBrowserLauncherParams.createForIntent(
                                                SplashActivity.this,
                                                getIntent(),
                                                hostBrowserPackageName,
                                                dialogShown,
                                                /* launchTimeMs= */ -1,
                                                splashShownTimeMs);
                                onHostBrowserSelected(params);
                            }
                        });
    }

    private void showPreSSplashScreen() {
        Bundle metadata = WebApkUtils.readMetaData(this);
        updateStatusBar(metadata);

        int orientation =
                WebApkUtils.computeNaturalScreenLockOrientationFromMetaData(this, metadata);
        if (orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
            setRequestedOrientation(orientation);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // This case will be hit when we are launched by a data intent.
            mSplashView = SplashUtilsForS.createSplashView(this);
        } else {
            mSplashView = SplashUtils.createSplashView(this);
        }
        mSplashView
                .getViewTreeObserver()
                .addOnGlobalLayoutListener(
                        new ViewTreeObserver.OnGlobalLayoutListener() {
                            @Override
                            public void onGlobalLayout() {
                                if (mSplashView.getWidth() == 0 || mSplashView.getHeight() == 0) {
                                    return;
                                }

                                mSplashView
                                        .getViewTreeObserver()
                                        .removeOnGlobalLayoutListener(this);
                                mBitmap =
                                        SplashUtils.screenshotView(
                                                mSplashView,
                                                SplashContentProvider.MAX_TRANSFER_SIZE_BYTES);
                                mLaunchTrigger.onSplashScreenReady();
                            }
                        });
        setContentView(mSplashView);
    }

    /** Sets the the color of the status bar and status bar icons. */
    @VisibleForTesting
    void updateStatusBar(Bundle metadata) {
        int statusBarColor =
                (int)
                        WebApkMetaDataUtils.getLongFromMetaData(
                                metadata, WebApkMetaDataKeys.THEME_COLOR, Color.WHITE);
        int defaultDarkStatusBarColor =
                (int)
                        WebApkMetaDataUtils.getLongFromMetaData(
                                metadata, WebApkMetaDataKeys.THEME_COLOR, Color.BLACK);
        int darkStatusBarColor =
                (int)
                        WebApkMetaDataUtils.getLongFromMetaData(
                                metadata,
                                WebApkMetaDataKeys.DARK_THEME_COLOR,
                                defaultDarkStatusBarColor);
        WebApkUtils.setStatusBarColor(
                this, WebApkUtils.inDarkMode(this) ? darkStatusBarColor : statusBarColor);
        boolean needsDarkStatusBarIcons =
                !WebApkUtils.shouldUseLightForegroundOnBackground(statusBarColor);
        WebApkUtils.setStatusBarIconColor(
                getWindow().getDecorView().getRootView(), needsDarkStatusBarIcons, this);
    }

    /** Called once the host browser has been selected. */
    private void onHostBrowserSelected(HostBrowserLauncherParams params) {
        if (params == null) {
            finish();
            return;
        }

        Context appContext = getApplicationContext();

        if (!HostBrowserUtils.shouldIntentLaunchSplashActivity(params)) {
            HostBrowserLauncher.launch(this, params);
            H2OLauncher.changeEnabledComponentsAndKillShellApk(
                    appContext,
                    new ComponentName(appContext, H2OMainActivity.class),
                    new ComponentName(appContext, H2OOpaqueMainActivity.class));
            finish();
            return;
        }

        mParams = params;
        mLaunchTrigger.onHostBrowserSelected();
    }

    /**
     * Launches the host browser on top of {@link SplashActivity}.
     *
     * @param splashEncoded Encoded screenshot of {@link mSplashView}.
     * @param encodingFormat The screenshot's encoding format.
     */
    private void launch(byte[] splashEncoded, Bitmap.CompressFormat encodingFormat) {
        SplashContentProvider.cache(
                this,
                splashEncoded,
                encodingFormat,
                mSplashView.getWidth(),
                mSplashView.getHeight());
        H2OLauncher.launch(this, mParams);
        mParams = null;
    }

    /** Screenshots and encodes {@link mSplashView} on a background thread. */
    @SuppressWarnings("NoAndroidAsyncTaskCheck")
    private void encodeSplashInBackground() {
        if (mBitmap == null) {
            launch(null, Bitmap.CompressFormat.PNG);
            return;
        }

        mScreenshotSplashTask =
                new android.os.AsyncTask<Void, Void, Pair<byte[], Bitmap.CompressFormat>>() {
                    @Override
                    protected Pair<byte[], Bitmap.CompressFormat> doInBackground(Void... args) {
                        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                            Bitmap.CompressFormat encodingFormat =
                                    SplashUtils.selectBitmapEncoding(
                                            mBitmap.getWidth(), mBitmap.getHeight());
                            mBitmap.compress(encodingFormat, 100, out);
                            return Pair.create(out.toByteArray(), encodingFormat);
                        } catch (IOException e) {
                        }
                        return null;
                    }

                    @Override
                    protected void onPostExecute(
                            Pair<byte[], Bitmap.CompressFormat> splashEncoded) {
                        mScreenshotSplashTask = null;
                        launch(
                                (splashEncoded == null) ? null : splashEncoded.first,
                                (splashEncoded == null)
                                        ? Bitmap.CompressFormat.PNG
                                        : splashEncoded.second);
                    }

                    // Do nothing if task was cancelled.
                }.executeOnExecutor(android.os.AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /** Whether we enable integration with Android S splash screens. */
    static boolean androidSSplashScreenEnabled() {
        return false;
    }
}