com.achep.acdisplay.ui.fragments.AcDisplayFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.achep.acdisplay.ui.fragments.AcDisplayFragment.java

Source

/*
 * 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.support.v4.app.Fragment;
import android.support.v4.media.session.PlaybackStateCompat;
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.Metadata;
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.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 Fragment implements NotificationPresenter.OnNotificationListChangedListener,
        ForwardingLayout.OnForwardedEventListener, View.OnTouchListener, Widget.Callback,
        ConfigBase.OnConfigChangedListener, MediaController2.MediaListener, 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;

    // 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 MediaController2 mMediaController;
    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
        mMediaController = MediaController2.newInstance(getActivity()).asyncWrap();
        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.
        mMediaController.start();
        mMediaController.registerListener(this);
        onPlaybackStateChanged(mMediaController.getPlaybackState());

        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.
        mMediaController.stop();
        mMediaController.unregisterListener(this);
        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) {
        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(null);
        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 mMediaController;
    }

    @Override
    public void onMetadataChanged(@NonNull Metadata metadata) {
        // This event is handled by
        // the media widget.
    }

    @Override
    public void onPlaybackStateChanged(int state) {
        switch (state) {
        case PlaybackStateCompat.STATE_PLAYING:
            mHandler.removeMessages(MSG_HIDE_MEDIA_WIDGET);
            makeMediaWidgetActive();
            break;
        default:
            if (mMediaWidgetActive) {
                int delay = 6000; // 6 sec.
                if (state == PlaybackStateCompat.STATE_NONE)
                    delay = 500;
                mHandler.sendEmptyMessageDelayed(MSG_HIDE_MEDIA_WIDGET, delay);
            }
            break;
        }
    }

    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);
            }
            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);
            }
        }

        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();
    }

    /**
     * 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;
            }
        }

    }

}