/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.acdisplay.ui.fragments;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Scene;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.util.Property;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateInterpolator;
import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.achep.acdisplay.Config;
import com.achep.acdisplay.R;
import com.achep.acdisplay.Timeout;
import com.achep.acdisplay.compat.SceneCompat;
import com.achep.acdisplay.notifications.NotificationPresenter;
import com.achep.acdisplay.notifications.NotificationUtils;
import com.achep.acdisplay.notifications.OpenNotification;
import com.achep.acdisplay.services.media.MediaController2;
import com.achep.acdisplay.services.media.MediaControlsHelper;
import com.achep.acdisplay.ui.CornerHelper;
import com.achep.acdisplay.ui.DynamicBackground;
import com.achep.acdisplay.ui.activities.AcDisplayActivity;
import com.achep.acdisplay.ui.components.ClockWidget;
import com.achep.acdisplay.ui.components.HostWidget;
import com.achep.acdisplay.ui.components.MediaWidget;
import com.achep.acdisplay.ui.components.NotifyWidget;
import com.achep.acdisplay.ui.components.Widget;
import com.achep.acdisplay.ui.view.ForwardingLayout;
import com.achep.acdisplay.ui.view.ForwardingListener;
import com.achep.acdisplay.ui.widgets.CircleView;
import com.achep.base.Device;
import com.achep.base.async.WeakHandler;
import com.achep.base.content.ConfigBase;
import com.achep.base.tests.Check;
import com.achep.base.ui.activities.ActivityBase;
import com.achep.base.ui.fragments.leakcanary.LeakWatchFragment;
import com.achep.base.ui.widgets.TextView;
import com.achep.base.utils.FloatProperty;
import com.achep.base.utils.MathUtils;
import com.achep.base.utils.ViewUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import static com.achep.base.Build.DEBUG;
/**
* This is main fragment of ActiveDisplay app.
*/
// TODO: Put main scene inside of widget.
public class AcDisplayFragment extends LeakWatchFragment implements
NotificationPresenter.OnNotificationListChangedListener,
ForwardingLayout.OnForwardedEventListener,
View.OnTouchListener,
Widget.Callback,
ConfigBase.OnConfigChangedListener,
CircleView.Callback {
private static final String TAG = "AcDisplayFragment";
private static final int MSG_SHOW_HOME_WIDGET = 0;
private static final int MSG_HIDE_MEDIA_WIDGET = 1;
private static final Property<AcDisplayFragment, Float> TRANSFORM =
new FloatProperty<AcDisplayFragment>("populateStdAnimation") {
private float mValue;
@Override
public void setValue(AcDisplayFragment fragment, float value) {
fragment.populateStdAnimation(mValue = value);
}
@Override
public Float get(AcDisplayFragment fragment) {
return mValue;
}
};
// Views
private CircleView mCircleView;
private TextView mStatusClockTextView;
private ProgressBar mProgressBar;
private ViewGroup mDividerView;
private ForwardingLayout mSceneContainer;
private ForwardingLayout mIconsForwarder;
private GridLayout mIconsContainer;
// Main
private ActivityBase mActivity;
private AcDisplayActivity mActivityAcd;
private final HashMap<View, Widget> mWidgetsMap = new HashMap<>();
private final HashMap<String, SceneCompat> mScenesMap = new HashMap<>();
private SceneCompat mCurrentScene;
private Widget mSelectedWidget;
private View mPressedIconView;
private boolean mHasPinnedWidget;
private ForwardingListener mSceneForwardingListener;
private ForwardingListener mIconsForwardingListener;
private final Handler mTouchHandler = new Handler();
private boolean mTouchSticky;
private int mConfigWidgetPinDuration;
private int mConfigWidgetSelectDelay;
// Quick glance
private int mNotificationHashGlanced;
private long mNotificationHashTime;
// Animations and transitions
private TransitionSet mTransitionJit;
private Transition mTransitionSwitchScene;
private ObjectAnimator mStdAnimator;
// Clock widget
private SceneCompat mSceneMainClock;
private Widget mClockWidget;
// Media widget
private SceneCompat mSceneMainMedia;
private MediaControlsHelper mMediaControlsHelper;
private MediaWidget mMediaWidget;
private boolean mMediaWidgetActive;
// Timeout
private Timeout.Gui mTimeoutGui;
private Timeout mTimeout;
private int mTimeoutNormal;
private int mTimeoutShort;
// Swipe to dismiss
private VelocityTracker mVelocityTracker;
private int mMaxFlingVelocity;
private int mMinFlingVelocity;
// Dynamic background
private DynamicBackground mBackground;
/**
* Handler to control delayed events.
*
* @see #MSG_HIDE_MEDIA_WIDGET
* @see #MSG_SHOW_HOME_WIDGET
*/
private final Handler mHandler = new H(this);
private boolean mPendingIconsSizeChange;
private boolean mPendingNotifyChange;
private boolean mResuming;
private boolean isPinnable() {
return getConfig().isWidgetPinnable();
}
private boolean isReadable() {
return getConfig().isWidgetReadable();
}
/**
* Unlocks the keyguard and runs {@link Runnable runnable} when unlocked.
*
* @param finish {@code true} to finish activity, {@code false} to keep it
* @see com.achep.acdisplay.ui.activities.KeyguardActivity
*/
public void unlock(Runnable runnable, boolean finish) {
if (!isNotDemo()) {
if (runnable != null) runnable.run();
return;
}
mActivityAcd.unlock(runnable, finish);
}
public Config getConfig() {
return Config.getInstance();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = (ActivityBase) activity;
mActivityAcd = isNotDemo() ? (AcDisplayActivity) activity : null;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources res = getResources();
mConfigWidgetPinDuration = res.getInteger(R.integer.config_maxPinTime);
mConfigWidgetSelectDelay = res.getInteger(R.integer.config_iconSelectDelayMillis);
ViewConfiguration vc = ViewConfiguration.get(getActivity());
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
// Clock widget
mClockWidget = getConfig().isCustomWidgetEnabled()
? new HostWidget(this, this)
: new ClockWidget(this, this);
// Media widget
MediaController2 mc = MediaController2.newInstance(getActivity()).asyncWrap();
mMediaControlsHelper = new MediaControlsHelper(mc);
mMediaControlsHelper.registerListener(new MediaControlsHelper.Callback() {
@Override
public void onStateChanged(boolean showing) {
if (showing) {
makeMediaWidgetActive();
} else makeMediaWidgetInactive();
}
});
mMediaWidget = new MediaWidget(this, this);
// Transitions
if (Device.hasKitKatApi()) {
mTransitionJit = new TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(new Fade())
.addTransition(new ChangeBounds());
mTransitionSwitchScene = new TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(new Fade(Fade.OUT).setDuration(200))
.addTransition(new Fade(Fade.IN).setStartDelay(80))
.addTransition(new ChangeBounds().setStartDelay(80));
}
// Timeout
mTimeout = isNotDemo()
? mActivityAcd.getTimeout()
: new Timeout();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "Creating view...");
View root = inflater.inflate(isNotDemo()
? R.layout.acdisplay_fragment_full
: R.layout.acdisplay_fragment, container, false);
assert root != null;
// Initialize secondary views
mStatusClockTextView = (TextView) root.findViewById(R.id.clock_small);
mCircleView = (CircleView) root.findViewById(R.id.circle);
mBackground = DynamicBackground.newInstance(this,
(ImageView) root.findViewById(R.id.background));
// Initialize main views
View c = root.findViewById(R.id.container);
mDividerView = (ViewGroup) c.findViewById(R.id.divider);
mProgressBar = (ProgressBar) mDividerView.findViewById(R.id.progress);
mSceneContainer = (ForwardingLayout) c.findViewById(R.id.scene);
mIconsForwarder = (ForwardingLayout) c.findViewById(R.id.forwarding);
mIconsContainer = (GridLayout) c.findViewById(R.id.grid);
// Initialize home widgets.
mSceneMainClock = new SceneCompat(mSceneContainer, mClockWidget
.createView(inflater, mSceneContainer, null));
mSceneMainMedia = new SceneCompat(mSceneContainer, mMediaWidget
.createView(inflater, mSceneContainer, null));
return root;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (DEBUG) Log.d(TAG, "Creating view (created)...");
mSceneForwardingListener = new ForwardingListener(mIconsForwarder, false, mSceneContainer);
mIconsForwardingListener = new ForwardingListener(mIconsForwarder, true, mIconsForwarder);
mIconsForwarder.setOnForwardedEventListener(this);
mIconsForwarder.setAllViewsForwardable(true, 1 /* the touch depth */);
mIconsForwarder.setOnTouchListener(this);
if (isNotDemo()) {
// Init the timeout
mTimeoutGui = new Timeout.Gui(mProgressBar);
mTimeout.registerListener(mTimeoutGui);
// Init the touch forwarding.
View.OnTouchListener listener = new TouchForwarder(getActivity(), mCircleView, mActivityAcd);
view.setOnTouchListener(listener);
mCircleView.setCallback(this);
mCircleView.setSupervisor(new CircleView.Supervisor() {
@Override
public boolean isAnimationEnabled() {
return isAnimatable();
}
@Override
public boolean isAnimationUnlockEnabled() {
return isAnimationEnabled() && getConfig().isUnlockAnimationEnabled();
}
});
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
showWidget(mClockWidget, false);
}
@Override
public void onStart() {
super.onStart();
NotificationPresenter.getInstance().registerListener(this);
getConfig().registerListener(this);
mPendingNotifyChange = true;
mPendingIconsSizeChange = true;
}
@Override
public void onResume() {
super.onResume();
mResuming = true;
// Start all available widgets.
for (Widget widget : mWidgetsMap.values()) widget.start();
mClockWidget.start();
mMediaWidget.start();
// Update notifications list & config.
if (mPendingNotifyChange) rebuildNotifications();
if (mPendingIconsSizeChange) updateIconsSize();
updateTimeouts();
mPendingNotifyChange = false;
mPendingIconsSizeChange = false;
// Media controller.
mMediaControlsHelper.start();
// Show the notification that is the cause of AcDisplay being shown. This
// allows user to see that damn notification in no time.
if (isNotDemo() && getConfig().isNotifyGlanceEnabled()) {
long now = SystemClock.elapsedRealtime();
int hash = mActivityAcd.getCause();
if (hash != 0 && (hash != mNotificationHashGlanced || now - mNotificationHashTime < 1000)) {
// Find the appropriate notification widget.
for (Widget widget : mWidgetsMap.values()) {
if (widget instanceof NotifyWidget) {
NotifyWidget nw = (NotifyWidget) widget;
OpenNotification n = nw.getNotification();
if (n != null && n.hashCode() == hash) {
mNotificationHashGlanced = hash;
if (!n.isContentSecret(getActivity())) {
// Show the appropriate widget.
if (DEBUG) Log.d(TAG, "Doing the quick glance on " + nw);
showWidget(widget);
onWidgetPin(widget);
} // Otherwise there's nothing helpful to show.
break;
}
}
}
}
// Avoid of an issue when the #onResume() is being called
// twice.
mNotificationHashTime = now;
// Logs
if (mNotificationHashGlanced != hash) {
mNotificationHashGlanced = hash;
Log.w(TAG, "The glance notification was not shown!");
}
}
mResuming = false;
}
@Override
public void onPause() {
// Back to the home widget.
showWidget(mClockWidget, false);
// Clear all ongoing events such as handling media widget,
// handing pinned widget, handing the touch delay, etc...
mMediaWidgetActive = false;
mHandler.removeCallbacksAndMessages(null);
mTouchHandler.removeCallbacksAndMessages(null);
// Stop all widgets.
for (Widget widget : mWidgetsMap.values()) widget.stop();
mClockWidget.stop();
mMediaWidget.stop();
// Media controller.
mMediaControlsHelper.stop();
super.onPause();
}
@Override
public void onStop() {
// Unregister everything.
NotificationPresenter.getInstance().unregisterListener(this);
getConfig().unregisterListener(this);
super.onStop();
}
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "Destroying view...");
if (isNotDemo()) {
mTimeout.unregisterListener(mTimeoutGui);
}
super.onDestroyView();
}
//-- CONFIG ---------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void onConfigChanged(@NonNull ConfigBase config,
@NonNull String key,
@NonNull Object value) {
switch (key) {
case Config.KEY_UI_ICON_SIZE:
updateIconsSize();
break;
case Config.KEY_TIMEOUT_NORMAL:
mTimeoutNormal = (int) value;
break;
case Config.KEY_TIMEOUT_SHORT:
mTimeoutShort = (int) value;
break;
}
}
private void updateTimeouts() {
mTimeoutNormal = getConfig().getTimeoutNormal();
mTimeoutShort = getConfig().getTimeoutShort();
}
/**
* Updates the size of all widget's icons as
* {@link com.achep.acdisplay.Config#getIconSizePx() set} in config.
*/
private void updateIconsSize() {
if (!isResumed()) {
mPendingIconsSizeChange = true;
return;
}
final int sizePx = getConfig().getIconSizePx();
final int childCount = mIconsContainer.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mIconsContainer.getChildAt(i);
ViewUtils.setSize(child, sizePx);
}
}
//-- TIMEOUT --------------------------------------------------------------
@Override
public void requestTimeoutRestart(@NonNull Widget widget) {
Check.getInstance().isTrue(isCurrentWidget(widget));
mTimeout.setTimeoutDelayed(mTimeoutNormal, true);
}
//-- TOUCH HANDLING -------------------------------------------------------
@Override
public void onCircleEvent(float radius, float ratio, int event, final int actionId) {
switch (event) {
case CircleView.ACTION_START:
if (mHasPinnedWidget) {
showHomeWidget();
}
mTimeout.setTimeoutDelayed(mTimeoutShort);
mTimeout.pause();
break;
case CircleView.ACTION_UNLOCK_START:
mActivityAcd.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
break;
case CircleView.ACTION_UNLOCK_CANCEL:
mActivityAcd.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
break;
case CircleView.ACTION_UNLOCK:
mActivityAcd.unlock(new Runnable() {
@Override
public void run() {
Context context = getActivity();
assert context != null;
CornerHelper.perform(context, actionId);
}
});
case CircleView.ACTION_CANCELED:
// Clear the pinned widget on short tap in emulator
// (and probably something in real life too).
if (mHasPinnedWidget) showHomeWidget();
mTimeout.resume();
int delta = (int) (2200 - mTimeout.getRemainingTime());
if (delta > 0) {
mTimeout.delay(delta);
}
break;
}
}
@Override
public void onPressedView(MotionEvent event, int activePointerId, View view) {
mTouchHandler.removeCallbacksAndMessages(null);
mPressedIconView = view;
if (view == null) {
return;
}
final Widget widget = findWidgetByIcon(view);
if (isCurrentWidget(widget)) {
// We need to reset this, cause current widget may be
// pinned.
mHandler.removeMessages(MSG_SHOW_HOME_WIDGET);
return;
} else if (widget == null && mSelectedWidget.isHomeWidget()) {
return;
}
int action = event.getActionMasked();
int delay = action != MotionEvent.ACTION_DOWN ? mConfigWidgetSelectDelay : 0;
mTouchHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (widget == null) {
showHomeWidget();
} else {
showWidget(widget);
}
}
}, delay);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v == mIconsForwarder) {
mSceneForwardingListener.onTouch(v, event);
mIconsForwardingListener.onTouch(v, event);
return true;
}
return false;
}
@Override
public void onForwardedEvent(MotionEvent event, int activePointerId) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Track the velocity of movement, so we
// can do swipe-to-dismiss.
mVelocityTracker = VelocityTracker.obtain();
mTouchSticky = false;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
populateStdMotion(event);
if (action != MotionEvent.ACTION_UP) {
return; // Don't fall down.
}
boolean dismissing = swipeToDismiss();
if (!dismissing) {
if (mTouchSticky) {
// Disable the default timeout mechanism and let
// the selected widget to stay for a while.
onWidgetStick(mSelectedWidget);
} else if (mPressedIconView == null || !isPinnable()) {
showHomeWidget();
} else {
onWidgetPin(mSelectedWidget);
}
}
case MotionEvent.ACTION_CANCEL:
mTouchHandler.removeCallbacksAndMessages(null);
mVelocityTracker.recycle();
mVelocityTracker = null;
mTouchSticky = false;
if (action == MotionEvent.ACTION_CANCEL) {
showHomeWidget();
}
break;
}
}
@Override
public void requestWidgetStick(@NonNull Widget widget) {
Check.getInstance().isTrue(isCurrentWidget(widget));
mTouchSticky = true;
}
//-- SWIPE-TO-DISMISS -----------------------------------------------------
private boolean swipeToDismiss() {
if (!isDismissible(mSelectedWidget)) return false;
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
float velocityY = mVelocityTracker.getYVelocity();
float absVelocityX = Math.abs(velocityX);
float absVelocityY = Math.abs(velocityY);
float deltaY = mSceneContainer.getTranslationY();
float absDeltaY = Math.abs(deltaY);
int height = getSceneView().getHeight();
if (height == 0) {
// Scene view is not measured yet.
return false;
} else if (absDeltaY < height / 2) {
boolean dismiss = false;
if (mMinFlingVelocity <= absVelocityY
&& absVelocityY <= mMaxFlingVelocity
&& absVelocityY > absVelocityX * 2
&& absDeltaY > height / 5) {
// Dismiss only if flinging in the same direction as dragging
dismiss = (velocityY < 0) == (deltaY < 0);
}
if (!dismiss) {
return false;
}
}
// /////////////////////
// ~~ DISMISS ~~
// /////////////////////
if (height > absDeltaY && isAnimatable()) {
int duration;
duration = Math.round(1000f /* ms. */ * (height - absDeltaY) / absVelocityX);
duration = Math.min(duration, 300);
final Widget widget = mSelectedWidget;
float progress = MathUtils.range(deltaY / height, 0f, 1f);
if (mStdAnimator != null) mStdAnimator.cancel();
mStdAnimator = ObjectAnimator.ofFloat(this, TRANSFORM, progress, 1f);
mStdAnimator.setDuration(duration);
mStdAnimator.addListener(new AnimatorListenerAdapter() {
/**
* {@inheritDoc}
*/
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
onWidgetDismiss(widget);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
onWidgetDismiss(widget);
}
});
mStdAnimator.start();
} else {
onWidgetDismiss(mSelectedWidget);
}
return true;
}
private void populateStdMotion(@NonNull MotionEvent srcEvent) {
// Track current movement to be able to handle
// flings correctly.
MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
mVelocityTracker.addMovement(MotionEvent.obtainNoHistory(srcEvent));
dstEvent.recycle();
// No need to handle swipe-to-dismiss if the
// widget is not dismissible.
if (!isDismissible(mSelectedWidget)) {
return;
}
final float y = srcEvent.getY() - mIconsContainer.getHeight();
if (y <= 0) {
if (mSceneContainer.getTranslationY() != 0) {
resetSceneContainerParams();
}
return;
}
// Populate current animation
float height = getSceneView().getHeight();
float progress = MathUtils.range(y / height, 0f, 1f);
populateStdAnimation(progress);
}
private void populateStdAnimation(float progress) {
float height = getSceneView().getHeight();
float y = height * progress;
double degrees = Math.toDegrees(Math.acos((height - y) / height));
mSceneContainer.setAlpha(1f - progress);
mSceneContainer.setTranslationY(y);
mSceneContainer.setRotationX((float) (-degrees / 2f));
}
//-- MANAGING WIDGETS -----------------------------------------------------
/**
* Resets {@link #mSceneContainer scene container}'s params, such
* as: animation, alpha level, translation, rotation etc.
*/
private void resetSceneContainerParams() {
if (mStdAnimator != null) mStdAnimator.cancel();
mSceneContainer.setAlpha(1f);
mSceneContainer.setTranslationY(0);
mSceneContainer.setRotationX(0);
}
/**
* @return {@code true} if current widget equals to given one, {@code false} otherwise.
*/
protected final boolean isCurrentWidget(Widget widget) {
return widget == mSelectedWidget;
}
/**
* @return {@code true} if widget is not {@code null} and
* {@link Widget#isDismissible() dismissible}, {@code false} otherwise.
*/
public final boolean isDismissible(@Nullable Widget widget) {
return widget != null && widget.isDismissible();
}
/**
* @return The view of the {@link #mCurrentScene current scene}.
*/
@NonNull
private View getSceneView() {
return mCurrentScene.getView();
}
@Nullable
private SceneCompat findSceneByWidget(@NonNull Widget widget) {
if (widget == mMediaWidget) {
return mSceneMainMedia;
} else if (widget == mClockWidget) {
return mSceneMainClock;
} else if (widget.getView() != null) {
String className = widget.getClass().getName();
return mScenesMap.get(className);
}
return null;
}
private Widget findWidgetByIcon(@NonNull View view) {
return mWidgetsMap.get(view);
}
//-- DISPLAYING WIDGETS ---------------------------------------------------
public void showHomeWidget() {
showHomeWidget(true);
}
public void showHomeWidget(boolean animate) {
Widget widget = isMediaWidgetHome()
? mMediaWidget
: mClockWidget;
showWidget(widget, animate);
}
/**
* @see #showWidget(com.achep.acdisplay.ui.components.Widget, boolean)
*/
protected void showWidget(@NonNull Widget widget) {
showWidget(widget, true);
}
/**
* @see #showWidget(com.achep.acdisplay.ui.components.Widget)
*/
protected void showWidget(@NonNull Widget widget, boolean animate) {
mHandler.removeMessages(MSG_SHOW_HOME_WIDGET);
mHasPinnedWidget = false;
Log.d(TAG, "showing widget " + widget);
View iconView;
if (mSelectedWidget != null) {
iconView = mSelectedWidget.getIconView();
if (iconView != null) {
iconView.setSelected(false);
}
mSelectedWidget.onViewDetached();
}
mSelectedWidget = widget;
resetSceneContainerParams();
animate &= isAnimatableAuto();
SceneCompat scene = findSceneByWidget(mSelectedWidget);
if (scene == null) scene = mSceneMainClock;
if (mCurrentScene != scene) {
goScene(scene, animate);
} else if (animate) {
final ViewGroup viewGroup = mSelectedWidget.getView();
maybeBeginDelayedTransition(viewGroup, mTransitionJit);
}
mSelectedWidget.onViewAttached();
mBackground.dispatchSetBackground(
mSelectedWidget.getBackground(),
mSelectedWidget.getBackgroundMask());
updateStatusClockVisibility(!mSelectedWidget.hasClock() && getConfig().isFullScreen());
iconView = mSelectedWidget.getIconView();
if (iconView != null) {
iconView.setSelected(true);
iconView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
if (!isResumed()) {
return;
}
// Start timeout on main or media widgets, and
// pause it otherwise.
if (widget.isHomeWidget()) {
mTimeout.resume();
} else {
mTimeout.setTimeoutDelayed(mTimeoutNormal, true);
mTimeout.pause();
}
}
/**
* Updates the visibility of status clock (appears above on top of the screen).
*/
private void updateStatusClockVisibility(boolean visibleNow) {
if (mStatusClockTextView == null) {
return;
}
View view = mStatusClockTextView;
boolean visible = view.getVisibility() == View.VISIBLE;
if (visible == visibleNow) return;
if (isAnimatable()) {
final float[] values;
if (visibleNow) {
values = new float[]{
0.0f, 1.0f,
0.8f, 1.0f,
0.8f, 1.0f,
};
view.setVisibility(View.VISIBLE);
view.animate().setListener(null);
} else {
values = new float[]{
1.0f, 0.0f,
1.0f, 0.8f,
1.0f, 0.8f,
};
view.animate().setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mStatusClockTextView.setVisibility(View.GONE);
}
});
}
view.setAlpha(values[0]);
view.setScaleX(values[2]);
view.setScaleY(values[4]);
view.animate()
.alpha(values[1])
.scaleX(values[3])
.scaleY(values[5]);
} else {
ViewUtils.setVisible(view, visibleNow);
}
}
//-- WIDGETS MANAGEMENT ---------------------------------------------------
protected void onWidgetPin(@NonNull Widget widget) {
mHandler.sendEmptyMessageDelayed(MSG_SHOW_HOME_WIDGET, mConfigWidgetPinDuration);
mHasPinnedWidget = true;
}
protected void onWidgetStick(@NonNull Widget widget) {
// mHandler.sendEmptyMessageDelayed(MSG_SHOW_HOME_WIDGET, mConfigWidgetPinDuration);
mHasPinnedWidget = true;
}
protected void onWidgetReadAloud(@NonNull Widget widget) { /* reading aloud */ }
/**
* Called on widget's dismissal. The code here {@link #internalRemoveWidget(Widget) removes}
* {@code widget} and provides the "Turn screen off on last widget dismissal" feature.
*/
protected void onWidgetDismiss(@NonNull Widget widget) {
internalRemoveWidget(widget);
widget.onDismiss();
updateDividerVisibility(true);
// The "Turn screen off on last widget dismissal" feature.
// Previously this feature was working only for notifications.
if (isNotDemo()
&& getConfig().isScreenOffAfterLastWidget()
&& mWidgetsMap.isEmpty() /* checking if there are any widgets */) {
mActivityAcd.lock();
}
}
//-- SCENES MANAGEMENT ----------------------------------------------------
/**
* Changes current scene to given one.
*
* @see #showWidget(com.achep.acdisplay.ui.components.Widget)
*/
@SuppressLint("NewApi")
protected synchronized final void goScene(@NonNull SceneCompat sceneCompat, boolean animate) {
if (mCurrentScene == sceneCompat) return;
mCurrentScene = sceneCompat;
if (DEBUG) Log.d(TAG, "Going to " + sceneCompat);
if (Device.hasKitKatApi()) animate &= mSceneContainer.isLaidOut();
if (!animate) {
sceneCompat.enter();
return;
}
if (Device.hasKitKatApi()) {
final Scene scene = sceneCompat.getScene();
try {
// This must be a synchronization problem with Android's Scene or TransitionManager,
// but those were declared as final classes, so I have no idea how to fix it.
TransitionManager.go(scene, mTransitionSwitchScene);
} catch (IllegalStateException e) {
Log.w(TAG, "TransitionManager has failed switching scenes!");
ViewGroup viewGroup = (ViewGroup) getSceneView().getParent();
viewGroup.removeView(getSceneView());
try {
// Reset internal scene's tag to make it work again.
int id = Resources.getSystem().getIdentifier("current_scene", "id", "android");
Method method = View.class.getMethod("setTagInternal", int.class, Object.class);
method.setAccessible(true);
method.invoke(viewGroup, id, null);
} catch (NoSuchMethodException
| IllegalAccessException
| InvocationTargetException e2) {
throw new RuntimeException("An attempt to fix the TransitionManager has failed.");
}
TransitionManager.go(scene, mTransitionSwitchScene);
}
} else {
sceneCompat.enter();
if (getActivity() != null) {
// TODO: Better animation for Jelly Bean users.
float density = getResources().getDisplayMetrics().density;
getSceneView().setAlpha(0.6f);
getSceneView().setRotationX(6f);
getSceneView().setTranslationY(6f * density);
getSceneView().animate().alpha(1).rotationX(0).translationY(0);
}
}
}
//-- DYNAMIC BACKGROUND ---------------------------------------------------
/**
* Updates current background. The widget must be actually selected, otherwise it
* will crash.
*/
@Override
public void requestBackgroundUpdate(@NonNull Widget widget) {
Check.getInstance().isTrue(isCurrentWidget(widget));
final int mask = widget.getBackgroundMask();
mBackground.dispatchSetBackground(widget.getBackground(), mask);
}
//-- MEDIA ----------------------------------------------------------------
/**
* Gets the controller which should be receiving media events
* while this fragment is in the foreground. The controller supports
* all platforms starting from Android 4.3 and does nothing on older
* versions.
*
* @return The controller which should receive events.
*/
@NonNull
public MediaController2 getMediaController2() {
return mMediaControlsHelper.getMediaController();
}
private void makeMediaWidgetActive() {
if (mMediaWidgetActive == (mMediaWidgetActive = true)) return;
// Update home widget if the current widget is
// the clock / media widget.
if (mSelectedWidget.isHomeWidget()) showHomeWidget();
}
private void makeMediaWidgetInactive() {
if (mMediaWidgetActive == (mMediaWidgetActive = false)) return;
// Update home widget if the current widget
// is the media widget.
if (isCurrentWidget(mMediaWidget)) showHomeWidget();
}
/**
* Defines if media widget replaces home widget
* or no.
*
* @return {@code true} if media widget replaces the home widget,
* {@code false} otherwise.
*/
private boolean isMediaWidgetHome() {
return mMediaWidgetActive;
}
//-- LOLLIPOP -------------------------------------------------------------
/**
* Returns {@code true} if the device is currently in power save mode.
* When in this mode, applications should reduce their functionality
* in order to conserve battery as much as possible.
*
* @return {@code true} if the device is currently in power save mode, {@code false} otherwise.
* @see com.achep.base.utils.power.PowerSaveDetector
*/
public boolean isPowerSaveMode() {
return mActivity.isPowerSaveMode();
}
/**
* @return {@code true} if this fragment is attached to {@link com.achep.acdisplay.ui.activities.AcDisplayActivity} and
* matches parent layout, {@code false} if this is only preview.
*/
public boolean isNotDemo() {
return getActivity() instanceof AcDisplayActivity;
}
public boolean isAnimatable() {
return !isPowerSaveMode() && isResumed();
}
public boolean isAnimatableAuto() {
return isAnimatable() && !mResuming;
}
//-- NOTIFICATION HANDLING ------------------------------------------------
@Nullable
private NotifyWidget find(@Nullable OpenNotification n) {
if (n == null) return null;
// Find the widget of this or previous notification,
// so we can manage it.
for (Widget item : mWidgetsMap.values()) {
if (item instanceof NotifyWidget) {
// Check if notification has the same key.
NotifyWidget nw = (NotifyWidget) item;
if (nw.hasIdenticalIds(n)) {
return nw;
}
}
}
return null;
}
@Override
public void onNotificationListChanged(@NonNull NotificationPresenter np,
OpenNotification osbn,
int event, boolean isLastEventInSequence) {
if (DEBUG) Log.d(TAG, "Handling notification list changed event: "
+ NotificationPresenter.getEventName(event));
if (!isResumed()) {
mPendingNotifyChange = true;
return;
}
NotifyWidget widgetPrev = null;
if (event == NotificationPresenter.EVENT_REMOVED
|| event == NotificationPresenter.EVENT_CHANGED) {
// Find the widget of this or previous notification,
// so we can manage it.
widgetPrev = find(osbn);
}
switch (event) { // don't update on spam-change.
case NotificationPresenter.EVENT_CHANGED:
if (widgetPrev != null) {
if (DEBUG) Log.d(TAG, "[Event] Updating notification widget...");
if (isCurrentWidget(widgetPrev)) {
final ViewGroup viewGroup = widgetPrev.getView();
maybeBeginDelayedTransition(viewGroup, mTransitionJit);
}
widgetPrev.setNotification(osbn);
break;
}
case NotificationPresenter.EVENT_POSTED:
if (DEBUG) Log.d(TAG, "[Event] Adding new notification widget...");
event = NotificationPresenter.EVENT_POSTED;
// Create new widget and inflate its
// icon view.
NotifyWidget nw = new NotifyWidget(this, this);
nw.start();
LayoutInflater inflater = getActivity().getLayoutInflater();
View iconView = nw.createIconView(inflater, mIconsContainer);
// Check if widget's scene is available.
String name = nw.getClass().getName();
SceneCompat scene = mScenesMap.get(name);
// Setup widget & view.
ViewUtils.setSize(iconView, getConfig().getIconSizePx());
nw.setNotification(osbn);
if (scene != null) {
// Initialize widget with previously created
// scene. This is possible by design.
nw.createView(null, null, scene.getView());
} else {
// Create scene view and put to map of scenes.
ViewGroup sceneView = nw.createView(inflater, mSceneContainer, null);
if (sceneView != null) {
scene = new SceneCompat(mSceneContainer, sceneView);
mScenesMap.put(name, scene);
}
}
mWidgetsMap.put(iconView, nw);
maybeBeginDelayedTransition(mIconsContainer, mTransitionJit);
mIconsContainer.addView(iconView);
break;
case NotificationPresenter.EVENT_REMOVED:
if (widgetPrev != null) {
if (DEBUG) Log.d(TAG, "[Event] Removing notification widget...");
internalRemoveWidget(widgetPrev);
internalCleanPressedIconViewIfRemovedFromContainer();
}
break;
case NotificationPresenter.EVENT_BATH:
if (DEBUG) Log.d(TAG, "[Event] Rebuilding notifications...");
rebuildNotifications();
break;
}
if (event == NotificationPresenter.EVENT_POSTED
|| event == NotificationPresenter.EVENT_REMOVED) {
if (isLastEventInSequence) updateDividerVisibility(true);
// NotificationPresenter#EVENT_BATH causes #rebuildNotifications() to be run,
// which calls #updateDividerVisibility() and begins delayed
// transition by itself.
}
}
private void rebuildNotifications() {
final long now = SystemClock.elapsedRealtime();
ViewGroup container = mIconsContainer;
final int childCount = container.getChildCount();
// Count the number of non-notification fragments
// such as unlock or music controls fragments.
int start = 0;
for (int i = 0; i < childCount; i++) {
View child = container.getChildAt(i);
Widget fragment = findWidgetByIcon(child);
if (fragment instanceof NotifyWidget) {
// Those fragments are placed at the begin of layout
// so no reason to continue searching.
break;
} else {
start++;
}
}
final ArrayList<OpenNotification> list = NotificationPresenter.getInstance().getList();
final int notifyCount = list.size();
final boolean[] notifyUsed = new boolean[notifyCount];
final boolean[] childUsed = new boolean[childCount];
for (int i = start; i < childCount; i++) {
View child = container.getChildAt(i);
NotifyWidget widget = (NotifyWidget) findWidgetByIcon(child);
OpenNotification target = widget.getNotification();
for (int j = 0; j < notifyCount; j++) {
OpenNotification n = list.get(j);
if (NotificationUtils.hasIdenticalIds(target, n)) {
notifyUsed[j] = true;
childUsed[i] = true;
if (target != n) {
widget.setNotification(n);
}
break;
}
}
}
// Re-use free views and remove redundant views.
boolean removeAllAfter = false;
for (int a = start, j = 0, offset = 0; a < childCount; a++) {
if (childUsed[a]) continue;
final int i = a + offset;
View child = container.getChildAt(i);
removing_all_next_views:
{
if (!removeAllAfter) {
for (; j < notifyCount; j++) {
if (notifyUsed[j]) continue;
assert child != null;
notifyUsed[j] = true;
NotifyWidget nw = (NotifyWidget) findWidgetByIcon(child);
nw.setNotification(list.get(j));
break removing_all_next_views;
}
}
removeAllAfter = true;
internalReleaseWidget(child);
// Remove widget's icon.
container.removeViewAt(i);
offset--;
}
}
assert getActivity() != null;
LayoutInflater inflater = getActivity().getLayoutInflater();
final int iconSize = getConfig().getIconSizePx();
for (int i = 0; i < notifyCount; i++) {
if (notifyUsed[i]) continue;
NotifyWidget nw = new NotifyWidget(this, this);
if (isResumed()) nw.start();
View iconView = nw.createIconView(inflater, container);
ViewUtils.setSize(iconView, iconSize);
container.addView(iconView);
nw.setNotification(list.get(i));
mWidgetsMap.put(iconView, nw);
}
// /////////////////////
// ~~ UPDATE HASH MAP ~~
// /////////////////////
HashMap<String, SceneCompat> map = (HashMap<String, SceneCompat>) mScenesMap.clone();
mScenesMap.clear();
for (Widget fragment : mWidgetsMap.values()) {
String type = fragment.getClass().getName();
SceneCompat scene = map.get(type);
if (scene != null) {
fragment.createView(null, null, scene.getView());
} else {
ViewGroup sceneView = fragment.createView(inflater, mSceneContainer, null);
if (sceneView != null) {
scene = new SceneCompat(mSceneContainer, sceneView);
map.put(type, scene);
}
}
if (scene != null) {
mScenesMap.put(type, scene);
}
}
internalCleanPressedIconViewIfRemovedFromContainer();
if (DEBUG) {
long delta = SystemClock.elapsedRealtime() - now;
Log.d(TAG, "Fragment list updated in " + delta + "ms.");
}
// Do not animate divider's visibility change on
// pause/resume, cause it _somehow_ confuses people.
boolean animate = !mResuming;
updateDividerVisibility(animate);
}
/**
* Stops the widget, which icon view has been passed as parameter, and removes it
* from the {@link #mWidgetsMap map}.
*/
private void internalReleaseWidget(@NonNull View iconView) {
if (isResumed()) findWidgetByIcon(iconView).stop();
mWidgetsMap.remove(iconView);
}
/**
* Stops the widget and removes it from the {@link #mWidgetsMap map}.
*/
private void internalReleaseWidget(@NonNull Widget widget) {
if (isResumed()) widget.stop();
mWidgetsMap.remove(widget.getIconView());
}
private void internalRemoveWidget(@NonNull Widget widget) {
internalReleaseWidget(widget);
maybeBeginDelayedTransition(mIconsContainer, mTransitionJit);
mIconsContainer.removeView(widget.getIconView());
// Remove widget's scene if it's not needed anymore.
boolean removeScene = true;
String name = widget.getClass().getName();
for (Widget item : mWidgetsMap.values()) {
if (name.equals(item.getClass().getName())) {
removeScene = false;
break;
}
}
if (removeScene) mScenesMap.remove(name);
if (isCurrentWidget(widget)) showHomeWidget();
}
private void internalCleanPressedIconViewIfRemovedFromContainer() {
if (mPressedIconView == null) {
return;
}
int length = mIconsContainer.getChildCount();
for (int i = 0; i < length; i++) {
View view = mIconsContainer.getChildAt(i);
if (mPressedIconView == view) {
return;
}
}
mPressedIconView = null;
}
/**
* Updates the visibility of divider between
* the scene and icons.
*/
@SuppressLint("NewApi")
private void updateDividerVisibility(boolean animate) {
final View view = mDividerView;
final boolean visible = view.getVisibility() == View.VISIBLE;
final boolean visibleNow = !mWidgetsMap.isEmpty();
if (animate && isAnimatable()) {
int visibleInt = MathUtils.bool(visible);
int visibleNowInt = MathUtils.bool(visibleNow);
float[] values = {1.0f, 0.1f, 1.0f, 0.5f};
ViewUtils.setVisible(view, true);
view.setScaleX(values[1 - visibleInt]);
view.setAlpha(values[3 - visibleInt]);
view.animate()
.scaleX(values[1 - visibleNowInt])
.alpha(values[3 - visibleNowInt])
.setInterpolator(new AccelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
ViewUtils.setVisible(view, visibleNow, View.INVISIBLE);
view.setAlpha(1);
view.setScaleX(1);
}
});
} else {
ViewUtils.setVisible(view, visibleNow, View.INVISIBLE);
}
}
//-- OTHER CLASSES --------------------------------------------------------
@SuppressLint("NewApi")
private void maybeBeginDelayedTransition(@Nullable ViewGroup sceneRoot,
@Nullable Transition transition) {
if (Device.hasKitKatApi()
&& isAnimatableAuto()
&& sceneRoot != null
&& sceneRoot.isLaidOut()) {
TransitionManager.beginDelayedTransition(sceneRoot, transition);
}
}
/**
* Transfers the touch between views, and implements double-tap-to-lock.
*
* @author Artem Chepurnoy
*/
private static class TouchForwarder implements View.OnTouchListener {
private final PocketFragment.OnSleepRequestListener mListener;
private final CircleView mCircleView;
private final GestureDetector mGestureDetector;
/**
* {@code true} if redirecting all touches to the {@link #mCircleView},
* {@code false} otherwise.
*/
private boolean mCircling;
public TouchForwarder(@NonNull Context context,
@NonNull CircleView circleView,
@NonNull PocketFragment.OnSleepRequestListener listener) {
mListener = listener;
mCircleView = circleView;
mGestureDetector = new GestureDetector(context, new GestureListener());
}
@Override
public boolean onTouch(View v, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
mCircling = ViewUtils.pointInView(v, x, y, -20);
default:
if (mCircling) mCircleView.sendTouchEvent(event);
}
return mCircling;
}
/**
* Implements double-tap gesture.
*
* @author Artem Chepurnoy
*/
class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Config config = Config.getInstance();
return config.isDoubleTapToSleepEnabled() && mListener.onSleepRequest();
}
}
}
private static class H extends WeakHandler<AcDisplayFragment> {
public H(@NonNull AcDisplayFragment fragment) {
super(fragment);
}
@Override
protected void onHandleMassage(@NonNull AcDisplayFragment fragment, Message msg) {
switch (msg.what) {
case MSG_HIDE_MEDIA_WIDGET:
fragment.makeMediaWidgetInactive();
break;
case MSG_SHOW_HOME_WIDGET:
fragment.showHomeWidget();
break;
}
}
}
}