diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b15a77980..fc445fbc9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,11 @@ +## 5.1.1 + +### New Features ++ MS-3220: Added Countdown Timer to Interstitial Ads + +### Bug Fixes ++ MS-3755: Fixed Assertion Error that occurred while initializing Settings + ## 5.1 ### Mediation partner upgrades/changes diff --git a/sdk/build.gradle b/sdk/build.gradle index 6e48f3fc9..ee58aa996 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -1,5 +1,5 @@ // Project properties -version = "5.1" +version = "5.1.1" group='com.appnexus.opensdk' // Android build diff --git a/sdk/src/com/appnexus/opensdk/AdView.java b/sdk/src/com/appnexus/opensdk/AdView.java index e53f46833..f38ce64e3 100644 --- a/sdk/src/com/appnexus/opensdk/AdView.java +++ b/sdk/src/com/appnexus/opensdk/AdView.java @@ -37,7 +37,6 @@ import android.view.ViewGroup; import android.webkit.WebView; import android.widget.FrameLayout; -import android.widget.ImageButton; import com.appnexus.opensdk.ut.UTConstants; import com.appnexus.opensdk.ut.UTRequestParameters; @@ -358,7 +357,7 @@ protected void setShouldResizeParent(boolean shouldResizeParent) { * MRAID functions and variables */ boolean mraid_is_closing = false; - ImageButton close_button; + CircularProgressBar close_button; @SuppressLint("StaticFieldLeak") static FrameLayout mraidFullscreenContainer; @SuppressLint("StaticFieldLeak") @@ -427,7 +426,8 @@ protected void mraidFullscreenExpand(final MRAIDImplementation caller, boolean u fslayout.addView(caller.owner); if (close_button == null) { - close_button = ViewUtil.createCloseButton(this.getContext(), use_custom_close); + close_button = ViewUtil.createCircularProgressBar(this.getContext()); + ViewUtil.showCloseButton(close_button, use_custom_close); close_button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -461,7 +461,8 @@ void expand(int w, int h, boolean custom_close, MRAIDChangeSize(w, h); // Add a stock close_button button to the top right corner - close_button = ViewUtil.createCloseButton(this.getContext(), custom_close); + close_button = ViewUtil.createCircularProgressBar(this.getContext()); + ViewUtil.showCloseButton(close_button, custom_close); FrameLayout.LayoutParams blp = (LayoutParams) close_button.getLayoutParams(); // place the close button at the top right of the adview if it isn't fullscreen @@ -505,7 +506,7 @@ void resize(int w, int h, int offset_x, int offset_y, MRAIDImplementation.CUSTOM buttonPxSideLength = (int) (50 * scale); } - close_button = new ImageButton(this.getContext()) { + close_button = new CircularProgressBar(this.getContext(), null, android.R.attr.indeterminateOnly) { @SuppressWarnings("deprecation") @SuppressLint({"NewApi", "DrawAllocation"}) @@ -570,8 +571,7 @@ public void run() { } }); - close_button.setImageDrawable(getResources().getDrawable( - android.R.drawable.ic_menu_close_clear_cancel)); + ViewUtil.showCloseButton(close_button, false); } } }; diff --git a/sdk/src/com/appnexus/opensdk/CircularProgressBar.java b/sdk/src/com/appnexus/opensdk/CircularProgressBar.java new file mode 100644 index 000000000..46057a214 --- /dev/null +++ b/sdk/src/com/appnexus/opensdk/CircularProgressBar.java @@ -0,0 +1,139 @@ +package com.appnexus.opensdk; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; +import android.text.Html; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ProgressBar; + +import com.appnexus.opensdk.utils.ViewUtil; + +public class CircularProgressBar extends ProgressBar{ + + private static final double DEFAULT_STROKE_WIDTH = 2.5; + private static final int TITLE_FONT_SIZE = 14; + private static final String CLOSE_X = "×"; + private int GRAY = Color.parseColor("#787878"); + private int WHITE = Color.parseColor("#ffffff"); + private static final int CROSS_X_FONT_SIZE = 24; + + private String title = ""; + + private int strokeWidth = 0; + + private final RectF circleBounds = new RectF(); + private final Paint progressColorPaint = new Paint(); + private final Paint strokeColorPaint = new Paint(); + private final Paint backgroundColorPaint = new Paint(); + private final Paint titlePaint = new Paint(); + + public CircularProgressBar(Context context) { + super(context); + initializeCountdownView(null, 0); + } + + public CircularProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + initializeCountdownView(attrs, 0); + } + + public CircularProgressBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initializeCountdownView(attrs, defStyle); + } + + public void initializeCountdownView(AttributeSet attrs, int style){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + strokeWidth = ViewUtil.getValueInPixel(this.getContext(), DEFAULT_STROKE_WIDTH) + 1; + + progressColorPaint.setColor(WHITE); + strokeColorPaint.setColor(GRAY); + backgroundColorPaint.setColor(WHITE); + titlePaint.setColor(GRAY); + + progressColorPaint.setAntiAlias(true); + progressColorPaint.setStyle(Style.STROKE); + progressColorPaint.setStrokeWidth(strokeWidth); + + strokeColorPaint.setAntiAlias(true); + strokeColorPaint.setStyle(Style.STROKE); + strokeColorPaint.setStrokeWidth(strokeWidth); + + backgroundColorPaint.setAntiAlias(true); + backgroundColorPaint.setStyle(Style.FILL); + backgroundColorPaint.setStrokeWidth(strokeWidth); + + titlePaint.setTextSize(TITLE_FONT_SIZE); + titlePaint.setStyle(Style.FILL); + titlePaint.setAntiAlias(true); + titlePaint.setTypeface(Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)); + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + canvas.drawArc(circleBounds, 0, 360, false, backgroundColorPaint); + canvas.drawArc(circleBounds, 0, 360, false, strokeColorPaint); + float scale = getMax() > 0 ? (float)getProgress()/getMax() * 360: 0; + canvas.drawArc(circleBounds, 270, -scale, false, progressColorPaint); + + if (!TextUtils.isEmpty(title)){ + int x = (int)(getMeasuredWidth()/2 - titlePaint.measureText(title) / 2); + int y = getMeasuredHeight()/2; + + float titleHeight = Math.abs(titlePaint.descent() + titlePaint.ascent()); + y += titleHeight/ 2; + canvas.drawText(title, x, y, titlePaint); + } + super.onDraw(canvas); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + final int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); + final int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); + final int min = Math.min(width, height); + setMeasuredDimension(min + 2 * strokeWidth, min + 2 * strokeWidth); + + circleBounds.set(strokeWidth, strokeWidth, min + strokeWidth, min + strokeWidth); + } + + @Override + public synchronized void setProgress(int progress) { + super.setProgress(progress); + invalidate(); + } + + public synchronized void setTitle(String title){ + if(title.equalsIgnoreCase("X")){ + this.title = Html.fromHtml(CLOSE_X).toString(); + titlePaint.setTextSize(ViewUtil.getValueInPixel(this.getContext(), CROSS_X_FONT_SIZE)); + }else{ + this.title = title; + titlePaint.setTextSize(ViewUtil.getValueInPixel(this.getContext(), TITLE_FONT_SIZE)); + } + invalidate(); + } + + + public String getTitle(){ + return title; + } + + public void setTransparent() { + GRAY = Color.parseColor("#00000000"); + WHITE = Color.parseColor("#00000000"); + initializeCountdownView(null, 0); + invalidate(); + } +} \ No newline at end of file diff --git a/sdk/src/com/appnexus/opensdk/InterstitialAdActivity.java b/sdk/src/com/appnexus/opensdk/InterstitialAdActivity.java index 62d12f489..099e78371 100644 --- a/sdk/src/com/appnexus/opensdk/InterstitialAdActivity.java +++ b/sdk/src/com/appnexus/opensdk/InterstitialAdActivity.java @@ -19,19 +19,16 @@ import android.app.Activity; import android.content.MutableContextWrapper; import android.os.Handler; -import android.os.Message; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.FrameLayout; -import android.widget.ImageButton; +import com.appnexus.opensdk.utils.ANCountdownTimer; import com.appnexus.opensdk.utils.Clog; import com.appnexus.opensdk.utils.Settings; import com.appnexus.opensdk.utils.ViewUtil; -import java.lang.ref.WeakReference; - class InterstitialAdActivity implements AdActivity.AdActivityImplementation { private Activity adActivity; private AdWebView webView; @@ -39,8 +36,10 @@ class InterstitialAdActivity implements AdActivity.AdActivityImplementation { private FrameLayout layout; private long now; private InterstitialAdView adView; - private static final int CLOSE_BUTTON_MESSAGE_ID = 8000; - private ImageButton closeButton; + public static final int COUNTDOWN_INTERVAL = 1; + private CircularProgressBar countdownWidget; + private ANCountdownTimer countdownTimer; + private Handler autoDismissHandler; public InterstitialAdActivity(Activity adActivity) { this.adActivity = adActivity; @@ -57,29 +56,82 @@ public void create() { System.currentTimeMillis()); setIAdView(InterstitialAdView.INTERSTITIALADVIEW_TO_USE); - // Add a close button after a delay. + int dismissAdDelay = adActivity.getIntent().getIntExtra( + InterstitialAdView.INTENT_KEY_AUTODISMISS_DELAY, + -1); + int dismissAdInterval = dismissAdDelay * 1000; + + int closeButtonDelay = adActivity.getIntent().getIntExtra( InterstitialAdView.INTENT_KEY_CLOSE_BUTTON_DELAY, Settings.DEFAULT_INTERSTITIAL_CLOSE_BUTTON_DELAY); + displayCountdownWidget(dismissAdInterval, closeButtonDelay); - new CloseButtonHandler(this).sendEmptyMessageDelayed(CLOSE_BUTTON_MESSAGE_ID, closeButtonDelay); - - int dismissAdDelay = adActivity.getIntent().getIntExtra( - InterstitialAdView.INTENT_KEY_AUTODISMISS_DELAY, - -1); if (adView != null && dismissAdDelay > -1) { - final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { + autoDismissHandler = new Handler(); + autoDismissHandler.postDelayed(new Runnable() { @Override public void run() { dismissInterstitial(); } - }, dismissAdDelay * 1000); + }, dismissAdInterval); } } + private void displayCountdownWidget(int dismissAdInterval, int closeButtonDelay) { + + // If the ad will auto-dismiss before the closeButtonDelay is hit, then show the countdown timer based on dismissAdInterval + if ((dismissAdInterval > 0) && dismissAdInterval <= closeButtonDelay) + closeButtonDelay = dismissAdInterval; + Clog.e("displayCountdownWidget", closeButtonDelay + ""); + + countdownWidget = ViewUtil.createCircularProgressBar(adActivity); + layout.addView(countdownWidget); + countdownWidget.setMax(closeButtonDelay); + countdownWidget.setProgress(closeButtonDelay); + countdownWidget.setVisibility(View.VISIBLE); + countdownWidget.bringToFront(); + + startCountdownTimer(closeButtonDelay); + } + + private void startCountdownTimer(final long closeButtonDelay) { + countdownTimer = new ANCountdownTimer(closeButtonDelay, COUNTDOWN_INTERVAL) { + @Override + public void onTick(long leftTimeInMilliseconds) { + if (countdownWidget != null) { + countdownWidget.setProgress((int) leftTimeInMilliseconds); + int seconds = (int) (leftTimeInMilliseconds / 1000) + 1; + countdownWidget.setTitle(String.valueOf(seconds)); + } + } + + @Override + public void onFinish() { + showCloseButton(); + } + }; + countdownTimer.startTimer(); + } + + private void showCloseButton() { + if (countdownWidget != null) { + if (!webView.isMRAIDUseCustomClose()) { + countdownWidget.setProgress(0); + countdownWidget.setTitle("X"); + } else { + countdownWidget.setTransparent(); + } + countdownWidget.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismissInterstitial(); + } + }); + } + } @Override public void backPressed() { @@ -105,7 +157,6 @@ public void destroy() { @Override public void interacted() { - addCloseButton(); } @@ -159,43 +210,15 @@ private void setIAdView(InterstitialAdView av) { layout.addView(webView); } - // add the close button if it hasn't been added already - private void addCloseButton() { - if ((layout == null) || (closeButton != null)) return; - boolean customClose = false; - if (webView != null) { - customClose = webView.isMRAIDUseCustomClose(); - } - closeButton = ViewUtil.createCloseButton(adActivity, customClose); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismissInterstitial(); - } - }); - layout.addView(closeButton); - } - private void dismissInterstitial() { if (adActivity != null) { if (adView != null && adView.getAdDispatcher() != null) { adView.getAdDispatcher().onAdCollapsed(); } + if (autoDismissHandler != null) { + autoDismissHandler.removeCallbacksAndMessages(null); + } adActivity.finish(); } } - - static class CloseButtonHandler extends Handler { - WeakReference weakReferenceIAA; - - public CloseButtonHandler(InterstitialAdActivity a) { - weakReferenceIAA = new WeakReference(a); - } - - @Override - public void handleMessage(Message msg) { - InterstitialAdActivity iAA = weakReferenceIAA.get(); - if (msg.what == CLOSE_BUTTON_MESSAGE_ID && iAA != null) iAA.addCloseButton(); - } - } } \ No newline at end of file diff --git a/sdk/src/com/appnexus/opensdk/utils/ANCountdownTimer.java b/sdk/src/com/appnexus/opensdk/utils/ANCountdownTimer.java new file mode 100644 index 000000000..52728547e --- /dev/null +++ b/sdk/src/com/appnexus/opensdk/utils/ANCountdownTimer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 APPNEXUS INC + * + * 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 com.appnexus.opensdk.utils; + +import android.os.CountDownTimer; + + +public abstract class ANCountdownTimer { + private long pauseTimeMillis = 0; + private long countdownInterval = 0; + private CountDownTimer countdownTimer; + + public ANCountdownTimer(long millisInFuture, long countdownInterval) { + initiateCountdownTimer(millisInFuture, countdownInterval); + } + + private void initiateCountdownTimer(final long millisInFuture, final long countdownInterval) { + ANCountdownTimer.this.countdownInterval = countdownInterval; + ANCountdownTimer.this.countdownTimer = new CountDownTimer(millisInFuture, countdownInterval) { + @Override + public void onTick(long millisUntilFinished) { + pauseTimeMillis = millisUntilFinished; + ANCountdownTimer.this.onTick(millisUntilFinished); + } + + @Override + public void onFinish() { + ANCountdownTimer.this.onFinish(); + } + }; + + } + + public void startTimer(){ + if(countdownTimer != null) { + countdownTimer.start(); + } + } + + public void pauseTimer(){ + if (countdownTimer != null) { + countdownTimer.cancel(); + } + } + + public void cancelTimer(){ + if (countdownTimer != null) { + countdownTimer.cancel(); + } + pauseTimeMillis = 0; + countdownInterval = 0; + countdownTimer = null; + } + + public void resumeTimer(){ + initiateCountdownTimer(pauseTimeMillis, countdownInterval); + startTimer(); + } + + public abstract void onTick(long millisUntilFinished); + + public abstract void onFinish(); +} \ No newline at end of file diff --git a/sdk/src/com/appnexus/opensdk/utils/Settings.java b/sdk/src/com/appnexus/opensdk/utils/Settings.java index 6a5e0063e..1f64995e2 100644 --- a/sdk/src/com/appnexus/opensdk/utils/Settings.java +++ b/sdk/src/com/appnexus/opensdk/utils/Settings.java @@ -45,11 +45,10 @@ public class Settings { public boolean debug_mode = false; // This should always be false here. public String ua = null; public boolean first_launch; - public final String sdkVersion = "5.1"; + public final String sdkVersion = "5.1.1"; public String mcc; public String mnc; - public final String dev_timezone = TimeZone.getDefault().getDisplayName(true, TimeZone.SHORT); public final String language = Locale.getDefault().getLanguage(); public boolean omEnabled = true; diff --git a/sdk/src/com/appnexus/opensdk/utils/ViewUtil.java b/sdk/src/com/appnexus/opensdk/utils/ViewUtil.java index 778c53826..dfeeabcec 100644 --- a/sdk/src/com/appnexus/opensdk/utils/ViewUtil.java +++ b/sdk/src/com/appnexus/opensdk/utils/ViewUtil.java @@ -26,21 +26,12 @@ import android.widget.FrameLayout; import android.widget.ImageButton; +import com.appnexus.opensdk.CircularProgressBar; + public class ViewUtil { - public static ImageButton createCloseButton(Context context, boolean custom_close) { - final ImageButton close = new ImageButton(context); - if (!custom_close){ - close.setImageDrawable(context.getResources().getDrawable( - android.R.drawable.ic_menu_close_clear_cancel)); - } - FrameLayout.LayoutParams blp = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.RIGHT - | Gravity.TOP); - close.setLayoutParams(blp); - close.setBackgroundColor(Color.TRANSPARENT); - return close; - } + + public static final int CCD_MARGIN = 10; + public static final int CCD_DIMENSIONS = 30; public static void removeChildFromParent(View view) { if ((view != null) && (view.getParent() != null)) { @@ -55,8 +46,8 @@ public static Context getTopContext(View view) { ViewParent parent = view.getParent(); if ((parent == null) || !(parent instanceof View)) { - if(view.getContext() instanceof MutableContextWrapper){ - return ((MutableContextWrapper)view.getContext()).getBaseContext(); + if (view.getContext() instanceof MutableContextWrapper) { + return ((MutableContextWrapper) view.getContext()).getBaseContext(); } return view.getContext(); } @@ -87,7 +78,7 @@ public static int[] getScreenSizeAsPixels(Activity activity) { screenHeight = d.getHeight(); } - return new int[] { screenWidth, screenHeight }; + return new int[]{screenWidth, screenHeight}; } // returns screen size as { width, height } in DP @@ -111,8 +102,47 @@ public static void convertFromDPToPixels(Activity activity, int[] pixels) { } } - public static int getValueInPixel(Context context, int valueInDP) { + public static int getValueInPixel(Context context, double valueInDP) { final float scale = context.getResources().getDisplayMetrics().density; return (int) ((valueInDP * scale) + 0.5f); } + + /** + * Adds the CircularProgressBar to the layout when passed + * Returns instance of created CircularProgressBar + * + * @param context - Context of the View/Activity currently running + */ + public static CircularProgressBar createCircularProgressBar(Context context) { + CircularProgressBar circularProgressBar = new CircularProgressBar(context, null, android.R.attr.indeterminateOnly); + circularProgressBar.setId(android.R.id.closeButton); + + int size = getValueInPixel(context, CCD_DIMENSIONS); + int margin = getValueInPixel(context, CCD_MARGIN); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + size, size, Gravity.END | Gravity.TOP); + params.setMargins(0, margin, margin, 0); + circularProgressBar.setVisibility(View.GONE); + circularProgressBar.setLayoutParams(params); + return circularProgressBar; + } + + /** + * Displays the Close Button when called + * + * @param circularProgressBar the instance of CircularProgressBar that is to be displayed + * @param custom_close boolean value that states if the custom close is enable or disabled + */ + public static void showCloseButton(CircularProgressBar circularProgressBar, boolean custom_close) { + if (circularProgressBar != null) { + circularProgressBar.setVisibility(View.VISIBLE); + if (!custom_close) { + circularProgressBar.setProgress(0); + circularProgressBar.setTitle("X"); + } else { + circularProgressBar.setTransparent(); + } + } + } }