/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.window; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_AVD; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UiThread; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteCallback; import android.os.Trace; import android.util.AttributeSet; import android.util.Log; import android.view.AttachedSurfaceControl; import android.view.Gravity; import android.view.LayoutInflater; import android.view.SurfaceControlViewHost; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.internal.R; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.DecorView; import java.io.Closeable; import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.function.Consumer; import java.util.function.LongConsumer; /** *

The view which allows an activity to customize its splash screen exit animation.

* *

Activities will receive this view as a parameter of * {@link SplashScreen.OnExitAnimationListener#onSplashScreenExit} if * they set {@link SplashScreen#setOnExitAnimationListener}. * When this callback is called, this view will be on top of the activity.

* *

This view is composed of a view containing the splashscreen icon (see * windowSplashscreenAnimatedIcon) and a background. * Developers can use {@link #getIconView} to get this view and replace the drawable or * add animation to it. The background of this view is filled with a single color, which can be * edited during the animation by {@link View#setBackground} or {@link View#setBackgroundColor}.

* * @see SplashScreen */ public final class SplashScreenView extends FrameLayout { private static final String TAG = SplashScreenView.class.getSimpleName(); private static final boolean DEBUG = Build.IS_DEBUGGABLE; private boolean mNotCopyable; private boolean mIsCopied; private int mInitBackgroundColor; private View mIconView; private Bitmap mParceledIconBitmap; private View mBrandingImageView; private Bitmap mParceledBrandingBitmap; private Bitmap mParceledIconBackgroundBitmap; private Duration mIconAnimationDuration; private Instant mIconAnimationStart; private final Rect mTmpRect = new Rect(); private final int[] mTmpPos = new int[2]; @Nullable private SurfaceControlViewHost.SurfacePackage mSurfacePackageCopy; @Nullable private SurfaceControlViewHost.SurfacePackage mSurfacePackage; @Nullable private SurfaceView mSurfaceView; @Nullable private SurfaceControlViewHost mSurfaceHost; @Nullable private RemoteCallback mClientCallback; // cache original window and status private Window mWindow; private boolean mHasRemoved; /** * Internal builder to create a SplashScreenView object. * @hide */ public static class Builder { private final Context mContext; private int mIconSize; private @ColorInt int mBackgroundColor; private Bitmap mParceledIconBitmap; private Bitmap mParceledIconBackgroundBitmap; private Drawable mIconDrawable; // It is only set for legacy splash screen which won't be sent across processes. private Drawable mOverlayDrawable; private Drawable mIconBackground; private SurfaceControlViewHost.SurfacePackage mSurfacePackage; private RemoteCallback mClientCallback; private int mBrandingImageWidth; private int mBrandingImageHeight; private Drawable mBrandingDrawable; private Bitmap mParceledBrandingBitmap; private Instant mIconAnimationStart; private Duration mIconAnimationDuration; private Consumer mUiThreadInitTask; private boolean mAllowHandleSolidColor = true; public Builder(@NonNull Context context) { mContext = context; } /** * When create from {@link SplashScreenViewParcelable}, all the materials were be settled so * you do not need to call other set methods. */ public Builder createFromParcel(SplashScreenViewParcelable parcelable) { mIconSize = parcelable.getIconSize(); mBackgroundColor = parcelable.getBackgroundColor(); mSurfacePackage = parcelable.mSurfacePackage; if (mSurfacePackage == null && parcelable.mIconBitmap != null) { // We only create a Bitmap copies of immobile icons since animated icon are using // a surface view mIconDrawable = new BitmapDrawable(mContext.getResources(), parcelable.mIconBitmap); mParceledIconBitmap = parcelable.mIconBitmap; } if (parcelable.mIconBackground != null) { mIconBackground = new BitmapDrawable(mContext.getResources(), parcelable.mIconBackground); mParceledIconBackgroundBitmap = parcelable.mIconBackground; } if (parcelable.mBrandingBitmap != null) { setBrandingDrawable(new BitmapDrawable(mContext.getResources(), parcelable.mBrandingBitmap), parcelable.mBrandingWidth, parcelable.mBrandingHeight); mParceledBrandingBitmap = parcelable.mBrandingBitmap; } mIconAnimationStart = Instant.ofEpochMilli(parcelable.mIconAnimationStartMillis); mIconAnimationDuration = Duration.ofMillis(parcelable.mIconAnimationDurationMillis); mClientCallback = parcelable.mClientCallback; if (DEBUG) { Log.d(TAG, String.format("Building from parcel drawable: %s", mIconDrawable)); } return this; } /** * Set the rectangle size for the center view. */ public Builder setIconSize(int iconSize) { mIconSize = iconSize; return this; } /** * Set the background color for the view. */ public Builder setBackgroundColor(@ColorInt int backgroundColor) { mBackgroundColor = backgroundColor; return this; } /** * Set the Drawable object to fill entire view */ public Builder setOverlayDrawable(@Nullable Drawable drawable) { mOverlayDrawable = drawable; return this; } /** * Set the Drawable object to fill the center view. */ public Builder setCenterViewDrawable(@Nullable Drawable drawable) { mIconDrawable = drawable; return this; } /** * Set the background color for the icon. */ public Builder setIconBackground(Drawable iconBackground) { mIconBackground = iconBackground; return this; } /** * Set the Runnable that can receive the task which should be executed on UI thread. */ public Builder setUiThreadInitConsumer(Consumer uiThreadInitTask) { mUiThreadInitTask = uiThreadInitTask; return this; } /** * Set the Drawable object and size for the branding view. */ public Builder setBrandingDrawable(@Nullable Drawable branding, int width, int height) { mBrandingDrawable = branding; mBrandingImageWidth = width; mBrandingImageHeight = height; return this; } /** * Sets whether this view can be copied and transferred to the client if the view is * empty style splash screen. */ public Builder setAllowHandleSolidColor(boolean allowHandleSolidColor) { mAllowHandleSolidColor = allowHandleSolidColor; return this; } /** * Create SplashScreenWindowView object from materials. */ public SplashScreenView build() { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SplashScreenView#build"); final LayoutInflater layoutInflater = LayoutInflater.from(mContext); final SplashScreenView view = (SplashScreenView) layoutInflater.inflate(R.layout.splash_screen_view, null, false); view.mInitBackgroundColor = mBackgroundColor; if (mOverlayDrawable != null) { view.setBackground(mOverlayDrawable); } else { view.setBackgroundColor(mBackgroundColor); } view.mClientCallback = mClientCallback; view.mBrandingImageView = view.findViewById(R.id.splashscreen_branding_view); boolean hasIcon = false; // center icon if (mIconDrawable instanceof SplashScreenView.IconAnimateListener || mSurfacePackage != null) { hasIcon = true; if (mUiThreadInitTask != null) { mUiThreadInitTask.accept(() -> view.mIconView = createSurfaceView(view)); } else { view.mIconView = createSurfaceView(view); } view.initIconAnimation(mIconDrawable); view.mIconAnimationStart = mIconAnimationStart; view.mIconAnimationDuration = mIconAnimationDuration; } else if (mIconSize != 0) { ImageView imageView = view.findViewById(R.id.splashscreen_icon_view); assert imageView != null; final ViewGroup.LayoutParams params = imageView.getLayoutParams(); params.width = mIconSize; params.height = mIconSize; imageView.setLayoutParams(params); if (mIconDrawable != null) { imageView.setImageDrawable(mIconDrawable); } if (mIconBackground != null) { imageView.setBackground(mIconBackground); } hasIcon = true; view.mIconView = imageView; } if (mOverlayDrawable != null || (!hasIcon && !mAllowHandleSolidColor)) { view.setNotCopyable(); } view.mParceledIconBackgroundBitmap = mParceledIconBackgroundBitmap; view.mParceledIconBitmap = mParceledIconBitmap; // branding image if (mBrandingImageHeight > 0 && mBrandingImageWidth > 0 && mBrandingDrawable != null) { final ViewGroup.LayoutParams params = view.mBrandingImageView.getLayoutParams(); params.width = mBrandingImageWidth; params.height = mBrandingImageHeight; view.mBrandingImageView.setLayoutParams(params); view.mBrandingImageView.setBackground(mBrandingDrawable); } else { view.mBrandingImageView.setVisibility(GONE); } if (mParceledBrandingBitmap != null) { view.mParceledBrandingBitmap = mParceledBrandingBitmap; } if (DEBUG) { Log.d(TAG, "Build " + view + "\nIcon: view: " + view.mIconView + " drawable: " + mIconDrawable + " size: " + mIconSize + "\nBranding: view: " + view.mBrandingImageView + " drawable: " + mBrandingDrawable + " size w: " + mBrandingImageWidth + " h: " + mBrandingImageHeight); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); return view; } private SurfaceView createSurfaceView(@NonNull SplashScreenView view) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SplashScreenView#createSurfaceView"); final Context viewContext = view.getContext(); final SurfaceView surfaceView = new SurfaceView(viewContext); surfaceView.setPadding(0, 0, 0, 0); surfaceView.setBackground(mIconBackground); if (mSurfacePackage == null) { if (DEBUG) { Log.d(TAG, "SurfaceControlViewHost created on thread " + Thread.currentThread().getId()); } AttachedSurfaceControl attachedSurfaceControl = surfaceView.getRootSurfaceControl(); SurfaceControlViewHost viewHost = new SurfaceControlViewHost(viewContext, viewContext.getDisplay(), attachedSurfaceControl == null ? null : attachedSurfaceControl.getInputTransferToken(), "SplashScreenView"); ImageView imageView = new ImageView(viewContext); imageView.setBackground(mIconDrawable); final int windowFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(mIconSize, mIconSize, WindowManager.LayoutParams.TYPE_APPLICATION, windowFlag, PixelFormat.TRANSPARENT); viewHost.setView(imageView, lp); SurfaceControlViewHost.SurfacePackage surfacePackage = viewHost.getSurfacePackage(); surfaceView.setChildSurfacePackage(surfacePackage); view.mSurfacePackage = surfacePackage; view.mSurfaceHost = viewHost; view.mSurfacePackageCopy = new SurfaceControlViewHost.SurfacePackage( surfacePackage); } else { if (DEBUG) { Log.d(TAG, "Using copy of SurfacePackage in the client"); } view.mSurfacePackage = mSurfacePackage; } if (mIconSize != 0) { LayoutParams lp = new FrameLayout.LayoutParams(mIconSize, mIconSize); lp.gravity = Gravity.CENTER; surfaceView.setLayoutParams(lp); if (DEBUG) { Log.d(TAG, "Icon size " + mIconSize); } } // We ensure that we can blend the alpha of the surface view with the SplashScreenView surfaceView.setUseAlpha(); surfaceView.setZOrderOnTop(true); surfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); view.addView(surfaceView); view.mSurfaceView = surfaceView; Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); return surfaceView; } } /** @hide */ public SplashScreenView(Context context) { super(context); } /** @hide */ public SplashScreenView(Context context, AttributeSet attributeSet) { super(context, attributeSet); } /** * Declared this view is not copyable. * @hide */ public void setNotCopyable() { mNotCopyable = true; } /** * Whether this view is copyable. * @hide */ public boolean isCopyable() { return !mNotCopyable; } /** * Called when this {@link SplashScreenView} has been copied to be transferred to the client. * * @hide */ public void onCopied() { mIsCopied = true; if (mSurfaceView == null) { return; } if (DEBUG) { Log.d(TAG, "Setting SurfaceView's SurfacePackage to null."); } // If we don't release the surface package, the surface will be reparented to this // surface view. So once it's copied into the client process, we release it. mSurfacePackage.release(); mSurfacePackage = null; } /** @hide **/ @Nullable public SurfaceControlViewHost getSurfaceHost() { return mSurfaceHost; } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); // The surface view's alpha is not multiplied with the containing view's alpha, so we // manually do it here if (mSurfaceView != null) { mSurfaceView.setAlpha(mSurfaceView.getAlpha() * alpha); } } /** * Returns the duration of the icon animation if icon is animatable. * * Note the return value can be null or 0 if the * {@link android.R.attr#windowSplashScreenAnimatedIcon} is not * {@link android.graphics.drawable.AnimationDrawable} or * {@link android.graphics.drawable.AnimatedVectorDrawable}. * * @see android.R.attr#windowSplashScreenAnimatedIcon * @see android.R.attr#windowSplashScreenAnimationDuration */ @Nullable public Duration getIconAnimationDuration() { return mIconAnimationDuration; } /** * If the replaced icon is animatable, return the animation start time based on system clock. */ @Nullable public Instant getIconAnimationStart() { return mIconAnimationStart; } /** * @hide */ public void syncTransferSurfaceOnDraw() { if (mSurfacePackage == null) { return; } if (DEBUG) { mSurfacePackage.getSurfaceControl().addOnReparentListener( (transaction, parent) -> Log.e(TAG, String.format("SurfacePackage'surface reparented to %s", parent))); Log.d(TAG, "Transferring surface " + mSurfaceView.toString()); } mSurfaceView.setChildSurfacePackage(mSurfacePackage); } void initIconAnimation(Drawable iconDrawable) { if (!(iconDrawable instanceof IconAnimateListener)) { return; } IconAnimateListener aniDrawable = (IconAnimateListener) iconDrawable; aniDrawable.prepareAnimate(this::animationStartCallback); aniDrawable.setAnimationJankMonitoring(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_AVD); } @Override public void onAnimationEnd(Animator animation) { InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_AVD); } @Override public void onAnimationStart(Animator animation) { InteractionJankMonitor.getInstance().begin( SplashScreenView.this, CUJ_SPLASHSCREEN_AVD); } }); } private void animationStartCallback(long animDuration) { mIconAnimationStart = Instant.now(); if (animDuration >= 0) { mIconAnimationDuration = Duration.ofMillis(animDuration); } } /** *

Remove this view and release its resource.

*

Do not invoke this method from a drawing method * ({@link #onDraw(android.graphics.Canvas)} for instance).

*/ @UiThread public void remove() { if (mHasRemoved) { return; } setVisibility(GONE); if (mParceledIconBitmap != null) { if (mIconView instanceof ImageView) { ((ImageView) mIconView).setImageDrawable(null); } else if (mIconView != null) { mIconView.setBackground(null); } mParceledIconBitmap.recycle(); mParceledIconBitmap = null; } if (mParceledBrandingBitmap != null) { mBrandingImageView.setBackground(null); mParceledBrandingBitmap.recycle(); mParceledBrandingBitmap = null; } if (mParceledIconBackgroundBitmap != null) { if (mIconView != null) { mIconView.setBackground(null); } mParceledIconBackgroundBitmap.recycle(); mParceledIconBackgroundBitmap = null; } if (mWindow != null) { final DecorView decorView = (DecorView) mWindow.peekDecorView(); if (DEBUG) { Log.d(TAG, "remove starting view"); } if (decorView != null) { decorView.removeView(this); } mWindow = null; } mHasRemoved = true; } /** @hide **/ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); releaseAnimationSurfaceHost(); if (mIconView instanceof ImageView imageView && imageView.getDrawable() instanceof Closeable closeableDrawable) { try { closeableDrawable.close(); } catch (IOException ignore) { } } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mBrandingImageView.getDrawingRect(mTmpRect); final int brandingHeight = mTmpRect.height(); if (brandingHeight == 0 || mIconView == null) { return; } final int visibility = mBrandingImageView.getVisibility(); if (visibility != VISIBLE) { return; } final int currentHeight = b - t; mIconView.getLocationInWindow(mTmpPos); mIconView.getDrawingRect(mTmpRect); final int iconHeight = mTmpRect.height(); final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mBrandingImageView.getLayoutParams(); if (params == null) { Log.e(TAG, "Unable to adjust branding image layout, layout changed?"); return; } final int marginBottom = params.bottomMargin; final int remainingHeight = currentHeight - mTmpPos[1] - iconHeight; final int remainingMaxMargin = remainingHeight - brandingHeight; if (remainingHeight < brandingHeight) { // unable to show the branding image, hide it mBrandingImageView.setVisibility(GONE); } else if (remainingMaxMargin < marginBottom) { // shorter than original margin params.bottomMargin = (int) Math.round(remainingMaxMargin / 2.0); mBrandingImageView.setLayoutParams(params); } // nothing need to adjust } private void releaseAnimationSurfaceHost() { if (mSurfaceHost != null && !mIsCopied) { if (DEBUG) { Log.d(TAG, "Shell removed splash screen." + " Releasing SurfaceControlViewHost on thread #" + Thread.currentThread().getId()); } releaseIconHost(mSurfaceHost); mSurfaceHost = null; } else if (mSurfacePackage != null && mSurfaceHost == null) { mSurfacePackage = null; mClientCallback.sendResult(null); } } /** * Release the host which hold the SurfaceView of the icon. * @hide */ public static void releaseIconHost(SurfaceControlViewHost host) { final Drawable background = host.getView().getBackground(); if (background instanceof SplashScreenView.IconAnimateListener) { ((SplashScreenView.IconAnimateListener) background).stopAnimation(); } host.release(); } /** * Called when this view is attached to a window of an activity. * * @hide */ public void attachHostWindow(Window window) { mWindow = window; } /** * Get the view containing the Splash Screen icon and its background. * @see android.R.attr#windowSplashScreenAnimatedIcon */ public @Nullable View getIconView() { return mIconView; } /** * Get the branding image view. * @hide */ @TestApi public @Nullable View getBrandingView() { return mBrandingImageView; } /** * Get the initial background color of this view. * @hide */ public @ColorInt int getInitBackgroundColor() { return mInitBackgroundColor; } /** * An interface for an animatable drawable object to register a callback when animation start. * @hide */ public interface IconAnimateListener { /** * Prepare the animation if this drawable also be animatable. * @param startListener The callback listener used to receive the start of the animation. */ void prepareAnimate(LongConsumer startListener); /** * Stop animation. */ void stopAnimation(); /** * Provides a chance to start interaction jank monitoring in avd animation. * @param listener a listener to start jank monitoring */ default void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {} } /** * Use to create {@link SplashScreenView} object across process. * @hide */ public static class SplashScreenViewParcelable implements Parcelable { private int mIconSize; private int mBackgroundColor; private Bitmap mIconBackground; private Bitmap mIconBitmap = null; private int mBrandingWidth; private int mBrandingHeight; private Bitmap mBrandingBitmap; private long mIconAnimationStartMillis; private long mIconAnimationDurationMillis; private SurfaceControlViewHost.SurfacePackage mSurfacePackage; private RemoteCallback mClientCallback; public SplashScreenViewParcelable(SplashScreenView view) { final View iconView = view.getIconView(); mIconSize = iconView != null ? iconView.getWidth() : 0; mBackgroundColor = view.getInitBackgroundColor(); mIconBackground = iconView != null ? copyDrawable(iconView.getBackground()) : null; mSurfacePackage = view.mSurfacePackageCopy; if (mSurfacePackage == null) { // We only need to copy the drawable if we are not using a SurfaceView mIconBitmap = iconView != null ? copyDrawable(((ImageView) view.getIconView()).getDrawable()) : null; } mBrandingBitmap = copyDrawable(view.getBrandingView().getBackground()); ViewGroup.LayoutParams params = view.getBrandingView().getLayoutParams(); mBrandingWidth = params.width; mBrandingHeight = params.height; if (view.getIconAnimationStart() != null) { mIconAnimationStartMillis = view.getIconAnimationStart().toEpochMilli(); } if (view.getIconAnimationDuration() != null) { mIconAnimationDurationMillis = view.getIconAnimationDuration().toMillis(); } } private Bitmap copyDrawable(Drawable drawable) { if (drawable != null) { final Rect initialBounds = drawable.copyBounds(); final int width = initialBounds.width(); final int height = initialBounds.height(); if (width <= 0 || height <= 0) { return null; } final Bitmap snapshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); final Canvas bmpCanvas = new Canvas(snapshot); drawable.setBounds(0, 0, width, height); drawable.draw(bmpCanvas); final Bitmap copyBitmap = snapshot.createAshmemBitmap(); snapshot.recycle(); return copyBitmap; } return null; } private SplashScreenViewParcelable(@NonNull Parcel source) { readParcel(source); } private void readParcel(@NonNull Parcel source) { mIconSize = source.readInt(); mBackgroundColor = source.readInt(); mIconBitmap = source.readTypedObject(Bitmap.CREATOR); mBrandingWidth = source.readInt(); mBrandingHeight = source.readInt(); mBrandingBitmap = source.readTypedObject(Bitmap.CREATOR); mIconAnimationStartMillis = source.readLong(); mIconAnimationDurationMillis = source.readLong(); mIconBackground = source.readTypedObject(Bitmap.CREATOR); mSurfacePackage = source.readTypedObject(SurfaceControlViewHost.SurfacePackage.CREATOR); mClientCallback = source.readTypedObject(RemoteCallback.CREATOR); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mIconSize); dest.writeInt(mBackgroundColor); dest.writeTypedObject(mIconBitmap, flags); dest.writeInt(mBrandingWidth); dest.writeInt(mBrandingHeight); dest.writeTypedObject(mBrandingBitmap, flags); dest.writeLong(mIconAnimationStartMillis); dest.writeLong(mIconAnimationDurationMillis); dest.writeTypedObject(mIconBackground, flags); dest.writeTypedObject(mSurfacePackage, flags); dest.writeTypedObject(mClientCallback, flags); } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public SplashScreenViewParcelable createFromParcel(@NonNull Parcel source) { return new SplashScreenViewParcelable(source); } public SplashScreenViewParcelable[] newArray(int size) { return new SplashScreenViewParcelable[size]; } }; /** * Release the bitmap if another process cannot handle it. */ public void clearIfNeeded() { if (mIconBitmap != null) { mIconBitmap.recycle(); mIconBitmap = null; } if (mBrandingBitmap != null) { mBrandingBitmap.recycle(); mBrandingBitmap = null; } } int getIconSize() { return mIconSize; } int getBackgroundColor() { return mBackgroundColor; } /** * Sets the {@link RemoteCallback} that will be called by the client to notify the shell * of the removal of the {@link SplashScreenView}. */ public void setClientCallback(@NonNull RemoteCallback clientCallback) { mClientCallback = clientCallback; } } }