package com.mopub.mobileads;
import java.util.ArrayList;
import com.mopub.mobileads.R;
import com.mopub.mobileads.MraidView.ExpansionStyle;
import com.mopub.mobileads.MraidView.NativeCloseButtonStyle;
import com.mopub.mobileads.MraidView.PlacementType;
import com.mopub.mobileads.MraidView.ViewState;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.StateListDrawable;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.webkit.URLUtil;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
class MraidDisplayController extends MraidAbstractController {
private static final String LOGTAG = "MraidDisplayController";
private static final long VIEWABILITY_TIMER_MILLIS = 3000;
private static final int CLOSE_BUTTON_SIZE_DP = 50;
// The view's current state.
private ViewState mViewState = ViewState.HIDDEN;
// Tracks whether this controller's view responds to expand() calls.
private final ExpansionStyle mExpansionStyle;
// Tracks how this controller's view should display its native close button.
private final NativeCloseButtonStyle mNativeCloseButtonStyle;
// Separate instance of MraidView, for displaying "two-part" creatives via the expand(URL) API.
private MraidView mTwoPartExpansionView;
// A reference to the root view.
private FrameLayout mRootView;
// Tracks whether this controller's view is currently on-screen.
private boolean mIsViewable;
// Task that periodically checks whether this controller's view is on-screen.
private Runnable mCheckViewabilityTask = new Runnable() {
public void run() {
boolean currentViewable = checkViewable();
if (mIsViewable != currentViewable) {
mIsViewable = currentViewable;
getView().fireChangeEventForProperty(
MraidViewableProperty.createWithViewable(mIsViewable));
}
mHandler.postDelayed(this, VIEWABILITY_TIMER_MILLIS);
}
};
// Handler for scheduling viewability checks.
private Handler mHandler = new Handler();
// 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.
private final int mOriginalRequestedOrientation;
private BroadcastReceiver mOrientationBroadcastReceiver = new BroadcastReceiver() {
private int mLastRotation;
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
int orientation = MraidDisplayController.this.getDisplayRotation();
if (orientation != mLastRotation) {
mLastRotation = orientation;
MraidDisplayController.this.onOrientationChanged(mLastRotation);
}
}
}
};
// Native close button, used for expanded content.
private ImageView mCloseButton;
// Tracks whether expanded content provides its own, non-native close button.
private boolean mAdWantsCustomCloseButton;
// The scale factor for a dip (relative to a 160 dpi screen).
protected float mDensity;
// The width of the screen in pixels.
protected int mScreenWidth = -1;
// The height of the screen in pixels.
protected int mScreenHeight = -1;
// The view's position within its parent.
private int mViewIndexInParent;
// A view that replaces the MraidView within its parent view when the MraidView is expanded
// (i.e. moved to the top of the view hierarchy).
FrameLayout mPlaceholderView;
MraidDisplayController(MraidView view, MraidView.ExpansionStyle expStyle,
MraidView.NativeCloseButtonStyle buttonStyle) {
super(view);
mExpansionStyle = expStyle;
mNativeCloseButtonStyle = buttonStyle;
Context context = getView().getContext();
mOriginalRequestedOrientation = (context instanceof Activity) ?
((Activity) context).getRequestedOrientation() :
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
initialize();
}
private void initialize() {
mViewState = ViewState.LOADING;
initializeScreenMetrics();
initializeViewabilityTimer();
getView().getContext().registerReceiver(mOrientationBroadcastReceiver,
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
}
private void initializeScreenMetrics() {
Context context = getView().getContext();
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
mDensity = metrics.density;
int statusBarHeight = 0, titleBarHeight = 0;
if (context instanceof Activity) {
Activity activity = (Activity) context;
Window window = activity.getWindow();
Rect rect = new Rect();
window.getDecorView().getWindowVisibleDisplayFrame(rect);
statusBarHeight = rect.top;
int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
titleBarHeight = contentViewTop - statusBarHeight;
}
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels - statusBarHeight - titleBarHeight;
mScreenWidth = (int) (widthPixels * (160.0 / metrics.densityDpi));
mScreenHeight = (int) (heightPixels * (160.0 / metrics.densityDpi));
}
private void initializeViewabilityTimer() {
mHandler.removeCallbacks(mCheckViewabilityTask);
mHandler.post(mCheckViewabilityTask);
}
private int getDisplayRotation() {
WindowManager wm = (WindowManager) getView().getContext()
.getSystemService(Context.WINDOW_SERVICE);
return wm.getDefaultDisplay().getOrientation();
}
private void onOrientationChanged(int currentRotation) {
initializeScreenMetrics();
getView().fireChangeEventForProperty(
MraidScreenSizeProperty.createWithSize(mScreenWidth, mScreenHeight));
}
public void destroy() {
mHandler.removeCallbacks(mCheckViewabilityTask);
try {
getView().getContext().unregisterReceiver(mOrientationBroadcastReceiver);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Receiver not registered")) {
// Ignore this exception.
} else throw e;
}
}
protected void initializeJavaScriptState() {
ArrayList<MraidProperty> properties = new ArrayList<MraidProperty>();
properties.add(MraidScreenSizeProperty.createWithSize(mScreenWidth, mScreenHeight));
properties.add(MraidViewableProperty.createWithViewable(mIsViewable));
getView().fireChangeEventForProperties(properties);
mViewState = ViewState.DEFAULT;
getView().fireChangeEventForProperty(MraidStateProperty.createWithViewState(mViewState));
}
protected boolean isExpanded() {
return (mViewState == ViewState.EXPANDED);
}
protected void close() {
if (mViewState == ViewState.EXPANDED) {
resetViewToDefaultState();
setOrientationLockEnabled(false);
mViewState = ViewState.DEFAULT;
getView().fireChangeEventForProperty(MraidStateProperty.createWithViewState(mViewState));
} else if (mViewState == ViewState.DEFAULT) {
getView().setVisibility(View.INVISIBLE);
mViewState = ViewState.HIDDEN;
getView().fireChangeEventForProperty(MraidStateProperty.createWithViewState(mViewState));
}
if (getView().getOnCloseListener() != null) {
getView().getOnCloseListener().onClose(getView(), mViewState);
}
}
private void resetViewToDefaultState() {
FrameLayout adContainerLayout =
(FrameLayout) mRootView.findViewById(R.id.ad_container_layout_id);
RelativeLayout expansionLayout = (RelativeLayout) mRootView.findViewById(
R.id.modal_container_layout_id);
setNativeCloseButtonEnabled(false);
adContainerLayout.removeAllViewsInLayout();
mRootView.removeView(expansionLayout);
getView().requestLayout();
ViewGroup parent = (ViewGroup) mPlaceholderView.getParent();
parent.addView(getView(), mViewIndexInParent);
parent.removeView(mPlaceholderView);
parent.invalidate();
}
protected void expand(String url, int width, int height, boolean shouldUseCustomClose,
boolean shouldLockOrientation) {
if (mExpansionStyle == MraidView.ExpansionStyle.DISABLED) return;
if (url != null && !URLUtil.isValidUrl(url)) {
getView().fireErrorEvent("expand", "URL passed to expand() was invalid.");
return;
}
// Obtain the root content view, since that's where we're going to insert the expanded
// content. We must do this before swapping the MraidView with its place-holder;
// otherwise, getRootView() will return the wrong view (or null).
mRootView = (FrameLayout) getView().getRootView().findViewById(android.R.id.content);
useCustomClose(shouldUseCustomClose);
setOrientationLockEnabled(shouldLockOrientation);
swapViewWithPlaceholderView();
View expansionContentView = getView();
if (url != null) {
mTwoPartExpansionView = new MraidView(getView().getContext(), ExpansionStyle.DISABLED,
NativeCloseButtonStyle.AD_CONTROLLED, PlacementType.INLINE);
mTwoPartExpansionView.setOnCloseListener(new MraidView.OnCloseListener() {
public void onClose(MraidView view, ViewState newViewState) {
close();
}
});
mTwoPartExpansionView.loadUrl(url);
expansionContentView = mTwoPartExpansionView;
}
ViewGroup expansionViewContainer = createExpansionViewContainer(expansionContentView,
(int) (width * mDensity), (int) (height * mDensity));
mRootView.addView(expansionViewContainer, new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT));
if (mNativeCloseButtonStyle == MraidView.NativeCloseButtonStyle.ALWAYS_VISIBLE ||
(!mAdWantsCustomCloseButton &&
mNativeCloseButtonStyle != MraidView.NativeCloseButtonStyle.ALWAYS_HIDDEN)) {
setNativeCloseButtonEnabled(true);
}
mViewState = ViewState.EXPANDED;
getView().fireChangeEventForProperty(MraidStateProperty.createWithViewState(mViewState));
if (getView().getOnExpandListener() != null) getView().getOnExpandListener().onExpand(getView());
}
private void swapViewWithPlaceholderView() {
ViewGroup parent = (ViewGroup) getView().getParent();
if (parent == null) return;
mPlaceholderView = new FrameLayout(getView().getContext());
int index;
int count = parent.getChildCount();
for (index = 0; index < count; index++) {
if (parent.getChildAt(index) == getView()) break;
}
mViewIndexInParent = index;
parent.addView(mPlaceholderView, index,
new ViewGroup.LayoutParams(getView().getWidth(), getView().getHeight()));
parent.removeView(getView());
}
private ViewGroup createExpansionViewContainer(View expansionContentView, int expandWidth,
int expandHeight) {
int closeButtonSize = (int) (CLOSE_BUTTON_SIZE_DP * mDensity + 0.5f);
if (expandWidth < closeButtonSize) expandWidth = closeButtonSize;
if (expandHeight < closeButtonSize) expandHeight = closeButtonSize;
RelativeLayout expansionLayout = new RelativeLayout(getView().getContext());
expansionLayout.setId(R.id.modal_container_layout_id);
View dimmingView = new View(getView().getContext());
dimmingView.setBackgroundColor(Color.TRANSPARENT);
dimmingView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
expansionLayout.addView(dimmingView, new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT));
FrameLayout adContainerLayout = new FrameLayout(getView().getContext());
adContainerLayout.setId(R.id.ad_container_layout_id);
adContainerLayout.addView(expansionContentView, new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT));
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(expandWidth, expandHeight);
lp.addRule(RelativeLayout.CENTER_IN_PARENT);
expansionLayout.addView(adContainerLayout, lp);
return expansionLayout;
}
private void setOrientationLockEnabled(boolean enabled) {
Context context = getView().getContext();
Activity activity = null;
try {
activity = (Activity) context;
int requestedOrientation = enabled ?
activity.getResources().getConfiguration().orientation :
mOriginalRequestedOrientation;
activity.setRequestedOrientation(requestedOrientation);
} catch (ClassCastException e) {
Log.d(LOGTAG, "Unable to modify device orientation.");
}
}
protected void setNativeCloseButtonEnabled(boolean enabled) {
if (mRootView == null) return;
FrameLayout adContainerLayout =
(FrameLayout) mRootView.findViewById(R.id.ad_container_layout_id);
if (enabled) {
if (mCloseButton == null) {
StateListDrawable states = new StateListDrawable();
states.addState(new int[] {-android.R.attr.state_pressed},
getView().getResources().getDrawable(R.drawable.close_button_normal));
states.addState(new int[] {android.R.attr.state_pressed},
getView().getResources().getDrawable(R.drawable.close_button_pressed));
mCloseButton = new ImageButton(getView().getContext());
mCloseButton.setImageDrawable(states);
mCloseButton.setBackgroundDrawable(null);
mCloseButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
MraidDisplayController.this.close();
}
});
}
int buttonSize = (int) (CLOSE_BUTTON_SIZE_DP * mDensity + 0.5f);
FrameLayout.LayoutParams buttonLayout = new FrameLayout.LayoutParams(
buttonSize, buttonSize, Gravity.RIGHT);
adContainerLayout.addView(mCloseButton, buttonLayout);
} else {
adContainerLayout.removeView(mCloseButton);
}
MraidView view = getView();
if (view.getOnCloseButtonStateChangeListener() != null) {
view.getOnCloseButtonStateChangeListener().onCloseButtonStateChange(view, enabled);
}
}
protected void useCustomClose(boolean shouldUseCustomCloseButton) {
mAdWantsCustomCloseButton = shouldUseCustomCloseButton;
MraidView view = getView();
boolean enabled = !shouldUseCustomCloseButton;
if (view.getOnCloseButtonStateChangeListener() != null) {
view.getOnCloseButtonStateChangeListener().onCloseButtonStateChange(view, enabled);
}
}
protected boolean checkViewable() {
// TODO: Perform more sophisticated check for viewable.
return true;
}
}