package com.mixpanel.android.mpmetrics; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; import android.content.Intent; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.TypedValue; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; import android.widget.ImageView; import android.widget.TextView; import com.mixpanel.android.R; import com.mixpanel.android.util.MPLog; import com.mixpanel.android.util.ViewUtils; /** * Attached to an Activity when you display a mini in-app notification. * * Users of the library should not reference this class directly. */ @TargetApi(MPConfig.UI_FEATURES_MIN_API) @SuppressLint("ClickableViewAccessibility") public class InAppFragment extends Fragment { public void setDisplayState(final MixpanelAPI mixpanel, final int stateId, final UpdateDisplayState.DisplayState.InAppNotificationState displayState) { // It would be better to pass in displayState to the only constructor, but // Fragments require a default constructor that is called when Activities recreate them. // This means that when the Activity recreates this Fragment (due to rotation, or // the Activity going away and coming back), mDisplayStateId and mDisplayState are not // initialized. Lifecycle methods should be aware of this case, and decline to show. mMixpanel = mixpanel; mDisplayStateId = stateId; mDisplayState = displayState; } // It's safe to use onAttach(Activity) in API 23 as its implementation has not been changed. // Bypass the Lint check for now. @SuppressWarnings("deprecation") @Override public void onAttach(Activity activity) { super.onAttach(activity); mParent = activity; if (null == mDisplayState) { cleanUp(); return; } // We have to manually clear these Runnables in onStop in case they exist, since they // do illegal operations when onSaveInstanceState has been called already. mHandler = new Handler(); mRemover = new Runnable() { public void run() { InAppFragment.this.remove(); } }; mDisplayMini = new Runnable() { @Override public void run() { mInAppView.setVisibility(View.VISIBLE); mInAppView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { return InAppFragment.this.mDetector.onTouchEvent(event); } }); final ImageView notifImage = (ImageView) mInAppView.findViewById(R.id.com_mixpanel_android_notification_image); final float heightPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 65, mParent.getResources().getDisplayMetrics()); final TranslateAnimation translate = new TranslateAnimation(0, 0, heightPx, 0); translate.setInterpolator(new DecelerateInterpolator()); translate.setDuration(200); mInAppView.startAnimation(translate); final ScaleAnimation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, heightPx / 2, heightPx / 2); scale.setInterpolator(new SineBounceInterpolator()); scale.setDuration(400); scale.setStartOffset(200); notifImage.startAnimation(scale); } }; mDetector = new GestureDetector(activity, new GestureDetector.OnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (velocityY > 0) { remove(); } return true; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent event) { final MiniInAppNotification inApp = (MiniInAppNotification) mDisplayState.getInAppNotification(); final String uriString = inApp.getCtaUrl(); if (uriString != null && uriString.length() > 0) { Uri uri; try { uri = Uri.parse(uriString); } catch (IllegalArgumentException e) { MPLog.i(LOGTAG, "Can't parse notification URI, will not take any action", e); return true; } try { Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri); mParent.startActivity(viewIntent); mMixpanel.getPeople().trackNotification("$campaign_open", inApp); } catch (ActivityNotFoundException e) { MPLog.i(LOGTAG, "User doesn't have an activity for notification URI " + uri); } } remove(); return true; } }); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCleanedUp = false; } @SuppressWarnings("deprecation") @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); if (null == mDisplayState) { cleanUp(); } else { mInAppView = inflater.inflate(R.layout.com_mixpanel_android_activity_notification_mini, container, false); final TextView bodyTextView = (TextView) mInAppView.findViewById(R.id.com_mixpanel_android_notification_title); final ImageView notifImage = (ImageView) mInAppView.findViewById(R.id.com_mixpanel_android_notification_image); MiniInAppNotification inApp = (MiniInAppNotification) mDisplayState.getInAppNotification(); bodyTextView.setText(inApp.getBody()); bodyTextView.setTextColor(inApp.getBodyColor()); notifImage.setImageBitmap(inApp.getImage()); mHandler.postDelayed(mRemover, MINI_REMOVE_TIME); GradientDrawable viewBackground = new GradientDrawable(); viewBackground.setColor(inApp.getBackgroundColor()); viewBackground.setCornerRadius(ViewUtils.dpToPx(7, getActivity())); viewBackground.setStroke((int)ViewUtils.dpToPx(2, getActivity()), inApp.getBorderColor()); if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { mInAppView.setBackgroundDrawable(viewBackground); } else { mInAppView.setBackground(viewBackground); } Drawable myIcon = new BitmapDrawable(getResources(), mDisplayState.getInAppNotification().getImage()); myIcon.setColorFilter(inApp.getImageTintColor(), PorterDuff.Mode.SRC_ATOP); notifImage.setImageDrawable(myIcon); } return mInAppView; } @Override public void onStart() { super.onStart(); if (mCleanedUp) { mParent.getFragmentManager().beginTransaction().remove(this).commit(); } } @Override public void onResume() { super.onResume(); // getHighlightColorFromBackground doesn't seem to work on onResume because the view // has not been fully rendered, so try and delay a little bit. This is also a bit better UX // by giving the user some time to process the new Activity before displaying the notification. mHandler.postDelayed(mDisplayMini, 500); } @Override public void onSaveInstanceState(Bundle outState) { cleanUp(); super.onSaveInstanceState(outState); } @Override public void onPause() { super.onPause(); cleanUp(); } private void cleanUp() { if (!mCleanedUp) { mHandler.removeCallbacks(mRemover); mHandler.removeCallbacks(mDisplayMini); UpdateDisplayState.releaseDisplayState(mDisplayStateId); final FragmentManager fragmentManager = mParent.getFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.remove(this).commit(); } mCleanedUp = true; } private void remove() { if (mParent != null && !mCleanedUp) { mHandler.removeCallbacks(mRemover); mHandler.removeCallbacks(mDisplayMini); final FragmentManager fragmentManager = mParent.getFragmentManager(); // setCustomAnimations works on a per transaction level, so the animations set // when this fragment was created do not apply FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.setCustomAnimations(0, R.animator.com_mixpanel_android_slide_down).remove(this).commit(); UpdateDisplayState.releaseDisplayState(mDisplayStateId); mCleanedUp = true; } } private class SineBounceInterpolator implements Interpolator { public SineBounceInterpolator() { } public float getInterpolation(float t) { return (float) -(Math.pow(Math.E, -8*t) * Math.cos(12*t)) + 1; } } private MixpanelAPI mMixpanel; private Activity mParent; private GestureDetector mDetector; private Handler mHandler; private int mDisplayStateId; private UpdateDisplayState.DisplayState.InAppNotificationState mDisplayState; private Runnable mRemover, mDisplayMini; private View mInAppView; private boolean mCleanedUp; private static final String LOGTAG = "MixpanelAPI.InAppFrag"; private static final int MINI_REMOVE_TIME = 10000; }