package com.mopub.mraid; import android.annotation.TargetApi; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.Rect; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowManager; import android.webkit.ConsoleMessage; import android.webkit.JsResult; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import com.mopub.common.AdReport; import com.mopub.common.CloseableLayout; import com.mopub.common.CloseableLayout.ClosePosition; import com.mopub.common.CloseableLayout.OnCloseListener; import com.mopub.common.MoPubBrowser; import com.mopub.common.Preconditions; import com.mopub.common.VisibleForTesting; import com.mopub.common.logging.MoPubLog; import com.mopub.common.util.DeviceUtils; import com.mopub.common.util.Dips; import com.mopub.common.util.Intents; import com.mopub.common.util.Views; import com.mopub.exceptions.IntentNotResolvableException; import com.mopub.exceptions.UrlParseException; import com.mopub.mobileads.MraidVideoPlayerActivity; import com.mopub.mobileads.util.WebViews; import com.mopub.mraid.MraidBridge.MraidBridgeListener; import com.mopub.mraid.MraidBridge.MraidWebView; import java.net.URI; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; import static com.mopub.common.util.Utils.bitMaskContainsFlag; public class MraidController { private final AdReport mAdReport; public interface MraidListener { public void onLoaded(View view); public void onFailedToLoad(); public void onExpand(); public void onOpen(); public void onClose(); } public interface UseCustomCloseListener { public void useCustomCloseChanged(boolean useCustomClose); } @Nullable private Activity mActivity; @NonNull private final Context mContext; @NonNull private final PlacementType mPlacementType; // An ad container, which contains the ad web view in default state, but is empty when expanded. @NonNull private final FrameLayout mDefaultAdContainer; // Ad ad container which contains the ad view in expanded state. @NonNull private final CloseableLayout mCloseableAdContainer; // Root view, where we'll add the expanded ad @Nullable private ViewGroup mRootView; // Helper classes for updating screen values @NonNull private final ScreenMetricsWaiter mScreenMetricsWaiter; @NonNull private final MraidScreenMetrics mScreenMetrics; // Current view state @NonNull private ViewState mViewState = ViewState.LOADING; // Listeners @Nullable private MraidListener mMraidListener; @Nullable private UseCustomCloseListener mOnCloseButtonListener; @Nullable private MraidWebViewDebugListener mDebugListener; // The WebView which will display the ad. "Two part" creatives, loaded via handleExpand(URL) // are shown in a separate web view @Nullable private MraidWebView mMraidWebView; @Nullable private MraidWebView mTwoPartWebView; // A bridge to handle all interactions with the WebView HTML and Javascript. @NonNull private final MraidBridge mMraidBridge; @NonNull private final MraidBridge mTwoPartBridge; @NonNull private OrientationBroadcastReceiver mOrientationBroadcastReceiver = new OrientationBroadcastReceiver(); // Stores the requested orientation for the Activity to which this controller's view belongs. // This is needed to restore the Activity's requested orientation in the event that the view // itself requires an orientation lock. @Nullable private Integer mOriginalActivityOrientation; private boolean mAllowOrientationChange = true; private MraidOrientation mForceOrientation = MraidOrientation.NONE; private final MraidNativeCommandHandler mMraidNativeCommandHandler; private boolean mIsPaused; public MraidController(@NonNull Context context, @Nullable AdReport adReport, @NonNull PlacementType placementType) { this(context, adReport, placementType, new MraidBridge(adReport, placementType), new MraidBridge(adReport, PlacementType.INTERSTITIAL), new ScreenMetricsWaiter()); } @VisibleForTesting MraidController(@NonNull Context context, @Nullable AdReport adReport, @NonNull PlacementType placementType, @NonNull MraidBridge bridge, @NonNull MraidBridge twoPartBridge, @NonNull ScreenMetricsWaiter screenMetricsWaiter) { mContext = context; mAdReport = adReport; if (mContext instanceof Activity) { mActivity = (Activity) mContext; } mPlacementType = placementType; mMraidBridge = bridge; mTwoPartBridge = twoPartBridge; mScreenMetricsWaiter = screenMetricsWaiter; mViewState = ViewState.LOADING; DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); mScreenMetrics = new MraidScreenMetrics(mContext, displayMetrics.density); mDefaultAdContainer = new FrameLayout(mContext); mCloseableAdContainer = new CloseableLayout(mContext); mCloseableAdContainer.setOnCloseListener(new OnCloseListener() { @Override public void onClose() { handleClose(); } }); View dimmingView = new View(mContext); dimmingView.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { return true; } }); mCloseableAdContainer.addView(dimmingView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mOrientationBroadcastReceiver.register(mContext); mMraidBridge.setMraidBridgeListener(mMraidBridgeListener); mTwoPartBridge.setMraidBridgeListener(mTwoPartBridgeListener); mMraidNativeCommandHandler = new MraidNativeCommandHandler(); } @SuppressWarnings("FieldCanBeLocal") private final MraidBridgeListener mMraidBridgeListener = new MraidBridgeListener() { @Override public void onPageLoaded() { handlePageLoad(); } @Override public void onVisibilityChanged(final boolean isVisible) { // The bridge only receives visibility events if there is no 2 part covering it if (!mTwoPartBridge.isAttached()) { mMraidBridge.notifyViewability(isVisible); } } @Override public boolean onJsAlert(@NonNull final String message, @NonNull final JsResult result) { return handleJsAlert(message, result); } @Override public boolean onConsoleMessage(@NonNull final ConsoleMessage consoleMessage) { return handleConsoleMessage(consoleMessage); } @Override public void onClose() { handleClose(); } @Override public void onResize(final int width, final int height, final int offsetX, final int offsetY, @NonNull final ClosePosition closePosition, final boolean allowOffscreen) throws MraidCommandException { handleResize(width, height, offsetX, offsetY, closePosition, allowOffscreen); } public void onExpand(@Nullable final URI uri, final boolean shouldUseCustomClose) throws MraidCommandException { handleExpand(uri, shouldUseCustomClose); } @Override public void onUseCustomClose(final boolean shouldUseCustomClose) { handleCustomClose(shouldUseCustomClose); } @Override public void onSetOrientationProperties(final boolean allowOrientationChange, final MraidOrientation forceOrientation) throws MraidCommandException { handleSetOrientationProperties(allowOrientationChange, forceOrientation); } @Override public void onOpen(@NonNull final URI uri) { handleOpen(uri.toString()); } @Override public void onPlayVideo(@NonNull final URI uri) { handleShowVideo(uri.toString()); } }; @SuppressWarnings("FieldCanBeLocal") private final MraidBridgeListener mTwoPartBridgeListener = new MraidBridgeListener() { @Override public void onPageLoaded() { handleTwoPartPageLoad(); } @Override public void onVisibilityChanged(final boolean isVisible) { // The original web view must see the 2-part bridges visibility mMraidBridge.notifyViewability(isVisible); mTwoPartBridge.notifyViewability(isVisible); } @Override public boolean onJsAlert(@NonNull final String message, @NonNull final JsResult result) { return handleJsAlert(message, result); } @Override public boolean onConsoleMessage(@NonNull final ConsoleMessage consoleMessage) { return handleConsoleMessage(consoleMessage); } @Override public void onResize(final int width, final int height, final int offsetX, final int offsetY, @NonNull final ClosePosition closePosition, final boolean allowOffscreen) throws MraidCommandException { throw new MraidCommandException("Not allowed to resize from an expanded state"); } @Override public void onExpand(@Nullable final URI uri, final boolean shouldUseCustomClose) { // The MRAID spec dictates that this is ignored rather than firing an error } @Override public void onClose() { handleClose(); } @Override public void onUseCustomClose(final boolean shouldUseCustomClose) { handleCustomClose(shouldUseCustomClose); } @Override public void onSetOrientationProperties(final boolean allowOrientationChange, final MraidOrientation forceOrientation) throws MraidCommandException { handleSetOrientationProperties(allowOrientationChange, forceOrientation); } @Override public void onOpen(final URI uri) { handleOpen(uri.toString()); } @Override public void onPlayVideo(@NonNull final URI uri) { handleShowVideo(uri.toString()); } }; public void setMraidListener(@Nullable MraidListener mraidListener) { mMraidListener = mraidListener; } public void setUseCustomCloseListener(@Nullable UseCustomCloseListener listener) { mOnCloseButtonListener = listener; } public void setDebugListener(@Nullable MraidWebViewDebugListener debugListener) { mDebugListener = debugListener; } public void loadContent(@NonNull String htmlData) { Preconditions.checkState(mMraidWebView == null, "loadContent should only be called once"); mMraidWebView = new MraidWebView(mContext); mMraidBridge.attachView(mMraidWebView); mDefaultAdContainer.addView(mMraidWebView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // onPageLoaded gets fired once the html is loaded into the webView mMraidBridge.setContentHtml(htmlData); } // onPageLoaded gets fired once the html is loaded into the webView. private int getDisplayRotation() { WindowManager wm = (WindowManager) mContext .getSystemService(Context.WINDOW_SERVICE); return wm.getDefaultDisplay().getRotation(); } @VisibleForTesting boolean handleConsoleMessage(@NonNull final ConsoleMessage consoleMessage) { //noinspection SimplifiableIfStatement if (mDebugListener != null) { return mDebugListener.onConsoleMessage(consoleMessage); } return true; } @VisibleForTesting boolean handleJsAlert(@NonNull final String message, @NonNull final JsResult result) { if (mDebugListener != null) { return mDebugListener.onJsAlert(message, result); } result.confirm(); return true; } @VisibleForTesting static class ScreenMetricsWaiter { static class WaitRequest { @NonNull private final View[] mViews; @NonNull private final Handler mHandler; @Nullable private Runnable mSuccessRunnable; int mWaitCount; private WaitRequest(@NonNull Handler handler, @NonNull final View[] views) { mHandler = handler; mViews = views; } private void countDown() { mWaitCount--; if (mWaitCount == 0 && mSuccessRunnable != null) { mSuccessRunnable.run(); mSuccessRunnable = null; } } private final Runnable mWaitingRunnable = new Runnable() { @Override public void run() { for (final View view : mViews) { // Immediately count down for any views that already have a size if (view.getHeight() > 0 || view.getWidth() > 0) { countDown(); continue; } // For views that didn't have a size, listen (once) for a preDraw. Note // that this doesn't leak because the ViewTreeObserver gets detached when // the view is no longer part of the view hierarchy. view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { @Override public boolean onPreDraw() { view.getViewTreeObserver().removeOnPreDrawListener(this); countDown(); return true; } }); } } }; void start(@NonNull Runnable successRunnable) { mSuccessRunnable = successRunnable; mWaitCount = mViews.length; mHandler.post(mWaitingRunnable); } void cancel() { mHandler.removeCallbacks(mWaitingRunnable); mSuccessRunnable = null; } } @NonNull private final Handler mHandler = new Handler(); @Nullable private WaitRequest mLastWaitRequest; WaitRequest waitFor(@NonNull View... views) { mLastWaitRequest = new WaitRequest(mHandler, views); return mLastWaitRequest; } void cancelLastRequest() { if (mLastWaitRequest != null) { mLastWaitRequest.cancel(); mLastWaitRequest = null; } } } @Nullable private View getCurrentWebView() { return mTwoPartBridge.isAttached() ? mTwoPartWebView : mMraidWebView; } private boolean isInlineVideoAvailable() { //noinspection SimplifiableIfStatement if (mActivity == null || getCurrentWebView() == null) { return false; } return mMraidNativeCommandHandler.isInlineVideoAvailable(mActivity, getCurrentWebView()); } @VisibleForTesting void handlePageLoad() { setViewState(ViewState.DEFAULT, new Runnable() { @Override public void run() { mMraidBridge.notifySupports( mMraidNativeCommandHandler.isSmsAvailable(mContext), mMraidNativeCommandHandler.isTelAvailable(mContext), MraidNativeCommandHandler.isCalendarAvailable(mContext), MraidNativeCommandHandler.isStorePictureSupported(mContext), isInlineVideoAvailable()); mMraidBridge.notifyPlacementType(mPlacementType); mMraidBridge.notifyViewability(mMraidBridge.isVisible()); mMraidBridge.notifyReady(); } }); // Call onLoaded immediately. This causes the container to get added to the view hierarchy if (mMraidListener != null) { mMraidListener.onLoaded(mDefaultAdContainer); } } @VisibleForTesting void handleTwoPartPageLoad() { updateScreenMetricsAsync(new Runnable() { @Override public void run() { mTwoPartBridge.notifySupports( mMraidNativeCommandHandler.isSmsAvailable(mContext), mMraidNativeCommandHandler.isTelAvailable(mContext), mMraidNativeCommandHandler.isCalendarAvailable(mContext), mMraidNativeCommandHandler.isStorePictureSupported(mContext), isInlineVideoAvailable()); mTwoPartBridge.notifyViewState(mViewState); mTwoPartBridge.notifyPlacementType(mPlacementType); mTwoPartBridge.notifyViewability(mTwoPartBridge.isVisible()); mTwoPartBridge.notifyReady(); } }); } /** * Updates screen metrics, calling the successRunnable once they are available. The * successRunnable will always be called asynchronously, ie on the next main thread loop. */ private void updateScreenMetricsAsync(@Nullable final Runnable successRunnable) { // Don't allow multiple metrics wait requests at once mScreenMetricsWaiter.cancelLastRequest(); // Determine which web view should be used for the current ad position final View currentWebView = getCurrentWebView(); if (currentWebView == null) { return; } // Wait for the next draw pass on the default ad container and current web view mScreenMetricsWaiter.waitFor(mDefaultAdContainer, currentWebView).start( new Runnable() { @Override public void run() { DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); mScreenMetrics.setScreenSize( displayMetrics.widthPixels, displayMetrics.heightPixels); int[] location = new int[2]; View rootView = getRootView(); rootView.getLocationOnScreen(location); mScreenMetrics.setRootViewPosition(location[0], location[1], rootView.getWidth(), rootView.getHeight()); mDefaultAdContainer.getLocationOnScreen(location); mScreenMetrics.setDefaultAdPosition(location[0], location[1], mDefaultAdContainer.getWidth(), mDefaultAdContainer.getHeight()); currentWebView.getLocationOnScreen(location); mScreenMetrics.setCurrentAdPosition(location[0], location[1], currentWebView.getWidth(), currentWebView.getHeight()); // Always notify both bridges of the new metrics mMraidBridge.notifyScreenMetrics(mScreenMetrics); if (mTwoPartBridge.isAttached()) { mTwoPartBridge.notifyScreenMetrics(mScreenMetrics); } if (successRunnable != null) { successRunnable.run(); } } }); } void handleOrientationChange(int currentRotation) { updateScreenMetricsAsync(null); } public void pause() { mIsPaused = true; // This causes an inline video to pause if there is one playing if (mMraidWebView != null) { WebViews.onPause(mMraidWebView); } if (mTwoPartWebView != null) { WebViews.onPause(mTwoPartWebView); } } public void resume() { mIsPaused = false; // This causes an inline video to resume if it was playing previously if (mMraidWebView != null) { WebViews.onResume(mMraidWebView); } if (mTwoPartWebView != null) { WebViews.onResume(mTwoPartWebView); } } public void destroy() { mScreenMetricsWaiter.cancelLastRequest(); try { mOrientationBroadcastReceiver.unregister(); } catch (IllegalArgumentException e) { if (!e.getMessage().contains("Receiver not registered")) { throw e; } // Else ignore this exception. } // Pause the controller to make sure the video gets stopped. if (!mIsPaused) { pause(); } // Remove the closeable ad container from the view hierarchy, if necessary Views.removeFromParent(mCloseableAdContainer); // Calling destroy eliminates a memory leak on Gingerbread devices mMraidBridge.detach(); if (mMraidWebView != null) { mMraidWebView.destroy(); mMraidWebView = null; } mTwoPartBridge.detach(); if (mTwoPartWebView != null) { mTwoPartWebView.destroy(); mTwoPartWebView = null; } } private void setViewState(@NonNull ViewState viewState) { setViewState(viewState, null); } private void setViewState(@NonNull ViewState viewState, @Nullable Runnable successRunnable) { // Make sure this is a valid transition. MoPubLog.d("MRAID state set to " + viewState); mViewState = viewState; mMraidBridge.notifyViewState(viewState); // Changing state notifies the two part view, but only if it's loaded if (mTwoPartBridge.isLoaded()) { mTwoPartBridge.notifyViewState(viewState); } if (mMraidListener != null) { if (viewState == ViewState.EXPANDED) { mMraidListener.onExpand(); } else if (viewState == ViewState.HIDDEN) { mMraidListener.onClose(); } } updateScreenMetricsAsync(successRunnable); } int clampInt(int min, int target, int max) { return Math.max(min, Math.min(target, max)); } @VisibleForTesting void handleResize(final int widthDips, final int heightDips, final int offsetXDips, final int offsetYDips, @NonNull final ClosePosition closePosition, final boolean allowOffscreen) throws MraidCommandException { if (mMraidWebView == null) { throw new MraidCommandException("Unable to resize after the WebView is destroyed"); } // The spec says that there is no effect calling resize from loaded or hidden, but that // calling it from expanded should raise an error. if (mViewState == ViewState.LOADING || mViewState == ViewState.HIDDEN) { return; } else if (mViewState == ViewState.EXPANDED) { throw new MraidCommandException("Not allowed to resize from an already expanded ad"); } if (mPlacementType == PlacementType.INTERSTITIAL) { throw new MraidCommandException("Not allowed to resize from an interstitial ad"); } // Translate coordinates to px and get the resize rect int width = Dips.dipsToIntPixels(widthDips, mContext); int height = Dips.dipsToIntPixels(heightDips, mContext); int offsetX = Dips.dipsToIntPixels(offsetXDips, mContext); int offsetY = Dips.dipsToIntPixels(offsetYDips, mContext); int left = mScreenMetrics.getDefaultAdRect().left + offsetX; int top = mScreenMetrics.getDefaultAdRect().top + offsetY; Rect resizeRect = new Rect(left, top, left + width, top + height); if (!allowOffscreen) { // Require the entire ad to be on-screen. Rect bounds = mScreenMetrics.getRootViewRect(); if (resizeRect.width() > bounds.width() || resizeRect.height() > bounds.height()) { throw new MraidCommandException("resizeProperties specified a size (" + widthDips + ", " + heightDips + ") and offset (" + offsetXDips + ", " + offsetYDips + ") that doesn't allow the ad to" + " appear within the max allowed size (" + mScreenMetrics.getRootViewRectDips().width() + ", " + mScreenMetrics.getRootViewRectDips().height() + ")"); } // Offset the resize rect so that it displays on the screen int newLeft = clampInt(bounds.left, resizeRect.left, bounds.right - resizeRect.width()); int newTop = clampInt(bounds.top, resizeRect.top, bounds.bottom - resizeRect.height()); resizeRect.offsetTo(newLeft, newTop); } // The entire close region must always be visible. Rect closeRect = new Rect(); mCloseableAdContainer.applyCloseRegionBounds(closePosition, resizeRect, closeRect); if (!mScreenMetrics.getRootViewRect().contains(closeRect)) { throw new MraidCommandException("resizeProperties specified a size (" + widthDips + ", " + heightDips + ") and offset (" + offsetXDips + ", " + offsetYDips + ") that doesn't allow the close" + " region to appear within the max allowed size (" + mScreenMetrics.getRootViewRectDips().width() + ", " + mScreenMetrics.getRootViewRectDips().height() + ")"); } if (!resizeRect.contains(closeRect)) { throw new MraidCommandException("resizeProperties specified a size (" + widthDips + ", " + height +") and offset (" + offsetXDips + ", " + offsetYDips + ") that don't allow the close region to appear " + "within the resized ad."); } // Resized ads always rely on the creative's close button (as if useCustomClose were true) mCloseableAdContainer.setCloseVisible(false); mCloseableAdContainer.setClosePosition(closePosition); // Put the ad in the closeable container and resize it LayoutParams layoutParams = new LayoutParams(resizeRect.width(), resizeRect.height()); layoutParams.leftMargin = resizeRect.left - mScreenMetrics.getRootViewRect().left; layoutParams.topMargin = resizeRect.top - mScreenMetrics.getRootViewRect().top; if (mViewState == ViewState.DEFAULT) { mDefaultAdContainer.removeView(mMraidWebView); mDefaultAdContainer.setVisibility(View.INVISIBLE); mCloseableAdContainer.addView(mMraidWebView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); getRootView().addView(mCloseableAdContainer, layoutParams); } else if (mViewState == ViewState.RESIZED) { mCloseableAdContainer.setLayoutParams(layoutParams); } mCloseableAdContainer.setClosePosition(closePosition); setViewState(ViewState.RESIZED); } void handleExpand(@Nullable URI uri, boolean shouldUseCustomClose) throws MraidCommandException { if (mMraidWebView == null) { throw new MraidCommandException("Unable to expand after the WebView is destroyed"); } if (mPlacementType == PlacementType.INTERSTITIAL) { return; } if (mViewState != ViewState.DEFAULT && mViewState != ViewState.RESIZED) { return; } applyOrientation(); // For two part expands, create a new web view boolean isTwoPart = (uri != null); if (isTwoPart) { // Of note: the two part ad will start off with its view state as LOADING, and will // transition to EXPANDED once the page is fully loaded mTwoPartWebView = new MraidWebView(mContext); mTwoPartBridge.attachView(mTwoPartWebView); // onPageLoaded gets fired once the html is loaded into the two part webView mTwoPartBridge.setContentUrl(uri.toString()); } // Make sure the correct webView is in the closeable container and make it full screen LayoutParams layoutParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); if (mViewState == ViewState.DEFAULT) { if (isTwoPart) { mCloseableAdContainer.addView(mTwoPartWebView, layoutParams); } else { mDefaultAdContainer.removeView(mMraidWebView); mDefaultAdContainer.setVisibility(View.INVISIBLE); mCloseableAdContainer.addView(mMraidWebView, layoutParams); } getRootView().addView(mCloseableAdContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else if (mViewState == ViewState.RESIZED) { if (isTwoPart) { // Move the ad back to the original container so that when we close the // resized ad, it will be in the correct place mCloseableAdContainer.removeView(mMraidWebView); mDefaultAdContainer.addView(mMraidWebView, layoutParams); mDefaultAdContainer.setVisibility(View.INVISIBLE); mCloseableAdContainer.addView(mTwoPartWebView, layoutParams); } // If we were resized and not 2 part, nothing to do. } mCloseableAdContainer.setLayoutParams(layoutParams); handleCustomClose(shouldUseCustomClose); // Update to expanded once we have new screen metrics. This won't update the two-part ad, // because it is not yet loaded. setViewState(ViewState.EXPANDED); } @VisibleForTesting void handleClose() { if (mMraidWebView == null) { // Doesn't throw an exception because the ad has been destroyed return; } if (mViewState == ViewState.LOADING || mViewState == ViewState.HIDDEN) { return; } // Unlock the orientation before changing the view hierarchy. if (mViewState == ViewState.EXPANDED || mPlacementType == PlacementType.INTERSTITIAL) { unApplyOrientation(); } if (mViewState == ViewState.RESIZED || mViewState == ViewState.EXPANDED) { if (mTwoPartBridge.isAttached() && mTwoPartWebView != null) { // If we have a two part web view, simply remove it from the closeable container mCloseableAdContainer.removeView(mTwoPartWebView); mTwoPartBridge.detach(); } else { // Move the web view from the closeable container back to the default container mCloseableAdContainer.removeView(mMraidWebView); mDefaultAdContainer.addView(mMraidWebView, new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mDefaultAdContainer.setVisibility(View.VISIBLE); } getRootView().removeView(mCloseableAdContainer); // Set the view state to default setViewState(ViewState.DEFAULT); } else if (mViewState == ViewState.DEFAULT) { mDefaultAdContainer.setVisibility(View.INVISIBLE); setViewState(ViewState.HIDDEN); } } @NonNull @TargetApi(VERSION_CODES.KITKAT) private ViewGroup getRootView() { if (mRootView == null) { // This method should never be called this method before the container is ready, ie before // handlePageLoad. if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { Preconditions.checkState(mDefaultAdContainer.isAttachedToWindow()); } mRootView = (ViewGroup) mDefaultAdContainer.getRootView().findViewById( android.R.id.content); } return mRootView; } @VisibleForTesting void handleShowVideo(@NonNull String videoUrl) { MraidVideoPlayerActivity.startMraid(mContext, videoUrl); } @VisibleForTesting void lockOrientation(final int screenOrientation) throws MraidCommandException { if (mActivity == null || !shouldAllowForceOrientation(mForceOrientation)) { throw new MraidCommandException("Attempted to lock orientation to unsupported value: " + mForceOrientation.name()); } if (mOriginalActivityOrientation == null) { mOriginalActivityOrientation = mActivity.getRequestedOrientation(); } mActivity.setRequestedOrientation(screenOrientation); } @VisibleForTesting void applyOrientation() throws MraidCommandException { if (mForceOrientation == MraidOrientation.NONE) { if (mAllowOrientationChange) { // If screen orientation can be changed, an orientation of NONE means that any // orientation lock should be removed unApplyOrientation(); } else { if (mActivity == null) { throw new MraidCommandException("Unable to set MRAID expand orientation to " + "'none'; expected passed in Activity Context."); } // If screen orientation cannot be changed and we can obtain the current // screen orientation, locking it to the current orientation is a best effort lockOrientation(DeviceUtils.getScreenOrientation(mActivity)); } } else { // Otherwise, we have a valid, non-NONE orientation. Lock the screen based on this value lockOrientation(mForceOrientation.getActivityInfoOrientation()); } } @VisibleForTesting void unApplyOrientation() { if (mActivity != null && mOriginalActivityOrientation != null) { mActivity.setRequestedOrientation(mOriginalActivityOrientation); } mOriginalActivityOrientation = null; } @TargetApi(VERSION_CODES.HONEYCOMB_MR2) @VisibleForTesting boolean shouldAllowForceOrientation(final MraidOrientation newOrientation) { // NONE is the default and always allowed if (newOrientation == MraidOrientation.NONE) { return true; } // If we can't obtain an Activity context, return false if (mActivity == null) { return false; } final ActivityInfo activityInfo; try { activityInfo = mActivity.getPackageManager().getActivityInfo( new ComponentName(mActivity, mActivity.getClass()), 0); } catch (PackageManager.NameNotFoundException e) { return false; } // If an orientation is explicitly declared in the manifest, allow forcing this orientation final int activityOrientation = activityInfo.screenOrientation; if (activityOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { return activityOrientation == newOrientation.getActivityInfoOrientation(); } // Make sure the config changes won't tear down the activity when moving to this orientation // The necessary configChanges must always include "orientation" boolean containsNecessaryConfigChanges = bitMaskContainsFlag(activityInfo.configChanges, CONFIG_ORIENTATION); // And on API 13+, configChanges must also include "screenSize" if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR2) { containsNecessaryConfigChanges = containsNecessaryConfigChanges && bitMaskContainsFlag(activityInfo.configChanges, CONFIG_SCREEN_SIZE); } return containsNecessaryConfigChanges; } @VisibleForTesting void handleCustomClose(boolean useCustomClose) { boolean wasUsingCustomClose = !mCloseableAdContainer.isCloseVisible(); if (useCustomClose == wasUsingCustomClose) { return; } mCloseableAdContainer.setCloseVisible(!useCustomClose); if (mOnCloseButtonListener != null) { mOnCloseButtonListener.useCustomCloseChanged(useCustomClose); } } @NonNull public FrameLayout getAdContainer() { return mDefaultAdContainer; } /** * Loads a javascript URL. Useful for running callbacks, such as javascript:webviewDidClose() */ public void loadJavascript(@NonNull String javascript) { mMraidBridge.injectJavaScript(javascript); } @VisibleForTesting class OrientationBroadcastReceiver extends BroadcastReceiver { @Nullable private Context mContext; // -1 until this gets set at least once private int mLastRotation = -1; public void onReceive(Context context, Intent intent) { if (mContext == null) { return; } if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { int orientation = getDisplayRotation(); if (orientation != mLastRotation) { mLastRotation = orientation; handleOrientationChange(mLastRotation); } } } public void register(@NonNull final Context context) { mContext = context; mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); } public void unregister() { if (mContext != null) { mContext.unregisterReceiver(this); mContext = null; } } } @NonNull public Context getContext() { return mContext; } @VisibleForTesting void handleSetOrientationProperties(final boolean allowOrientationChange, final MraidOrientation forceOrientation) throws MraidCommandException { if (!shouldAllowForceOrientation(forceOrientation)) { throw new MraidCommandException( "Unable to force orientation to " + forceOrientation); } mAllowOrientationChange = allowOrientationChange; mForceOrientation = forceOrientation; if (mViewState == ViewState.EXPANDED || mPlacementType == PlacementType.INTERSTITIAL) { applyOrientation(); } } /** * Attempts to handle mopubnativebrowser links in the device browser, deep-links in the * corresponding application, and all other links in the MoPub in-app browser. */ @VisibleForTesting void handleOpen(@NonNull String url) { MoPubLog.d("Opening url: " + url); if (mMraidListener != null) { mMraidListener.onOpen(); } // MoPubNativeBrowser URLs if (Intents.isNativeBrowserScheme(url)) { try { final Intent intent = Intents.intentForNativeBrowserScheme(url); Intents.startActivity(mContext, intent); } catch (UrlParseException e) { MoPubLog.d("Unable to load mopub native browser url: " + url + ". " + e.getMessage()); } catch (IntentNotResolvableException e) { MoPubLog.d("Unable to load mopub native browser url: " + url + ". " + e.getMessage()); } return; } // Non-http(s) URLs if (!Intents.isHttpUrl(url) && Intents.canHandleApplicationUrl(mContext, url)) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); try { Intents.startActivity(mContext, intent); } catch (IntentNotResolvableException e) { MoPubLog.d("Unable to resolve application url: " + url); } return; } final Bundle extras = new Bundle(); extras.putString(MoPubBrowser.DESTINATION_URL_KEY, url); final Intent intent = Intents.getStartActivityIntent(mContext, MoPubBrowser.class, extras); try { Intents.startActivity(mContext, intent); } catch (IntentNotResolvableException e) { MoPubLog.d("Unable to launch intent for URL: " + url + "."); } } @VisibleForTesting @Deprecated // for testing @NonNull ViewState getViewState() { return mViewState; } @VisibleForTesting @Deprecated // for testing void setViewStateForTesting(@NonNull ViewState viewState) { mViewState = viewState; } @VisibleForTesting @Deprecated // for testing @NonNull CloseableLayout getExpandedAdContainer() { return mCloseableAdContainer; } @VisibleForTesting @Deprecated // for testing void setRootView(FrameLayout rootView) { mRootView = rootView; } @VisibleForTesting @Deprecated // for testing void setRootViewSize(int width, int height) { mScreenMetrics.setRootViewPosition(0, 0, width, height); } @VisibleForTesting @Deprecated // for testing Integer getOriginalActivityOrientation() { return mOriginalActivityOrientation; } @VisibleForTesting @Deprecated // for testing boolean getAllowOrientationChange() { return mAllowOrientationChange; } @VisibleForTesting @Deprecated // for testing MraidOrientation getForceOrientation() { return mForceOrientation; } @VisibleForTesting @Deprecated // for testing void setOrientationBroadcastReceiver(OrientationBroadcastReceiver receiver) { mOrientationBroadcastReceiver = receiver; } @VisibleForTesting @Deprecated // for testing MraidWebView getMraidWebView() { return mMraidWebView; } @VisibleForTesting @Deprecated // for testing MraidWebView getTwoPartWebView() { return mTwoPartWebView; } }