/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package com.linkbubble.ui;
import android.animation.Animator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.linkbubble.Config;
import com.linkbubble.Constant;
import com.linkbubble.MainApplication;
import com.linkbubble.MainController;
import com.linkbubble.R;
import com.linkbubble.Settings;
import com.linkbubble.physics.Circle;
import com.linkbubble.util.Util;
import com.linkbubble.webrender.WebRenderer;
import com.squareup.otto.Subscribe;
import java.util.Vector;
public class CanvasView extends FrameLayout {
private static final String TAG = "CanvasView";
private WindowManager.LayoutParams mWindowManagerParams = new WindowManager.LayoutParams();
private Vector<BubbleTargetView> mTargets = new Vector<BubbleTargetView>();
private ImageView mTopMaskView;
private ImageView mBottomMaskView;
boolean mExpanded;
boolean mDragging;
int mTargetAlpha;
int mContentViewTargetAlpha;
private ContentView mContentView;
private Paint mTargetOffsetDebugPaint;
private Paint mTargetTractorDebugPaint;
private Rect mTargetDebugRect;
private Util.ClipResult mClipResult = new Util.ClipResult();
private Util.Point mClosestPoint = new Util.Point();
private Rect mTractorRegion = new Rect();
private ImageView mStatusBarCoverView;
private ExpandedActivity.MinimizeExpandedActivityEvent mMinimizeExpandedActivityEvent = new ExpandedActivity.MinimizeExpandedActivityEvent();
public CanvasView(Context context) {
super(context);
MainApplication.registerForBus(context, this);
int canvasMaskHeight = getResources().getDimensionPixelSize(R.dimen.canvas_mask_height);
if (Constant.COVER_STATUS_BAR) {
int statusBarHeight = Util.getSystemStatusBarHeight(context);
mStatusBarCoverView = new ImageView(context);
mStatusBarCoverView.setImageResource(R.drawable.masked_status_bar);
mStatusBarCoverView.setScaleType(ImageView.ScaleType.FIT_XY);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.gravity = Gravity.TOP | Gravity.LEFT;
lp.x = 0;
lp.y = -statusBarHeight;
lp.height = statusBarHeight;
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
lp.flags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
lp.format = PixelFormat.TRANSPARENT;
mStatusBarCoverView.setLayoutParams(lp);
MainController.addRootWindow(mStatusBarCoverView, lp);
}
Resources resources = getResources();
LayoutInflater inflater = LayoutInflater.from(context);
if (Constant.TOP_CANVAS_MASK) {
mTopMaskView = new ImageView(context);
mTopMaskView.setImageResource(R.drawable.masked_background_half);
mTopMaskView.setScaleType(ImageView.ScaleType.FIT_XY);
FrameLayout.LayoutParams topMaskLP = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, canvasMaskHeight);
topMaskLP.gravity = Gravity.TOP;
mTopMaskView.setLayoutParams(topMaskLP);
addView(mTopMaskView);
}
if (Constant.BOTTOM_CANVAS_MASK) {
mBottomMaskView = new ImageView(context);
mBottomMaskView.setImageResource(R.drawable.masked_background_half);
mBottomMaskView.setScaleType(ImageView.ScaleType.FIT_XY);
mBottomMaskView.setRotation(180);
FrameLayout.LayoutParams bottomMaskLP = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, canvasMaskHeight);
bottomMaskLP.gravity = Gravity.BOTTOM;
mBottomMaskView.setLayoutParams(bottomMaskLP);
addView(mBottomMaskView);
}
int closeBubbleTargetY = getResources().getDimensionPixelSize(R.dimen.close_bubble_target_y);
CloseTabTargetView closeTabTargetView = (CloseTabTargetView) inflater.inflate(R.layout.view_close_tab_target, null);
closeTabTargetView.configure(this, context, null, Constant.BubbleAction.Close,
0, BubbleTargetView.HorizontalAnchor.Center,
closeBubbleTargetY, BubbleTargetView.VerticalAnchor.Bottom,
resources.getDimensionPixelSize(R.dimen.close_bubble_target_x_offset), closeBubbleTargetY,
resources.getDimensionPixelSize(R.dimen.close_bubble_target_tractor_offset_x), closeBubbleTargetY);
mTargets.add(closeTabTargetView);
int consumeTargetY = resources.getDimensionPixelSize(R.dimen.bubble_target_y);
int consumeDefaultX = resources.getDimensionPixelSize(R.dimen.consume_bubble_target_default_x);
int consumeXOffset = resources.getDimensionPixelSize(R.dimen.consume_bubble_target_x_offset);
int consumeTractorBeamX = resources.getDimensionPixelSize(R.dimen.consume_bubble_target_tractor_beam_x);
BubbleTargetView leftConsumeTarget = (BubbleTargetView) inflater.inflate(R.layout.view_consume_bubble_target, null);
Drawable leftConsumeDrawable = Settings.get().getConsumeBubbleIcon(Constant.BubbleAction.ConsumeLeft);
leftConsumeTarget.configure(this, context, leftConsumeDrawable, Constant.BubbleAction.ConsumeLeft,
consumeDefaultX, BubbleTargetView.HorizontalAnchor.Left,
consumeTargetY, BubbleTargetView.VerticalAnchor.Top,
consumeXOffset, consumeTargetY, consumeTractorBeamX, consumeTargetY);
mTargets.add(leftConsumeTarget);
BubbleTargetView rightConsumeTarget = (BubbleTargetView) inflater.inflate(R.layout.view_consume_bubble_target, null);
Drawable rightConsumeDrawable = Settings.get().getConsumeBubbleIcon(Constant.BubbleAction.ConsumeRight);
rightConsumeTarget.configure(this, context, rightConsumeDrawable, Constant.BubbleAction.ConsumeRight,
consumeDefaultX, BubbleTargetView.HorizontalAnchor.Right,
consumeTargetY, BubbleTargetView.VerticalAnchor.Top,
consumeXOffset, consumeTargetY, consumeTractorBeamX, consumeTargetY);
mTargets.add(rightConsumeTarget);
setVisibility(GONE);
mWindowManagerParams.gravity = Gravity.TOP | Gravity.LEFT;
mWindowManagerParams.x = 0;
mWindowManagerParams.y = 0;
mWindowManagerParams.height = WindowManager.LayoutParams.MATCH_PARENT;
mWindowManagerParams.width = WindowManager.LayoutParams.MATCH_PARENT;
mWindowManagerParams.type = WindowManager.LayoutParams.TYPE_PHONE;
mWindowManagerParams.flags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
mWindowManagerParams.format = PixelFormat.TRANSPARENT;
mWindowManagerParams.setTitle("LinkBubble: CanvasView");
MainController.addRootWindow(this, mWindowManagerParams);
if (Constant.DEBUG_SHOW_TARGET_REGIONS) {
mTargetDebugRect = new Rect();
mTargetOffsetDebugPaint = new Paint();
mTargetOffsetDebugPaint.setColor(0x80800000);
mTargetTractorDebugPaint = new Paint();
mTargetTractorDebugPaint.setColor(0x80008000);
}
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (Constant.DEBUG_SHOW_TARGET_REGIONS) {
for (BubbleTargetView bubbleTargetView : mTargets) {
bubbleTargetView.getTractorDebugRegion(mTargetDebugRect);
canvas.drawRect(mTargetDebugRect, mTargetTractorDebugPaint);
bubbleTargetView.getOffsetDebugRegion(mTargetDebugRect);
canvas.drawRect(mTargetDebugRect, mTargetOffsetDebugPaint);
}
}
}
private void applyAlpha(final int targetAlpha) {
mTargetAlpha = targetAlpha;
setVisibility(VISIBLE);
if (mStatusBarCoverView != null) {
mStatusBarCoverView.setVisibility(VISIBLE);
mStatusBarCoverView.animate().alpha(targetAlpha).setDuration(Constant.CANVAS_FADE_ANIM_TIME).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mStatusBarCoverView != null) {
if (targetAlpha == 0) {
mStatusBarCoverView.setVisibility(GONE);
mStatusBarCoverView.setAlpha(0);
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
if (mTopMaskView != null) {
mTopMaskView.setVisibility(VISIBLE);
mTopMaskView.animate().alpha(targetAlpha).setDuration(Constant.CANVAS_FADE_ANIM_TIME).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mTopMaskView != null) {
if (targetAlpha == 0) {
mTopMaskView.setVisibility(GONE);
mTopMaskView.setAlpha(0);
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
if (mBottomMaskView != null) {
//mBottomMaskView.setVisibility(VISIBLE);
///mBottomMaskView.setAlpha(mCurrentAlpha);
mBottomMaskView.animate().alpha(targetAlpha).setDuration(Constant.CANVAS_FADE_ANIM_TIME).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mBottomMaskView != null) {
if (targetAlpha == 0) {
mBottomMaskView.setVisibility(GONE);
mBottomMaskView.setAlpha(0);
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
applyContentViewAlpha(mTargetAlpha);
}
private void applyContentViewAlpha(final int targetAlpha) {
mContentViewTargetAlpha = targetAlpha;
if (mContentView != null) {
mContentView.animate().cancel();
mContentView.setVisibility(VISIBLE);
setVisibility(VISIBLE);
if (mContentViewTargetAlpha != 0 && !mExpanded) {
mContentView.setAlpha(0f);
}
mContentView.animate().alpha(targetAlpha).setDuration(Constant.CANVAS_FADE_ANIM_TIME).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mContentView != null) {
if (mContentViewTargetAlpha == 0) {
mContentView.setAlpha(0f);
mContentView.setVisibility(GONE);
if (!mExpanded) {
removeView(mContentView);
}
} else {
mContentView.setAlpha(1f);
mContentView.setVisibility(VISIBLE);
}
}
// If we also have target alpha 0 then hide the view so clicks behind will work.
if (mTargetAlpha == 0 && !mDragging) {
setVisibility(GONE);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
if (targetAlpha == 0) {
try {
clearFocus();
} catch (Exception e) {
Log.d(TAG, "handled exception while clearing focus");
}
}
}
}
private void setContentView(TabView bubble, boolean unhideNotification) {
if (mContentView != null) {
// The webview can throw an exception when trying to remove focus inside of removeView.
// To prevent a crash we try to manually unfocus first, within a try/catch to reset ViewGroup::mFocused.
// Prevents crash: https://fabric.io/brave6/android/apps/com.linkbubble.playstore/issues/55dccdeee0d514e5d640ab55
try {
mContentView.clearFocus();
} catch(Exception e) {
Log.d(TAG, "handled exception while clearing focus");
}
removeView(mContentView);
if (mExpanded) {
applyContentViewAlpha(1);
}
else {
mContentView.setAlpha(0f);
mContentView.setVisibility(GONE);
}
mContentView.onCurrentContentViewChanged(false);
}
ContentView contentView = bubble != null ? bubble.getContentView() : null;
//if (bubble != null) {
// int bubbleIndex = MainController.get().getTabIndex(bubble);
// Log.d("CanvasView", "setContentView() - index:" + bubbleIndex);
//}
//Log.d("blerg", "setContentView(): from " + (mContentView != null ? "valid" : "none") + " to " + (contentView != null ? "valid" : "none"));
mContentView = contentView;
if (unhideNotification) {
return;
}
if (mContentView != null) {
FrameLayout.LayoutParams p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
p.topMargin = Config.mContentOffset;
addView(mContentView, p);
mContentView.onCurrentContentViewChanged(true);
mContentView.requestFocus();
}
}
private void showContentView() {
applyContentViewAlpha(1);
}
private void hideContentView() {
applyContentViewAlpha(0);
}
@SuppressWarnings("unused")
@Subscribe
public void onCurrentTabResume(MainController.CurrentTabResumeEvent e) {
if (null == e.mTab) {
return;
}
ContentView contentView = e.mTab.getContentView();
if (null == contentView) {
return;
}
WebRenderer webRenderer = contentView.getWebRenderer();
if (null == webRenderer) {
return;
}
webRenderer.resumeOnSetActive();
}
@SuppressWarnings("unused")
@Subscribe
public void onCurrentTabPause(MainController.CurrentTabPauseEvent e) {
if (null == e.mTab) {
return;
}
ContentView contentView = e.mTab.getContentView();
if (null == contentView) {
return;
}
WebRenderer webRenderer = contentView.getWebRenderer();
if (null == webRenderer) {
return;
}
webRenderer.pauseOnSetInactive();
}
@SuppressWarnings("unused")
@Subscribe
public void onCurrentTabChanged(MainController.CurrentTabChangedEvent e) {
setContentView(e.mTab, e.mUnhideNotification);
}
@SuppressWarnings("unused")
@Subscribe
public void onBeginBubbleDrag(MainController.BeginBubbleDragEvent e) {
mDragging = true;
if (mExpanded) {
fadeOut();
} else {
setVisibility(VISIBLE);
fadeIn();
if (mBottomMaskView != null) {
mBottomMaskView.setVisibility(VISIBLE);
}
hideContentView();
MainController.get().showBadge(false);
if (mContentView != null) {
mContentView.onBeginBubbleDrag();
}
}
}
@SuppressWarnings("unused")
@Subscribe
public void onEndBubbleDragEvent(MainController.EndBubbleDragEvent e) {
mDragging = false;
mExpanded = false;
fadeOut();
removeView(mContentView);
setVisibility(GONE);
MainController.get().showBadge(true);
MainApplication.postEvent(getContext(), mMinimizeExpandedActivityEvent);
}
@SuppressWarnings("unused")
@Subscribe
public void onBeginCollapseTransition(MainController.BeginCollapseTransitionEvent e) {
if (!mExpanded) {
return;
}
mExpanded = false;
if (mContentView != null) {
mContentView.onAnimateOffscreen();
fadeOut();
}
// TODO: replace 24 with Android N version once we update an SDK
if (Build.VERSION.SDK_INT < 24 || !e.mFromCloseSystemDialogs) {
MainApplication.postEvent(getContext(), mMinimizeExpandedActivityEvent);
}
}
@SuppressWarnings("unused")
@Subscribe
public void onBeginExpandTransition(MainController.BeginExpandTransitionEvent e) {
mExpanded = true;
fadeIn();
if (mContentView != null) {
if (null == mContentView.getParent()) {
addView(mContentView);
}
mContentView.onAnimateOnScreen();
showContentView();
}
}
@SuppressWarnings("unused")
@Subscribe
public void onEndCollapseTransition(MainController.EndCollapseTransitionEvent e) {
if (mExpanded) {
fadeOut();
}
}
@SuppressWarnings("unused")
@Subscribe
public void onOrientationChanged(MainController.OrientationChangedEvent e) {
for (int i=0 ; i < mTargets.size() ; ++i) {
BubbleTargetView bt = mTargets.get(i);
bt.OnOrientationChanged();
}
if (mContentView != null) {
mContentView.onOrientationChanged();
}
}
@SuppressWarnings("unused")
@Subscribe
public void onBeginAnimateFinalTabAway(MainController.BeginAnimateFinalTabAwayEvent event) {
fadeOut();
hideContentView();
setContentView(event.mTab, false);
MainController.BeginCollapseTransitionEvent collapseTransitionEvent = new MainController.BeginCollapseTransitionEvent();
collapseTransitionEvent.mPeriod = (Constant.BUBBLE_ANIM_TIME / 1000.f) * 0.666667f;
onBeginCollapseTransition(collapseTransitionEvent);
}
@SuppressWarnings("unused")
@Subscribe
public void onHideContentEvent(MainController.HideContentEvent event) {
mExpanded = false;
setContentView(null, false);
}
@SuppressWarnings("unused")
@Subscribe
public void onConsumeBubblesChanged(Settings.OnConsumeBubblesChangedEvent event) {
for (int i = 0; i < mTargets.size(); ++i) {
mTargets.get(i).onConsumeBubblesChanged();
}
}
private void fadeIn() {
applyAlpha(1);
}
private void fadeOut() {
applyAlpha(0);
}
public void destroy() {
for (BubbleTargetView bt : mTargets) {
bt.destroy();
}
MainApplication.unregisterForBus(getContext(), this);
MainController.removeRootWindow(this);
if (mStatusBarCoverView != null) {
MainController.removeRootWindow(mStatusBarCoverView);
}
// Note: sometimes this element leaks. Seems to be result of this: http://goo.gl/Ite5F9
}
public void update(float dt) {
for (int i=0 ; i < mTargets.size() ; ++i) {
mTargets.get(i).update(dt);
}
}
public BubbleTargetView getSnapTarget(float x0, float y0, float x1, float y1, Util.Point p) {
BubbleTargetView closestTargetView = null;
float closestDistance = 9e9f;
for (BubbleTargetView tv : mTargets) {
tv.getTractorDebugRegion(mTractorRegion);
Circle targetCircle = tv.GetDefaultCircle();
if (Util.clipLineSegmentToRectangle(x0, y0, x1, y1, mTractorRegion.left, mTractorRegion.top, mTractorRegion.right, mTractorRegion.bottom, mClipResult)) {
Util.closestPointToLineSegment(mClipResult.x0, mClipResult.y0, mClipResult.x1, mClipResult.y1, targetCircle.mX, targetCircle.mY, mClosestPoint);
float d = Util.distance(x0, y0, mClosestPoint.x, mClosestPoint.y);
if (d < closestDistance) {
p.x = mClosestPoint.x;
p.y = mClosestPoint.y;
closestTargetView = tv;
}
}
}
return closestTargetView;
}
public BubbleTargetView getSnapTarget(Circle bubbleCircle, float radiusScaler) {
for (int i=0 ; i < mTargets.size() ; ++i) {
BubbleTargetView bt = mTargets.get(i);
if (bt.shouldSnap(bubbleCircle, radiusScaler)) {
return bt;
}
}
return null;
}
}