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