diff --git a/.gitignore b/.gitignore index 5c038db904d1d3762e5118f8b5b1acd460340646..68fa747df2e5943ef9073f838257f8b9e493712f 100755 --- a/.gitignore +++ b/.gitignore @@ -37,13 +37,16 @@ build/ # IntelliJ *iml .idea/workspace.xml +.idea/jarRepositories.xml .idea/libraries .idea/caches .idea/navEditor.xml .idea/tasks.xml .idea/modules.xml .idea/assetWizardSettings.xml +.idea/markdown* gradle.xml +projectFilesBackup/ .classpath .project diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0601958f2e8bbfa87f66d5a0b5123251df12f921..8df729f1062328d8d68c89a696208d92ec0da35c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,8 @@ image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest" stages: -- build +- build-domain +- build-data before_script: - export GRADLE_USER_HOME=$(pwd)/.gradle @@ -12,10 +13,12 @@ cache: paths: - .gradle/ -build: - stage: build +build-domain: + stage: build-domain script: - - ./gradlew build - artifacts: - paths: - - app/build/outputs/apk + - ./gradlew :domain:build + +build-data: + stage: build-data + script: + - ./gradlew :data:build diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 2e191f67592c749c461215ebfb568a458e7bb541..1a654515e5f80c640102f19057f14542e8f8ffbf 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,8 +1,5 @@ - - - - @@ -59,7 +53,7 @@ .*:id - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n @@ -70,7 +64,7 @@ .*:name - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n @@ -115,7 +109,7 @@ .* - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n ANDROID_ATTRIBUTE_ORDER diff --git a/.idea/dictionaries/amit.xml b/.idea/dictionaries/amit.xml new file mode 100644 index 0000000000000000000000000000000000000000..e990825d104a77861150b12c8411eae4ac7a0f99 --- /dev/null +++ b/.idea/dictionaries/amit.xml @@ -0,0 +1,8 @@ + + + + hotseat + unbadged + + + \ No newline at end of file diff --git a/.idea/dictionaries/blisslauncher.xml b/.idea/dictionaries/blisslauncher.xml new file mode 100644 index 0000000000000000000000000000000000000000..39d51360c258963ecd59542d27c214b74f207408 --- /dev/null +++ b/.idea/dictionaries/blisslauncher.xml @@ -0,0 +1,13 @@ + + + + amit + badging + flowable + interactor + interactors + kumar + unsuspend + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index babdb775f4ffa9de31fbd718aa7268f8b00d75d4..066a3df987e4a5864aa19293a9d9184ba9a4764c 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -111,7 +111,7 @@ dependencies { // Rx Relay implementation "com.jakewharton.rxrelay2:rxrelay:2.1.1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61" // Blur Library implementation 'com.hoko:hoko-blur:1.3.4' diff --git a/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt b/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt index 2578758dfd24c0536ee15261dba21208794ab95e..5ba6050252dffaad1e0c4270dcac3f96f14772fc 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt +++ b/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt @@ -51,7 +51,6 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: } blurBitmap = if (blurBitmap!!.height > (blurBounds.bottom.toInt() - blurBounds.top.toInt())) { - Bitmap.createBitmap( blurBitmap!!, blurBounds.left.toInt(), blurBounds.top.toInt(), diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java b/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java deleted file mode 100644 index 9ec64ffa76b0fbe2c0f0b8ebd6374facad71ed68..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java +++ /dev/null @@ -1,1595 +0,0 @@ -package foundation.e.blisslauncher.core.customviews; - -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.animation.LayoutTransition; -import android.animation.TimeInterpolator; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewDebug; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.Interpolator; -import android.widget.ScrollView; -import android.widget.Scroller; - -import java.util.ArrayList; - -import foundation.e.blisslauncher.R; -import foundation.e.blisslauncher.core.Utilities; -import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicator; -import foundation.e.blisslauncher.core.touch.OverScroll; - -/** - * An abstraction of the original Workspace which supports browsing through a - * sequential list of "pages" - */ -public abstract class PagedView extends ViewGroup { - private static final String TAG = "PagedView"; - private static final boolean DEBUG = false; - - protected static final int INVALID_PAGE = -1; - protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE; - - public static final int PAGE_SNAP_ANIMATION_DURATION = 750; - public static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; - - // OverScroll constants - private final static int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270; - - private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; - // The page is moved more than halfway, automatically move to the next page on touch up. - private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; - - private static final float MAX_SCROLL_PROGRESS = 1.0f; - - // The following constants need to be scaled based on density. The scaled versions will be - // assigned to the corresponding member variables below. - private static final int FLING_THRESHOLD_VELOCITY = 500; - private static final int MIN_SNAP_VELOCITY = 1500; - private static final int MIN_FLING_VELOCITY = 250; - - public static final int INVALID_RESTORE_PAGE = -1001; - - private boolean mFreeScroll = false; - private boolean mSettleOnPageInFreeScroll = false; - - protected int mFlingThresholdVelocity; - protected int mMinFlingVelocity; - protected int mMinSnapVelocity; - - protected boolean mFirstLayout = true; - - @ViewDebug.ExportedProperty(category = "launcher") - protected int mCurrentPage; - - @ViewDebug.ExportedProperty(category = "launcher") - protected int mNextPage = INVALID_PAGE; - protected int mMaxScrollX; - public Scroller mScroller; - private Interpolator mDefaultInterpolator; - private VelocityTracker mVelocityTracker; - protected int mPageSpacing = 0; - - private float mDownMotionX; - private float mDownMotionY; - private float mLastMotionX; - private float mLastMotionXRemainder; - private float mTotalMotionX; - - protected int[] mPageScrolls; - - protected final static int TOUCH_STATE_REST = 0; - protected final static int TOUCH_STATE_SCROLLING = 1; - protected final static int TOUCH_STATE_PREV_PAGE = 2; - protected final static int TOUCH_STATE_NEXT_PAGE = 3; - - protected int mTouchState = TOUCH_STATE_REST; - - protected int mTouchSlop; - private int mMaximumVelocity; - protected boolean mAllowOverScroll = true; - - protected static final int INVALID_POINTER = -1; - - protected int mActivePointerId = INVALID_POINTER; - - protected boolean mIsPageInTransition = false; - - protected boolean mWasInOverscroll = false; - - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise - // it is equal to the scaled overscroll position. We use a separate value so as to prevent - // the screens from continuing to translate beyond the normal bounds. - protected int mOverScrollX; - - protected int mUnboundedScrollX; - - // Page Indicator - int mPageIndicatorViewId; - protected T mPageIndicator; - - // Convenience/caching - private static final Rect sTmpRect = new Rect(); - - protected final Rect mInsets = new Rect(); - protected boolean mIsRtl; - - // Similar to the platform implementation of isLayoutValid(); - protected boolean mIsLayoutValid; - - private int[] mTmpIntPair = new int[2]; - - public PagedView(Context context) { - this(context, null); - } - - public PagedView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.PagedView, defStyle, 0); - mPageIndicatorViewId = a.getResourceId(R.styleable.PagedView_pageIndicator, -1); - a.recycle(); - - setHapticFeedbackEnabled(false); - mIsRtl = false; - init(); - } - - /** - * Initializes various states for this workspace. - */ - protected void init() { - mScroller = new Scroller(getContext()); - mCurrentPage = 0; - - final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - mTouchSlop = configuration.getScaledPagingTouchSlop(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - - float density = getResources().getDisplayMetrics().density; - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * density); - mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density); - mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density); - - if (Utilities.ATLEAST_OREO) { - setDefaultFocusHighlightEnabled(false); - } - } - - public void initParentViews(View parent) { - if (mPageIndicatorViewId > -1) { - mPageIndicator = parent.findViewById(mPageIndicatorViewId); - mPageIndicator.setMarkersCount(getChildCount()); - } - } - - public T getPageIndicator() { - return mPageIndicator; - } - - /** - * Returns the index of the currently displayed page. When in free scroll mode, this is the page - * that the user was on before entering free scroll mode (e.g. the home screen page they - * long-pressed on to enter the overview). Try using {@link #getPageNearestToCenterOfScreen()} - * to get the page the user is currently scrolling over. - */ - public int getCurrentPage() { - return mCurrentPage; - } - - /** - * Returns the index of page to be shown immediately afterwards. - */ - public int getNextPage() { - return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - } - - public int getPageCount() { - return getChildCount(); - } - - public View getPageAt(int index) { - return getChildAt(index); - } - - protected int indexToPage(int index) { - return index; - } - - /** - * Updates the scroll of the current page immediately to its final scroll position. We use this - * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of - * the previous tab page. - */ - protected void updateCurrentPageScroll() { - // If the current page is invalid, just reset the scroll position to zero - int newX = 0; - if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { - newX = getScrollForPage(mCurrentPage); - } - scrollTo(newX, 0); - mScroller.setFinalX(newX); - forceFinishScroller(true); - } - - public void abortScrollerAnimation(boolean resetNextPage) { - mScroller.abortAnimation(); - // We need to clean up the next page here to avoid computeScrollHelper from - // updating current page on the pass. - if (resetNextPage) { - mNextPage = INVALID_PAGE; - pageEndTransition(); - } - } - - private void forceFinishScroller(boolean resetNextPage) { - mScroller.forceFinished(true); - // We need to clean up the next page here to avoid computeScrollHelper from - // updating current page on the pass. - if (resetNextPage) { - mNextPage = INVALID_PAGE; - pageEndTransition(); - } - } - - private int validateNewPage(int newPage) { - // Ensure that it is clamped by the actual set of children in all cases - return Utilities.boundToRange(newPage, 0, getPageCount() - 1); - } - - /** - * Sets the current page. - */ - public void setCurrentPage(int currentPage) { - if (!mScroller.isFinished()) { - abortScrollerAnimation(true); - } - // don't introduce any checks like mCurrentPage == currentPage here-- if we change the - // the default - if (getChildCount() == 0) { - return; - } - int prevPage = mCurrentPage; - mCurrentPage = validateNewPage(currentPage); - updateCurrentPageScroll(); - notifyPageSwitchListener(prevPage); - invalidate(); - } - - /** - * Should be called whenever the page changes. In the case of a scroll, we wait until the page - * has settled. - */ - protected void notifyPageSwitchListener(int prevPage) { - updatePageIndicator(); - } - - private void updatePageIndicator() { - if (mPageIndicator != null) { - mPageIndicator.setActiveMarker(getNextPage()); - } - } - protected void pageBeginTransition() { - if (!mIsPageInTransition) { - mIsPageInTransition = true; - onPageBeginTransition(); - } - } - - protected void pageEndTransition() { - if (mIsPageInTransition) { - mIsPageInTransition = false; - onPageEndTransition(); - } - } - - protected boolean isPageInTransition() { - return mIsPageInTransition; - } - - /** - * Called when the page starts moving as part of the scroll. Subclasses can override this - * to provide custom behavior during animation. - */ - protected void onPageBeginTransition() { - } - - /** - * Called when the page ends moving as part of the scroll. Subclasses can override this - * to provide custom behavior during animation. - */ - protected void onPageEndTransition() { - mWasInOverscroll = false; - } - - protected int getUnboundedScrollX() { - return mUnboundedScrollX; - } - - @Override - public void scrollBy(int x, int y) { - scrollTo(getUnboundedScrollX() + x, getScrollY() + y); - } - - @Override - public void scrollTo(int x, int y) { - // In free scroll mode, we clamp the scrollX - if (mFreeScroll) { - // If the scroller is trying to move to a location beyond the maximum allowed - // in the free scroll mode, we make sure to end the scroll operation. - if (!mScroller.isFinished() && (x > mMaxScrollX || x < 0)) { - forceFinishScroller(false); - } - - x = Utilities.boundToRange(x, 0, mMaxScrollX); - } - - mUnboundedScrollX = x; - - boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); - boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); - if (isXBeforeFirstPage) { - super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); - if (mAllowOverScroll) { - mWasInOverscroll = true; - if (mIsRtl) { - overScroll(x - mMaxScrollX); - } else { - overScroll(x); - } - } - } else if (isXAfterLastPage) { - super.scrollTo(mIsRtl ? 0 : mMaxScrollX, y); - if (mAllowOverScroll) { - mWasInOverscroll = true; - if (mIsRtl) { - overScroll(x); - } else { - overScroll(x - mMaxScrollX); - } - } - } else { - if (mWasInOverscroll) { - overScroll(0); - mWasInOverscroll = false; - } - mOverScrollX = x; - super.scrollTo(x, y); - } - } - - private void sendScrollAccessibilityEvent() { - } - - // we moved this functionality to a helper function so SmoothPagedView can reuse it - protected boolean computeScrollHelper() { - return computeScrollHelper(true); - } - - protected void announcePageForAccessibility() { - } - - protected boolean computeScrollHelper(boolean shouldInvalidate) { - if (mScroller.computeScrollOffset()) { - // Don't bother scrolling if the page does not need to be moved - if (getUnboundedScrollX() != mScroller.getCurrX() - || getScrollY() != mScroller.getCurrY() - || mOverScrollX != mScroller.getCurrX()) { - scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - } - if (shouldInvalidate) { - invalidate(); - } - return true; - } else if (mNextPage != INVALID_PAGE && shouldInvalidate) { - sendScrollAccessibilityEvent(); - - int prevPage = mCurrentPage; - mCurrentPage = validateNewPage(mNextPage); - mNextPage = INVALID_PAGE; - notifyPageSwitchListener(prevPage); - - // We don't want to trigger a page end moving unless the page has settled - // and the user has stopped scrolling - if (mTouchState == TOUCH_STATE_REST) { - pageEndTransition(); - } - - if (canAnnouncePageDescription()) { - announcePageForAccessibility(); - } - } - return false; - } - - @Override - public void computeScroll() { - computeScrollHelper(); - } - - public int getExpectedHeight() { - return getMeasuredHeight(); - } - - public int getNormalChildHeight() { - return getExpectedHeight() - getPaddingTop() - getPaddingBottom() - - mInsets.top - mInsets.bottom; - } - - public int getExpectedWidth() { - return getMeasuredWidth(); - } - - public int getNormalChildWidth() { - return getExpectedWidth() - getPaddingLeft() - getPaddingRight() - - mInsets.left - mInsets.right; - } - - @Override - public void requestLayout() { - mIsLayoutValid = false; - super.requestLayout(); - } - - @Override - public void forceLayout() { - mIsLayoutValid = false; - super.forceLayout(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (getChildCount() == 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // We measure the dimensions of the PagedView to be larger than the pages so that when we - // zoom out (and scale down), the view is still contained in the parent - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // Return early if we aren't given a proper dimension - if (widthSize <= 0 || heightSize <= 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // The children are given the same width and height as the workspace - // unless they were set to WRAP_CONTENT - if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); - - int myWidthSpec = MeasureSpec.makeMeasureSpec( - widthSize - mInsets.left - mInsets.right, MeasureSpec.EXACTLY); - int myHeightSpec = MeasureSpec.makeMeasureSpec( - heightSize - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY); - - // measureChildren takes accounts for content padding, we only need to care about extra - // space due to insets. - measureChildren(myWidthSpec, myHeightSpec); - setMeasuredDimension(widthSize, heightSize); - } - - @SuppressLint("DrawAllocation") - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - mIsLayoutValid = true; - final int childCount = getChildCount(); - boolean pageScrollChanged = false; - if (mPageScrolls == null || childCount != mPageScrolls.length) { - mPageScrolls = new int[childCount]; - pageScrollChanged = true; - } - - if (childCount == 0) { - return; - } - - if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); - - if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) { - pageScrollChanged = true; - } - - final LayoutTransition transition = getLayoutTransition(); - // If the transition is running defer updating max scroll, as some empty pages could - // still be present, and a max scroll change could cause sudden jumps in scroll. - if (transition != null && transition.isRunning()) { - transition.addTransitionListener(new LayoutTransition.TransitionListener() { - - @Override - public void startTransition(LayoutTransition transition, ViewGroup container, - View view, int transitionType) { } - - @Override - public void endTransition(LayoutTransition transition, ViewGroup container, - View view, int transitionType) { - // Wait until all transitions are complete. - if (!transition.isRunning()) { - transition.removeTransitionListener(this); - updateMaxScrollX(); - } - } - }); - } else { - updateMaxScrollX(); - } - - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { - updateCurrentPageScroll(); - mFirstLayout = false; - } - - if (mScroller.isFinished() && pageScrollChanged) { - setCurrentPage(getNextPage()); - } - } - - /** - * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length - * of {@code outPageScrolls} should be same as the the childCount - * - */ - protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, - ComputePageScrollsLogic scrollLogic) { - final int childCount = getChildCount(); - - final int startIndex = mIsRtl ? childCount - 1 : 0; - final int endIndex = mIsRtl ? -1 : childCount; - final int delta = mIsRtl ? -1 : 1; - - final int verticalCenter = (getPaddingTop() + getMeasuredHeight() + mInsets.top - - mInsets.bottom - getPaddingBottom()) / 2; - - final int scrollOffsetLeft = mInsets.left + getPaddingLeft(); - final int scrollOffsetRight = getWidth() - getPaddingRight() - mInsets.right; - boolean pageScrollChanged = false; - - for (int i = startIndex, childLeft = scrollOffsetLeft; i != endIndex; i += delta) { - final View child = getPageAt(i); - if (scrollLogic.shouldIncludeView(child)) { - final int childWidth = child.getMeasuredWidth(); - final int childRight = childLeft + childWidth; - - if (layoutChildren) { - final int childHeight = child.getMeasuredHeight(); - final int childTop = verticalCenter - childHeight / 2; - child.layout(childLeft, childTop, childRight, childTop + childHeight); - } - - // In case the pages are of different width, align the page to left or right edge - // based on the orientation. - final int pageScroll = mIsRtl - ? (childLeft - scrollOffsetLeft) - : Math.max(0, childRight - scrollOffsetRight); - if (outPageScrolls[i] != pageScroll) { - pageScrollChanged = true; - outPageScrolls[i] = pageScroll; - } - - childLeft += childWidth + mPageSpacing + getChildGap(); - } - } - return pageScrollChanged; - } - - protected int getChildGap() { - return 0; - } - - private void updateMaxScrollX() { - mMaxScrollX = computeMaxScrollX(); - } - - protected int computeMaxScrollX() { - int childCount = getChildCount(); - if (childCount > 0) { - final int index = mIsRtl ? 0 : childCount - 1; - return getScrollForPage(index); - } else { - return 0; - } - } - - public void setPageSpacing(int pageSpacing) { - mPageSpacing = pageSpacing; - requestLayout(); - } - - private void dispatchPageCountChanged() { - if (mPageIndicator != null) { - mPageIndicator.setMarkersCount(getChildCount()); - } - // This ensures that when children are added, they get the correct transforms / alphas - // in accordance with any scroll effects. - invalidate(); - } - - @Override - public void onViewAdded(View child) { - super.onViewAdded(child); - dispatchPageCountChanged(); - } - - @Override - public void onViewRemoved(View child) { - super.onViewRemoved(child); - mCurrentPage = validateNewPage(mCurrentPage); - dispatchPageCountChanged(); - } - - protected int getChildOffset(int index) { - if (index < 0 || index > getChildCount() - 1) return 0; - return getPageAt(index).getLeft(); - } - - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { - int page = indexToPage(indexOfChild(child)); - if (page != mCurrentPage || !mScroller.isFinished()) { - if (immediate) { - setCurrentPage(page); - } else { - snapToPage(page); - } - return true; - } - return false; - } - - @Override - protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - int focusablePage; - if (mNextPage != INVALID_PAGE) { - focusablePage = mNextPage; - } else { - focusablePage = mCurrentPage; - } - View v = getPageAt(focusablePage); - if (v != null) { - return v.requestFocus(direction, previouslyFocusedRect); - } - return false; - } - - @Override - public boolean dispatchUnhandledMove(View focused, int direction) { - if (super.dispatchUnhandledMove(focused, direction)) { - return true; - } - - if (mIsRtl) { - if (direction == View.FOCUS_LEFT) { - direction = View.FOCUS_RIGHT; - } else if (direction == View.FOCUS_RIGHT) { - direction = View.FOCUS_LEFT; - } - } - if (direction == View.FOCUS_LEFT) { - if (getCurrentPage() > 0) { - snapToPage(getCurrentPage() - 1); - getChildAt(getCurrentPage() - 1).requestFocus(direction); - return true; - } - } else if (direction == View.FOCUS_RIGHT) { - if (getCurrentPage() < getPageCount() - 1) { - snapToPage(getCurrentPage() + 1); - getChildAt(getCurrentPage() + 1).requestFocus(direction); - return true; - } - } - return false; - } - - @Override - public void addFocusables(ArrayList views, int direction, int focusableMode) { - if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { - return; - } - - // XXX-RTL: This will be fixed in a future CL - if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { - getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); - } - if (direction == View.FOCUS_LEFT) { - if (mCurrentPage > 0) { - getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); - } - } else if (direction == View.FOCUS_RIGHT){ - if (mCurrentPage < getPageCount() - 1) { - getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); - } - } - } - - /** - * If one of our descendant views decides that it could be focused now, only - * pass that along if it's on the current page. - * - * This happens when live folders requery, and if they're off page, they - * end up calling requestFocus, which pulls it on page. - */ - @Override - public void focusableViewAvailable(View focused) { - View current = getPageAt(mCurrentPage); - View v = focused; - while (true) { - if (v == current) { - super.focusableViewAvailable(focused); - return; - } - if (v == this) { - return; - } - ViewParent parent = v.getParent(); - if (parent instanceof View) { - v = (View)v.getParent(); - } else { - return; - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - if (disallowIntercept) { - // We need to make sure to cancel our long press if - // a scrollable widget takes over touch events - final View currentPage = getPageAt(mCurrentPage); - if(currentPage != null) { - currentPage.cancelLongPress(); - } - } - super.requestDisallowInterceptTouchEvent(disallowIntercept); - } - - /** Returns whether x and y originated within the buffered viewport */ - private boolean isTouchPointInViewportWithBuffer(int x, int y) { - sTmpRect.set(-getMeasuredWidth() / 2, 0, 3 * getMeasuredWidth() / 2, getMeasuredHeight()); - return sTmpRect.contains(x, y); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - /* - * This method JUST determines whether we want to intercept the motion. - * If we return true, onTouchEvent will be called and we do the actual - * scrolling there. - */ - acquireVelocityTrackerAndAddMovement(ev); - - // Skip touch handling if there are no pages to swipe - if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); - - /* - * Shortcut the most recurring case: the user is in the dragging - * state and he is moving his finger. We want to intercept this - * motion. - */ - final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && - (mTouchState == TOUCH_STATE_SCROLLING)) { - return true; - } - - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_MOVE: { - /* - * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check - * whether the user has moved far enough from his original down touch. - */ - if (mActivePointerId != INVALID_POINTER) { - determineScrollingStart(ev); - } - // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN - // event. in that case, treat the first occurence of a move event as a ACTION_DOWN - // i.e. fall through to the next case (don't break) - // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events - // while it's small- this was causing a crash before we checked for INVALID_POINTER) - break; - } - - case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - // Remember location of down touch - mDownMotionX = x; - mDownMotionY = y; - mLastMotionX = x; - mLastMotionXRemainder = 0; - mTotalMotionX = 0; - mActivePointerId = ev.getPointerId(0); - - /* - * If being flinged and user touches the screen, initiate drag; - * otherwise don't. mScroller.isFinished should be false when - * being flinged. - */ - final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); - final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3); - - if (finishedScrolling) { - mTouchState = TOUCH_STATE_REST; - if (!mScroller.isFinished() && !mFreeScroll) { - setCurrentPage(getNextPage()); - pageEndTransition(); - } - } else { - if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { - mTouchState = TOUCH_STATE_SCROLLING; - } else { - mTouchState = TOUCH_STATE_REST; - } - } - - break; - } - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - resetTouchState(); - break; - - case MotionEvent.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - releaseVelocityTracker(); - break; - } - - /* - * The only time we want to intercept motion events is if we are in the - * drag mode. - */ - return mTouchState != TOUCH_STATE_REST; - } - - public boolean isHandlingTouch() { - return mTouchState != TOUCH_STATE_REST; - } - - protected void determineScrollingStart(MotionEvent ev) { - determineScrollingStart(ev, 1.0f); - } - - /* - * Determines if we should change the touch state to start scrolling after the - * user moves their touch point too far. - */ - protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { - // Disallow scrolling if we don't have a valid pointer index - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == -1) return; - - // Disallow scrolling if we started the gesture from outside the viewport - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; - - final int xDiff = (int) Math.abs(x - mLastMotionX); - - final int touchSlop = Math.round(touchSlopScale * mTouchSlop); - boolean xMoved = xDiff > touchSlop; - - if (xMoved) { - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mTotalMotionX += Math.abs(mLastMotionX - x); - mLastMotionX = x; - mLastMotionXRemainder = 0; - onScrollInteractionBegin(); - pageBeginTransition(); - // Stop listening for things like pinches. - requestDisallowInterceptTouchEvent(true); - } - } - - protected void cancelCurrentPageLongPress() { - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } - } - - protected float getScrollProgress(int screenCenter, View v, int page) { - final int halfScreenSize = getMeasuredWidth() / 2; - - int delta = screenCenter - (getScrollForPage(page) + halfScreenSize); - int count = getChildCount(); - - final int totalDistance; - - int adjacentPage = page + 1; - if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { - adjacentPage = page - 1; - } - - if (adjacentPage < 0 || adjacentPage > count - 1) { - totalDistance = v.getMeasuredWidth() + mPageSpacing; - } else { - totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); - } - - float scrollProgress = delta / (totalDistance * 1.0f); - scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); - scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS); - return scrollProgress; - } - - public int getScrollForPage(int index) { - if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { - return 0; - } else { - return mPageScrolls[index]; - } - } - - // While layout transitions are occurring, a child's position may stray from its baseline - // position. This method returns the magnitude of this stray at any given time. - public int getLayoutTransitionOffsetForPage(int index) { - if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { - return 0; - } else { - View child = getChildAt(index); - - int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); - int baselineX = mPageScrolls[index] + scrollOffset; - return (int) (child.getX() - baselineX); - } - } - - protected void dampedOverScroll(float amount) { - if (Float.compare(amount, 0f) == 0) return; - - int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth()); - if (amount < 0) { - mOverScrollX = overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); - } else { - mOverScrollX = mMaxScrollX + overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); - } - invalidate(); - } - - protected void overScroll(float amount) { - dampedOverScroll(amount); - } - - - protected void enableFreeScroll(boolean settleOnPageInFreeScroll) { - setEnableFreeScroll(true); - mSettleOnPageInFreeScroll = settleOnPageInFreeScroll; - } - - private void setEnableFreeScroll(boolean freeScroll) { - boolean wasFreeScroll = mFreeScroll; - mFreeScroll = freeScroll; - - if (mFreeScroll) { - setCurrentPage(getNextPage()); - } else if (wasFreeScroll) { - snapToPage(getNextPage()); - } - - setEnableOverscroll(!freeScroll); - } - - protected void setEnableOverscroll(boolean enable) { - mAllowOverScroll = enable; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - super.onTouchEvent(ev); - - // Skip touch handling if there are no pages to swipe - if (getChildCount() <= 0) return super.onTouchEvent(ev); - - acquireVelocityTrackerAndAddMovement(ev); - - final int action = ev.getAction(); - - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished - * will be false if being flinged. - */ - if (!mScroller.isFinished()) { - abortScrollerAnimation(false); - } - - // Remember where the motion event started - mDownMotionX = mLastMotionX = ev.getX(); - mDownMotionY = ev.getY(); - mLastMotionXRemainder = 0; - mTotalMotionX = 0; - mActivePointerId = ev.getPointerId(0); - - if (mTouchState == TOUCH_STATE_SCROLLING) { - onScrollInteractionBegin(); - pageBeginTransition(); - } - break; - - case MotionEvent.ACTION_MOVE: - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - - if (pointerIndex == -1) return true; - - final float x = ev.getX(pointerIndex); - final float deltaX = mLastMotionX + mLastMotionXRemainder - x; - - mTotalMotionX += Math.abs(deltaX); - - // Only scroll and update mLastMotionX if we have moved some discrete amount. We - // keep the remainder because we are actually testing if we've moved from the last - // scrolled position (which is discrete). - if (Math.abs(deltaX) >= 1.0f) { - scrollBy((int) deltaX, 0); - mLastMotionX = x; - mLastMotionXRemainder = deltaX - (int) deltaX; - } else { - awakenScrollBars(); - } - } else { - determineScrollingStart(ev); - } - break; - - case MotionEvent.ACTION_UP: - if (mTouchState == TOUCH_STATE_SCROLLING) { - final int activePointerId = mActivePointerId; - final int pointerIndex = ev.findPointerIndex(activePointerId); - final float x = ev.getX(pointerIndex); - final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int velocityX = (int) velocityTracker.getXVelocity(activePointerId); - final int deltaX = (int) (x - mDownMotionX); - final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth(); - boolean isSignificantMove = Math.abs(deltaX) > pageWidth * - SIGNIFICANT_MOVE_THRESHOLD; - - mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); - boolean isFling = mTotalMotionX > mTouchSlop && shouldFlingForVelocity(velocityX); - - if (!mFreeScroll) { - // In the case that the page is moved far to one direction and then is flung - // in the opposite direction, we use a threshold to determine whether we should - // just return to the starting page, or if we should skip one further. - boolean returnToOriginalPage = false; - if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && - Math.signum(velocityX) != Math.signum(deltaX) && isFling) { - returnToOriginalPage = true; - } - - int finalPage; - // We give flings precedence over large moves, which is why we short-circuit our - // test for a large move if a fling has been registered. That is, a large - // move to the left and fling to the right will register as a fling to the right. - boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0; - boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0; - if (((isSignificantMove && !isDeltaXLeft && !isFling) || - (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { - finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; - snapToPageWithVelocity(finalPage, velocityX); - } else if (((isSignificantMove && isDeltaXLeft && !isFling) || - (isFling && isVelocityXLeft)) && - mCurrentPage < getChildCount() - 1) { - finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; - snapToPageWithVelocity(finalPage, velocityX); - } else { - snapToDestination(); - } - } else { - if (!mScroller.isFinished()) { - abortScrollerAnimation(true); - } - - float scaleX = getScaleX(); - int vX = (int) (-velocityX * scaleX); - int initialScrollX = (int) (getScrollX() * scaleX); - - mScroller.fling(initialScrollX, - getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); - int unscaledScrollX = (int) (mScroller.getFinalX() / scaleX); - mNextPage = getPageNearestToCenterOfScreen(unscaledScrollX); - int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1); - int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0); - if (mSettleOnPageInFreeScroll && unscaledScrollX > 0 - && unscaledScrollX < mMaxScrollX) { - // If scrolling ends in the half of the added space that is closer to the - // end, settle to the end. Otherwise snap to the nearest page. - // If flinging past one of the ends, don't change the velocity as it will - // get stopped at the end anyway. - final int finalX = unscaledScrollX < firstPageScroll / 2 ? - 0 : - unscaledScrollX > (lastPageScroll + mMaxScrollX) / 2 ? - mMaxScrollX : - getScrollForPage(mNextPage); - - mScroller.setFinalX((int) (finalX * getScaleX())); - // Ensure the scroll/snap doesn't happen too fast; - int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - - mScroller.getDuration(); - if (extraScrollDuration > 0) { - mScroller.extendDuration(extraScrollDuration); - } - } - invalidate(); - } - onScrollInteractionEnd(); - } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { - // at this point we have not moved beyond the touch slop - // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so - // we can just page - int nextPage = Math.max(0, mCurrentPage - 1); - if (nextPage != mCurrentPage) { - snapToPage(nextPage); - } else { - snapToDestination(); - } - } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { - // at this point we have not moved beyond the touch slop - // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so - // we can just page - int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); - if (nextPage != mCurrentPage) { - snapToPage(nextPage); - } else { - snapToDestination(); - } - } - - // End any intermediate reordering states - resetTouchState(); - break; - - case MotionEvent.ACTION_CANCEL: - if (mTouchState == TOUCH_STATE_SCROLLING) { - snapToDestination(); - onScrollInteractionEnd(); - } - resetTouchState(); - break; - - case MotionEvent.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - releaseVelocityTracker(); - break; - } - - return true; - } - - protected boolean shouldFlingForVelocity(int velocityX) { - return Math.abs(velocityX) > mFlingThresholdVelocity; - } - - private void resetTouchState() { - releaseVelocityTracker(); - mTouchState = TOUCH_STATE_REST; - mActivePointerId = INVALID_POINTER; - } - - /** - * Triggered by scrolling via touch - */ - protected void onScrollInteractionBegin() { - } - - protected void onScrollInteractionEnd() { - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - switch (event.getAction()) { - case MotionEvent.ACTION_SCROLL: { - // Handle mouse (or ext. device) by shifting the page depending on the scroll - final float vscroll; - final float hscroll; - if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { - vscroll = 0; - hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); - } else { - vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); - hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); - } - if (hscroll != 0 || vscroll != 0) { - boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) - : (hscroll > 0 || vscroll > 0); - if (isForwardScroll) { - scrollRight(); - } else { - scrollLeft(); - } - return true; - } - } - } - } - return super.onGenericMotionEvent(event); - } - - private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(ev); - } - - private void releaseVelocityTracker() { - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - } - - private void onSecondaryPointerUp(MotionEvent ev) { - final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> - MotionEvent.ACTION_POINTER_INDEX_SHIFT; - final int pointerId = ev.getPointerId(pointerIndex); - if (pointerId == mActivePointerId) { - // This was our active pointer going up. Choose a new - // active pointer and adjust accordingly. - // TODO: Make this decision more intelligent. - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); - mLastMotionXRemainder = 0; - mActivePointerId = ev.getPointerId(newPointerIndex); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } - } - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - int page = indexToPage(indexOfChild(child)); - if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { - snapToPage(page); - } - } - - public int getPageNearestToCenterOfScreen() { - return getPageNearestToCenterOfScreen(getScrollX()); - } - - private int getPageNearestToCenterOfScreen(int scaledScrollX) { - int screenCenter = scaledScrollX + (getMeasuredWidth() / 2); - int minDistanceFromScreenCenter = Integer.MAX_VALUE; - int minDistanceFromScreenCenterIndex = -1; - final int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - View layout = getPageAt(i); - int childWidth = layout.getMeasuredWidth(); - int halfChildWidth = (childWidth / 2); - int childCenter = getChildOffset(i) + halfChildWidth; - int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); - if (distanceFromScreenCenter < minDistanceFromScreenCenter) { - minDistanceFromScreenCenter = distanceFromScreenCenter; - minDistanceFromScreenCenterIndex = i; - } - } - return minDistanceFromScreenCenterIndex; - } - - protected void snapToDestination() { - snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()); - } - - protected boolean isInOverScroll() { - return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); - } - - protected int getPageSnapDuration() { - if (isInOverScroll()) { - return OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION; - } - return PAGE_SNAP_ANIMATION_DURATION; - } - - // We want the duration of the page snap animation to be influenced by the distance that - // the screen has to travel, however, we don't want this duration to be effected in a - // purely linear fashion. Instead, we use this method to moderate the effect that the distance - // of travel has on the overall snap duration. - private float distanceInfluenceForSnapDuration(float f) { - f -= 0.5f; // center the values about 0. - f *= 0.3f * Math.PI / 2.0f; - return (float) Math.sin(f); - } - - protected boolean snapToPageWithVelocity(int whichPage, int velocity) { - whichPage = validateNewPage(whichPage); - int halfScreenSize = getMeasuredWidth() / 2; - - final int newX = getScrollForPage(whichPage); - int delta = newX - getUnboundedScrollX(); - int duration = 0; - - if (Math.abs(velocity) < mMinFlingVelocity) { - // If the velocity is low enough, then treat this more as an automatic page advance - // as opposed to an apparent physical response to flinging - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); - } - - // Here we compute a "distance" that will be used in the computation of the overall - // snap duration. This is a function of the actual distance that needs to be traveled; - // we keep this value close to half screen size in order to reduce the variance in snap - // duration as a function of the distance the page needs to travel. - float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); - float distance = halfScreenSize + halfScreenSize * - distanceInfluenceForSnapDuration(distanceRatio); - - velocity = Math.abs(velocity); - velocity = Math.max(mMinSnapVelocity, velocity); - - // we want the page's snap velocity to approximately match the velocity at which the - // user flings, so we scale the duration by a value near to the derivative of the scroll - // interpolator at zero, ie. 5. We use 4 to make it a little slower. - duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); - - return snapToPage(whichPage, delta, duration); - } - - public boolean snapToPage(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); - } - - public boolean snapToPageImmediately(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); - } - - public boolean snapToPage(int whichPage, int duration) { - return snapToPage(whichPage, duration, false, null); - } - - public boolean snapToPage(int whichPage, int duration, TimeInterpolator interpolator) { - return snapToPage(whichPage, duration, false, interpolator); - } - - protected boolean snapToPage(int whichPage, int duration, boolean immediate, - TimeInterpolator interpolator) { - whichPage = validateNewPage(whichPage); - - int newX = getScrollForPage(whichPage); - final int delta = newX - getUnboundedScrollX(); - return snapToPage(whichPage, delta, duration, immediate, interpolator); - } - - protected boolean snapToPage(int whichPage, int delta, int duration) { - return snapToPage(whichPage, delta, duration, false, null); - } - - protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate, - TimeInterpolator interpolator) { - if (mFirstLayout) { - setCurrentPage(whichPage); - return false; - } - - whichPage = validateNewPage(whichPage); - - mNextPage = whichPage; - - awakenScrollBars(duration); - if (immediate) { - duration = 0; - } else if (duration == 0) { - duration = Math.abs(delta); - } - - if (duration != 0) { - pageBeginTransition(); - } - - if (!mScroller.isFinished()) { - abortScrollerAnimation(false); - } - - mScroller.startScroll(getUnboundedScrollX(), 0, delta, 0, duration); - - updatePageIndicator(); - - // Trigger a compute() to finish switching pages if necessary - if (immediate) { - computeScroll(); - pageEndTransition(); - } - - invalidate(); - return Math.abs(delta) > 0; - } - - public boolean scrollLeft() { - if (getNextPage() > 0) { - snapToPage(getNextPage() - 1); - return true; - } - return false; - } - - public boolean scrollRight() { - if (getNextPage() < getChildCount() - 1) { - snapToPage(getNextPage() + 1); - return true; - } - return false; - } - - @Override - public CharSequence getAccessibilityClassName() { - // Some accessibility services have special logic for ScrollView. Since we provide same - // accessibility info as ScrollView, inform the service to handle use the same way. - return ScrollView.class.getName(); - } - - protected boolean isPageOrderFlipped() { - return false; - } - - /* Accessibility */ - @SuppressWarnings("deprecation") - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - final boolean pagesFlipped = isPageOrderFlipped(); - info.setScrollable(getPageCount() > 1); - if (getCurrentPage() < getPageCount() - 1) { - info.addAction(pagesFlipped ? AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD - : AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - } - if (getCurrentPage() > 0) { - info.addAction(pagesFlipped ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD - : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } - - // Accessibility-wise, PagedView doesn't support long click, so disabling it. - // Besides disabling the accessibility long-click, this also prevents this view from getting - // accessibility focus. - info.setLongClickable(false); - info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); - } - - @Override - public void sendAccessibilityEvent(int eventType) { - // Don't let the view send real scroll events. - if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { - super.sendAccessibilityEvent(eventType); - } - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setScrollable(getPageCount() > 1); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { - return true; - } - final boolean pagesFlipped = isPageOrderFlipped(); - switch (action) { - case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { - if (pagesFlipped ? scrollLeft() : scrollRight()) { - return true; - } - } break; - case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { - if (pagesFlipped ? scrollRight() : scrollLeft()) { - return true; - } - } - break; - } - return false; - } - - protected boolean canAnnouncePageDescription() { - return true; - } - - protected String getCurrentPageDescription() { - return getContext().getString(R.string.default_scroll_format, - getNextPage() + 1, getChildCount()); - } - - protected float getDownMotionX() { - return mDownMotionX; - } - - protected float getDownMotionY() { - return mDownMotionY; - } - - protected interface ComputePageScrollsLogic { - - boolean shouldIncludeView(View view); - } - - public int[] getVisibleChildrenRange() { - float visibleLeft = 0; - float visibleRight = visibleLeft + getMeasuredWidth(); - float scaleX = getScaleX(); - if (scaleX < 1 && scaleX > 0) { - float mid = getMeasuredWidth() / 2; - visibleLeft = mid - ((mid - visibleLeft) / scaleX); - visibleRight = mid + ((visibleRight - mid) / scaleX); - } - - int leftChild = -1; - int rightChild = -1; - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getPageAt(i); - - float left = child.getLeft() + child.getTranslationX() - getScrollX(); - if (left <= visibleRight && (left + child.getMeasuredWidth()) >= visibleLeft) { - if (leftChild == -1) { - leftChild = i; - } - rightChild = i; - } - } - mTmpIntPair[0] = leftChild; - mTmpIntPair[1] = rightChild; - return mTmpIntPair; - } - - public boolean isScrollerFinished() { - return mScroller.isFinished(); - } -} diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java b/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java deleted file mode 100644 index 934693cd08c245b8ec0c1e3810c751c1bc8a53a9..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java +++ /dev/null @@ -1,67 +0,0 @@ -package foundation.e.blisslauncher.core.customviews; - -import android.animation.LayoutTransition; -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicatorDots; -import foundation.e.blisslauncher.features.launcher.LauncherActivity; - -public class Workspace extends PagedView implements View.OnTouchListener{ - - private static final String TAG = "Workspace"; - private static final int DEFAULT_PAGE = 0; - private final LauncherActivity mLauncher; - private LayoutTransition mLayoutTransition; - - public Workspace(Context context, AttributeSet attributeSet) { - this(context, attributeSet, 0); - } - - public Workspace(Context context, AttributeSet attributeSet, int defStyle) { - super(context, attributeSet, defStyle); - - mLauncher = LauncherActivity.getLauncher(context); - setHapticFeedbackEnabled(false); - initWorkspace(); - - setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } - }); - } - - private void initWorkspace() { - mCurrentPage = DEFAULT_PAGE; - setClipToPadding(false); - setupLayoutTransition(); - - //setWallpaperDimension(); - } - - private void setupLayoutTransition() { - // We want to show layout transitions when pages are deleted, to close the gap. - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING); - mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); - setLayoutTransition(mLayoutTransition); - } - - void enableLayoutTransitions() { - setLayoutTransition(mLayoutTransition); - } - void disableLayoutTransitions() { - setLayoutTransition(null); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } -} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java index cfe9cd778a42c1efddfccfc159b9ffa15e2e9cad..17278e58359b693d217751163cc8ddc42d3d45dd 100755 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java @@ -261,8 +261,7 @@ public class LauncherActivity extends AppCompatActivity implements mAppWidgetManager = BlissLauncher.getApplication(this).getAppWidgetManager(); mAppWidgetHost = BlissLauncher.getApplication(this).getAppWidgetHost(); - mLauncherView = LayoutInflater.from(this).inflate( - foundation.e.blisslauncher.R.layout.activity_main, null); + mLauncherView = LayoutInflater.from(this).inflate(R.layout.activity_main, null); setContentView(mLauncherView); setupViews(); @@ -475,7 +474,8 @@ public class LauncherActivity extends AppCompatActivity implements ManagedProfileBroadcastReceiver.unregister(this, managedProfileReceiver); LocalBroadcastManager.getInstance(this).unregisterReceiver(mWeatherReceiver); getCompositeDisposable().dispose(); - events.unsubscribe(); + if (events != null) + events.unsubscribe(); BlissLauncher.getApplication(this).getAppProvider().clear(); } diff --git a/blisslauncherv2/.gitignore b/blisslauncherv2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/blisslauncherv2/.gitignore @@ -0,0 +1 @@ +/build diff --git a/blisslauncherv2/build.gradle b/blisslauncherv2/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..ab09ee01449d200a2bece0154b31980a3622d9c6 --- /dev/null +++ b/blisslauncherv2/build.gradle @@ -0,0 +1,78 @@ +import foundation.e.blisslauncher.buildsrc.Libs +import foundation.e.blisslauncher.buildsrc.Versions + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion Versions.compile_sdk + buildToolsVersion "29.0.2" + + + defaultConfig { + applicationId "foundation.e.blisslauncher.v2" + minSdkVersion Versions.min_sdk + targetSdkVersion Versions.target_sdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(path: ':common') + implementation project(path: ':data-bridge') + implementation project(path: ':domain') + implementation project(path: ':mvicore') + implementation Libs.Kotlin.stdlib + implementation Libs.AndroidX.appcompat + implementation Libs.AndroidX.recyclerview + implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.constraintlayout + + // Rx + implementation Libs.RxJava.rxKotlin + implementation Libs.RxJava.rxJava + implementation Libs.RxJava.rxAndroid + + implementation Libs.Dagger.dagger + implementation Libs.Dagger.android + kapt Libs.Dagger.compiler + kapt Libs.Dagger.androidProcessor + + implementation Libs.timber + + testImplementation Libs.junit + testImplementation Libs.mockK + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + androidTestImplementation 'androidx.test:core:1.0.0' + + // AndroidJUnitRunner and JUnit Rules + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test:rules:1.1.0' + + // Assertions + androidTestImplementation 'androidx.test.ext:junit:1.0.0' + androidTestImplementation 'androidx.test.ext:truth:1.0.0' +} diff --git a/blisslauncherv2/proguard-rules.pro b/blisslauncherv2/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..f1b424510da51fd82143bc74a0a801ae5a1e2fcd --- /dev/null +++ b/blisslauncherv2/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/blisslauncherv2/src/main/AndroidManifest.xml b/blisslauncherv2/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..866623c8582e22c7ab460be886414fe5da7121a0 --- /dev/null +++ b/blisslauncherv2/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..4b4b7f91486c13e984812c6db6a9b331cbc4a774 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher + +import android.app.Application +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import foundation.e.blisslauncher.databridge.DataBridgeInitializer +import foundation.e.blisslauncher.domain.inject.DomainComponent +import foundation.e.blisslauncher.inject.DaggerAppComponent +import timber.log.Timber +import javax.inject.Inject + +class BlissLauncher : Application(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + override fun onCreate() { + super.onCreate() + DataBridgeInitializer.initialize(this) + DaggerAppComponent.factory().create( + this, DomainComponent.INSTANCE + ).inject(this) + setupTimber() + } + + private fun setupTimber() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + + override fun androidInjector(): AndroidInjector { + return androidInjector + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..0d4c0c14a882a27056fc7772e4374b31d9c5c1fa --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java @@ -0,0 +1,48 @@ +package foundation.e.blisslauncher; + +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; +import android.view.View; + +import static android.content.Context.WALLPAPER_SERVICE; + +public class WallpaperChangeReceiver extends BroadcastReceiver { + private final Context mContext; + private IBinder mWindowToken; + private boolean mRegistered; + private View mWorkspace; + + public WallpaperChangeReceiver(View workspace){ + this.mWorkspace = workspace; + this.mContext = mWorkspace.getContext(); + } + + @Override + public void onReceive(Context context, Intent intent) { + //BlurWallpaperProvider.Companion.getInstance(context).updateAsync(); + updateOffset(); + } + + public void setWindowToken(IBinder token) { + mWindowToken = token; + if (mWindowToken == null && mRegistered) { + mWorkspace.getContext().unregisterReceiver(this); + mRegistered = false; + } else if (mWindowToken != null && !mRegistered) { + mWorkspace.getContext() + .registerReceiver(this, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); + onReceive(mWorkspace.getContext(), null); + mRegistered = true; + } + } + + private void updateOffset() { + WallpaperManager wm = (WallpaperManager) mContext.getSystemService(WALLPAPER_SERVICE); + wm.setWallpaperOffsets(mWindowToken, 0f, 0.5f); + wm.setWallpaperOffsetSteps(0.0f, 0.0f); + } +} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..bd71869f3df99b7cb28fb8651864f258a99bdbe6 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.badge; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Log; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +/** + * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). + */ +public class BadgeRenderer { + + private static final String TAG = "BadgeRenderer"; + + // The badge sizes are defined as percentages of the app icon size. + private static final float SIZE_PERCENTAGE = 0.38f; + + // Extra scale down of the dot + private static final float DOT_SCALE = 0.6f; + + // Used to expand the width of the badge for each additional digit. + private static final float OFFSET_PERCENTAGE = 0.02f; + + private final float mDotCenterOffset; + private final int mOffset; + private final float mCircleRadius; + private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + + //private final Bitmap mBackgroundWithShadow; + private final float mBitmapOffset; + + public BadgeRenderer(int iconSizePx) { + mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx; + mOffset = (int) (OFFSET_PERCENTAGE * iconSizePx); + + int size = (int) (DOT_SCALE * mDotCenterOffset); + /*ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT); + mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);*/ + mCircleRadius = size/2; + + mBitmapOffset = 0f; // Same as width. + } + + /** + * Draw a circle in the top right corner of the given bounds, and draw + * @param color The color (based on the icon) to use for the badge. + * @param iconBounds The bounds of the icon being badged. + * @param badgeScale The progress of the animation, from 0 to 1. + * @param spaceForOffset How much space is available to offset the badge up and to the right. + */ + public void draw( + Canvas canvas, int color, Rect iconBounds, float badgeScale, Point spaceForOffset) { + if (iconBounds == null || spaceForOffset == null) { + Log.e(TAG, "Invalid null argument(s) passed in call to draw."); + return; + } + canvas.save(); + // We draw the badge relative to its center. + float badgeCenterX = iconBounds.right - mDotCenterOffset / 2; + float badgeCenterY = iconBounds.top + mDotCenterOffset / 2; + + int offsetX = Math.min(mOffset, spaceForOffset.x); + int offsetY = Math.min(mOffset, spaceForOffset.y); + canvas.translate(badgeCenterX + offsetX, badgeCenterY - offsetY); + canvas.scale(badgeScale, badgeScale); + + /* mCirclePaint.setColor(Color.BLACK); + canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);*/ + mCirclePaint.setColor(color); + canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); + canvas.restore(); + } +} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt new file mode 100644 index 0000000000000000000000000000000000000000..2693475ad0a943f2a2a0053f05c364d8d96d7bf5 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt @@ -0,0 +1,63 @@ +package foundation.e.blisslauncher.common.util + +import android.view.View +import android.view.Window +import foundation.e.blisslauncher.common.Utilities +import javax.inject.Inject + +class SystemUiController @Inject constructor(private val window: Window) { + private val states = IntArray(5) + + fun updateUiState(uiState: Int, isLight: Boolean) { + updateUiState( + uiState, + if (isLight) FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS else FLAG_DARK_NAV or FLAG_DARK_STATUS + ) + } + + fun updateUiState(uiState: Int, flags: Int) { + if (states[uiState] == flags) { + return + } + states[uiState] = flags + val oldFlags = window.decorView.systemUiVisibility + // Apply the state flags in priority order + var newFlags = oldFlags + for (stateFlag in states) { + if (Utilities.ATLEAST_OREO) { + if (stateFlag and FLAG_LIGHT_NAV != 0) { + newFlags = newFlags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else if (stateFlag and FLAG_DARK_NAV != 0) { + newFlags = + newFlags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + } + if (stateFlag and FLAG_LIGHT_STATUS != 0) { + newFlags = newFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else if (stateFlag and FLAG_DARK_STATUS != 0) { + newFlags = newFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } + if (newFlags != oldFlags) { + window.decorView.systemUiVisibility = newFlags + } + } + + override fun toString(): String { + return "states=${states.contentToString()}" + } + + companion object { + // Various UI states in increasing order of priority + const val UI_STATE_BASE_WINDOW = 0 + const val UI_STATE_ALL_APPS = 1 + const val UI_STATE_WIDGET_BOTTOM_SHEET = 2 + const val UI_STATE_ROOT_VIEW = 3 + const val UI_STATE_OVERVIEW = 4 + + const val FLAG_LIGHT_NAV = 1 shl 0 + const val FLAG_DARK_NAV = 1 shl 1 + const val FLAG_LIGHT_STATUS = 1 shl 2 + const val FLAG_DARK_STATUS = 1 shl 3 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..c811c3c37404e67fcc1a344827c0cc05d0b65de9 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt @@ -0,0 +1,60 @@ +package foundation.e.blisslauncher.common.util + +import android.os.SystemClock +import android.os.Trace +import android.util.ArrayMap +import android.util.Log +import android.util.Log.VERBOSE + +class TraceHelper { + companion object { + private const val SYSTEM_TRACE = false + private val upTimes = ArrayMap() + + fun beginSection(sectionName: String) { + var time = upTimes[sectionName] + if (time == null) { + time = if (Log.isLoggable(sectionName, VERBOSE)) 0 else -1 + upTimes.put(sectionName, time) + } + + if (time >= 0) { + if (SYSTEM_TRACE) { + Trace.beginSection(sectionName) + } + time = SystemClock.uptimeMillis() + } + } + + fun partitionSection(sectionName: String, partition: String) { + var time = upTimes[sectionName] + if (time != null && time >= 0) { + if (SYSTEM_TRACE) { + Trace.endSection() + Trace.beginSection(sectionName) + } + + val now = SystemClock.uptimeMillis() + Log.d(sectionName, "$partition : ${now - time}") + time = now + } + } + + fun endSection(sectionName: String) { + endSection(sectionName, "End") + } + + fun endSection(sectionName: String, msg: String) { + val time = upTimes[sectionName] + if (time != null && time >= 0) { + if (SYSTEM_TRACE) { + Trace.endSection() + } + Log.d( + sectionName, + "$msg : ${(SystemClock.uptimeMillis() - time)}" + ) + } + } + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..32ce658f39233595386a393d6f58a8cb24fb3e9d --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt @@ -0,0 +1,70 @@ +package foundation.e.blisslauncher.features + +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.interactor.LoadLauncher +import foundation.e.blisslauncher.features.LauncherStore.LauncherIntent +import foundation.e.blisslauncher.features.LauncherStore.Action +import foundation.e.blisslauncher.features.LauncherStore.Effect +import foundation.e.blisslauncher.features.LauncherStore.News +import foundation.e.blisslauncher.features.launcher.LauncherState +import foundation.e.blisslauncher.mvicore.component.Actor +import foundation.e.blisslauncher.mvicore.component.BaseStore +import foundation.e.blisslauncher.mvicore.component.IntentToAction +import foundation.e.blisslauncher.mvicore.component.Reducer +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import timber.log.Timber +import javax.inject.Inject + +class LauncherStore @Inject constructor(loadLauncher: LoadLauncher) : + BaseStore( + LauncherState.Loading, + IntentToActionImpl(), + ActorImpl(loadLauncher), + ReducerImpl() + ) { + + sealed class LauncherIntent { + object InitialIntent : LauncherIntent() + } + + sealed class Effect { + object Loading : Effect() + object ErrorLoading : Effect() + + data class LoadedResponse(val workspaceModel: WorkspaceModel) : Effect() + } + + sealed class News + + sealed class Action { + object LoadLauncher : Action() + } + + class IntentToActionImpl : IntentToAction { + override fun invoke(intent: LauncherIntent): Action = when (intent) { + is LauncherIntent.InitialIntent -> Action.LoadLauncher + } + } + + class ActorImpl(private val loadLauncher: LoadLauncher) : Actor { + override fun invoke(state: LauncherState, action: Action): Observable { + return loadLauncher().toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .map { Effect.LoadedResponse(it) as Effect } + .startWith(Effect.Loading) + .onErrorReturn { Effect.ErrorLoading } + } + } + + class ReducerImpl : Reducer { + override fun invoke(state: LauncherState, effect: Effect): LauncherState { + return when(effect) { + Effect.Loading -> LauncherState.Loading + Effect.ErrorLoading -> LauncherState.Error + is Effect.LoadedResponse -> LauncherState.Loaded(effect.workspaceModel) + } + } + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c351f3d8ddfa93f2c6816bea40cc416962897f1d --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt @@ -0,0 +1,75 @@ +package foundation.e.blisslauncher.features.base + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import androidx.annotation.IntDef +import foundation.e.blisslauncher.common.util.SystemUiController +import javax.inject.Inject + +open class BaseActivity : Activity() { + + /*val dpChangeListeners = ArrayList() + + @Inject + lateinit var deviceProfile: DeviceProfile*/ + + @Inject + lateinit var systemUiController: SystemUiController + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + flag = true, + value = [ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE] + ) + annotation class ActivityFlags + + @ActivityFlags + private var activityFlags: Int = 0 + + val isStarted: Boolean + get() = activityFlags and ACTIVITY_STATE_STARTED != 0 + + val hasBeenResumed: Boolean + get() = activityFlags and ACTIVITY_STATE_RESUMED != 0 + + override fun onStart() { + activityFlags = activityFlags or ACTIVITY_STATE_STARTED + super.onStart() + } + + override fun onResume() { + activityFlags = activityFlags or ACTIVITY_STATE_RESUMED or ACTIVITY_STATE_USER_ACTIVE + super.onResume() + } + + override fun onUserLeaveHint() { + activityFlags = activityFlags and ACTIVITY_STATE_USER_ACTIVE.inv() + super.onUserLeaveHint() + } + + override fun onPause() { + activityFlags = activityFlags and ACTIVITY_STATE_RESUMED.inv() + super.onPause() + } + + override fun onStop() { + super.onStop() + activityFlags = + activityFlags and ACTIVITY_STATE_STARTED.inv() and ACTIVITY_STATE_USER_ACTIVE.inv() + } + + protected fun dispatchDeviceProfileChanged() { + //dpChangeListeners.forEach { it.onDeviceProfileChanged(deviceProfile) } + } + + companion object { + private const val ACTIVITY_STATE_STARTED = 1 shl 0 + private const val ACTIVITY_STATE_RESUMED = 1 shl 1 + private const val ACTIVITY_STATE_USER_ACTIVE = 1 shl 2 + + fun fromContext(context: Context): BaseActivity = + if (context is BaseActivity) context + else ((context as ContextWrapper).baseContext) as BaseActivity + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c3397f074e79e83e37f3852e2e93a2c8fad96af --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt @@ -0,0 +1,157 @@ +package foundation.e.blisslauncher.features.base + +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.os.Process +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.view.ActionMode +import android.view.View +import android.widget.Toast +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import javax.inject.Inject + +/** + * BaseActivity Extension with the support of Drag and Drop + */ +abstract class BaseDraggingActivity : BaseActivity() { + + private var currentActionMode: ActionMode? = null + protected var isSafeModeEnabled = false + + @Inject + lateinit var launcherAppsRepository: LauncherAppsCompat + + // TODO Replace with LauncherTheme + var themeRes: Int = R.style.AppTheme + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isSafeModeEnabled = packageManager.isSafeMode + setTheme(themeRes) + } + + override fun onActionModeStarted(mode: ActionMode?) { + super.onActionModeStarted(mode) + currentActionMode = mode + } + + override fun onActionModeFinished(mode: ActionMode?) { + super.onActionModeFinished(mode) + currentActionMode = null + } + + abstract fun getRootView(): View + + abstract fun invalidateParent(launcherItem: LauncherItem) + + fun getViewBounds(v: View): Rect { + val pos = IntArray(2) + v.getLocationOnScreen(pos) + return Rect(pos[0], pos[1], pos[0] + v.width, pos[1] + v.height) + } + + abstract fun getActivityLaunchOptions(v: View): ActivityOptions? + + fun getActivityLaunchOptionsAsBundle(v: View): Bundle? { + val activityOptions = getActivityLaunchOptions(v) + return activityOptions?.toBundle() + } + + fun startActivitySafely(v: View, intent: Intent, item: LauncherItem?): Boolean { + if (isSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { + Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show() + return false + } + + val useLaunchAnimation = !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION) + val optsBundle = if (useLaunchAnimation) getActivityLaunchOptionsAsBundle(v) else null + + val user = item?.user + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.sourceBounds = getViewBounds(v) + try { + + //TODO + /*val isShortcut = (item is ShortcutInfo + && (item!!.itemType === Favorites.ITEM_TYPE_SHORTCUT + || item!!.itemType === Favorites.ITEM_TYPE_DEEP_SHORTCUT) + && !(item as ShortcutInfo).isPromise()) */ + val isShortcut = false + if (isShortcut) + startShortcutIntentSafely(intent, optsBundle!!, item!!) + else if (user == null || user == Process.myUserHandle()) { + startActivity(intent, optsBundle) + } else launcherAppsRepository.startActivityForProfile( + intent.component, + user, + intent.sourceBounds, + optsBundle + ) + + return true + } catch (e: Exception) { + when (e) { + is SecurityException, is ActivityNotFoundException -> Toast.makeText( + this, + R.string.activity_not_found, + Toast.LENGTH_SHORT + ).show() + else -> throw e + } + } + return false + } + + private fun startShortcutIntentSafely( + intent: Intent, + optsBundle: Bundle, + item: LauncherItem + ) { + try { + val oldPolicy = StrictMode.getVmPolicy() + try { + // Temporarily disable deathPenalty on all default checks. For eg, shortcuts + // containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure + // is enabled by default on NYC. + StrictMode.setVmPolicy( + VmPolicy.Builder().detectAll() + .penaltyLog().build() + ) + if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + /*val id: String = (info as ShortcutInfo).getDeepShortcutId() + val packageName = intent.getPackage() + DeepShortcutManager.getInstance(this).startShortcut( + packageName, id, intent.sourceBounds, optsBundle, info.user + )*/ + } else { // Could be launching some bookkeeping activity + startActivity(intent, optsBundle) + } + } finally { + StrictMode.setVmPolicy(oldPolicy) + } + } catch (e: SecurityException) { + throw e + } + } + + companion object { + private const val TAG = "BaseDraggingActivity" + const val INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = + "foundation.e.blisslauncher.intent.extra.shortcut.IGNORE_LAUNCH_ANIMATION" + val AUTO_CANCEL_ACTION_MODE = Any() + + fun fromContext(context: Context): BaseDraggingActivity = + if (context is BaseDraggingActivity) context + else ((context as ContextWrapper).baseContext) as BaseDraggingActivity + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt new file mode 100644 index 0000000000000000000000000000000000000000..5fc5a4ad86869457f30da3f5ea1c7071a05979f0 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt @@ -0,0 +1,35 @@ +package foundation.e.blisslauncher.features.launcher + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import foundation.e.blisslauncher.common.LauncherConstants +import foundation.e.blisslauncher.views.CellLayout + +class Hotseat @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CellLayout(context) { + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + + override var containerType = LauncherConstants.ContainerType.CONTAINER_DESKTOP + + override fun setInsets(insets: Rect) { + val lp = getLayoutParams() as FrameLayout.LayoutParams + val idp = launcher.deviceProfile + lp.gravity = Gravity.BOTTOM + lp.width = ViewGroup.LayoutParams.MATCH_PARENT + lp.height = idp.hotseatBarSizePx + insets!!.bottom + + val padding: Rect = idp.hotseatLayoutPadding + setPadding(padding.left, padding.top, padding.right, padding.bottom) + + layoutParams = lp + + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c67f9abf0b4cd2e3428cc225480af4acc5284805 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -0,0 +1,174 @@ +package foundation.e.blisslauncher.features.launcher + +import android.app.ActivityOptions +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Configuration +import android.os.Bundle +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.view.View +import dagger.android.AndroidInjection +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.util.TraceHelper +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.ShortcutItem +import foundation.e.blisslauncher.features.LauncherStore +import foundation.e.blisslauncher.features.base.BaseDraggingActivity +import foundation.e.blisslauncher.views.IconTextView +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_launcher.* +import timber.log.Timber +import java.util.ArrayList +import javax.inject.Inject + +class LauncherActivity : BaseDraggingActivity(), LauncherView { + + private lateinit var oldConfig: Configuration + + private val compositeDisposable: CompositeDisposable = CompositeDisposable() + + private val intentSubject = PublishSubject.create() + + override val events: Observable + get() = intentSubject + + @Inject + lateinit var launcherStore: LauncherStore + + @Inject + lateinit var idp: InvariantDeviceProfile + lateinit var deviceProfile: DeviceProfile + + lateinit var hotseat: Hotseat + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + deviceProfile = idp.getDeviceProfile(this) + if (DEBUG_STRICT_MODE) { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build() + ) + StrictMode.setVmPolicy( + VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build() + ) + } + + setContentView(R.layout.activity_launcher) + + //hotseat = findViewById(R.id.hotseat) + + TraceHelper.beginSection("Launcher-onCreate") + + super.onCreate(savedInstanceState) + TraceHelper.partitionSection("Launcher-onCreate", "super call") + + oldConfig = Configuration(resources.configuration) + + compositeDisposable += Observable.wrap(launcherStore).subscribe { render(it) } + //TODO set model and state here + compositeDisposable += events.subscribe(launcherStore) + + intentSubject.onNext(LauncherStore.LauncherIntent.InitialIntent) + } + + override fun getRootView(): View { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun invalidateParent(launcherItem: LauncherItem) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getActivityLaunchOptions(v: View): ActivityOptions? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onDestroy() { + super.onDestroy() + } + + companion object { + const val TAG = "Launcher" + const val LOGD = false + + const val DEBUG_STRICT_MODE = false + + private const val REQUEST_CREATE_SHORTCUT = 1 + private const val REQUEST_CREATE_APPWIDGET = 5 + + private const val REQUEST_PICK_APPWIDGET = 9 + + private const val REQUEST_BIND_APPWIDGET = 11 + + const val REQUEST_BIND_PENDING_APPWIDGET = 12 + const val REQUEST_RECONFIGURE_APPWIDGET = 13 + + // Type: int + private const val RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen" + + // Type: int + private const val RUNTIME_STATE = "launcher.state" + + // Type: PendingRequestArgs + private const val RUNTIME_STATE_PENDING_REQUEST_ARGS = "launcher.request_args" + + // Type: ActivityResultInfo + private const val RUNTIME_STATE_PENDING_ACTIVITY_RESULT = + "launcher.activity_result" + + // Type: SparseArray + private const val RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel" + + fun getLauncher(context: Context): LauncherActivity { + return if (context is LauncherActivity) { + context + } else (context as ContextWrapper).baseContext as LauncherActivity + } + } + + override fun render(state: LauncherState) { + Timber.d("Current state is $state") + if (state is LauncherState.Loaded) { + val model = state.workspaceModel + bindScreens(model) + bindWorkspaceItems(model.workspaceItems) + } + } + + private fun bindScreens(model: WorkspaceModel) { + workspace.addExtraEmptyScreen() + model.workspaceScreens.forEach { + workspace.insertNewWorkspaceScreen(it) + } + + Timber.d("Total child in workspace is: ${workspace.childCount}") + } + + private fun bindWorkspaceItems(workspaceItems: ArrayList) { + Timber.d("Total workspace items: ${workspaceItems.size}") + workspaceItems.forEach { + if (it is LauncherItemWithIcon) { + val view = IconTextView(this) + view.applyFromShortcutItem(it as ShortcutItem) + workspace.addInScreenFromBind(view, it) + } + } + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..da745e6cb8831714ec15999e17fc14b796c6ff83 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.features.launcher + +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.common.util.SystemUiController + +@Module +class LauncherActivityModule { + + @Provides + fun provideSystemUiController(launcherActivity: LauncherActivity): SystemUiController = + SystemUiController(launcherActivity.window) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt new file mode 100644 index 0000000000000000000000000000000000000000..a07a04d2b41751d1f6ee839097f5ce82923538e5 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -0,0 +1,30 @@ +package foundation.e.blisslauncher.features.launcher + +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.LauncherItem + +sealed class LauncherState { + + /** + * Very initial launcher state when there is nothing going on. + */ + object Empty : LauncherState() + + /** + * Launcher State when launcher is loading its content + */ + object Loading : LauncherState() + object Error : LauncherState() { + + } + + /** + * Launcher State when launcher finished its loading and available to show its data + */ + data class Loaded(val workspaceModel: WorkspaceModel) : LauncherState() + + /** + * Launcher State when launcher swipe to search down is invoked. + */ + data class Search(val searchQuery: String) : LauncherState() +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt new file mode 100644 index 0000000000000000000000000000000000000000..790f5a9791a0276e25279ff4ae928d8cf498bb28 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt @@ -0,0 +1,433 @@ +package foundation.e.blisslauncher.features.launcher + +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.os.UserHandle +import androidx.core.util.set +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import foundation.e.blisslauncher.domain.Matcher +import foundation.e.blisslauncher.domain.addFlag +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.removeFlag + +/** + * Stores data related to Launcher in memory. + */ +sealed class LauncherStateTemp constructor( + /*val context: Context, + val launcherApps: LauncherAppsCompat,*/ + /** + * Map of all the items (apps, shortcuts, folder or widgets) to their ids + */ + val itemsIdMap: LongArrayMap, + + /** + * List of all apps, folders, shortcuts and widgets directly on screen + * (no apps, shortcuts within folders). + */ + val allItems: List, + + /** + * Map of all the folders to their ids + */ + val folders: LongArrayMap, + + /** + * Ordered list of workspace screen ids + */ + val workspaceScreen: List, + + /** The list of all apps. */ + val data: List +) { + + @Synchronized + fun clear() { + itemsIdMap.clear() + folders.clear() + } + + /*override fun getAllActivities( + user: UserHandle, + quietMode: Boolean + ): List { + val apps = launcherApps.getActivityList(null, user) + if (apps.isNotEmpty()) { + apps.forEach { + add(ApplicationItem(it, user, quietMode), it) + } + } + return data + } + + override fun add( + packageName: String, + user: UserHandle, + quietMode: Boolean + ): ArrayList { + val addedPackageApps = ArrayList() + val matches = launcherApps.getActivityList(packageName, user) + matches.forEach { info -> + add(ApplicationItem( + info, user, quietMode + ).apply { + id = System.nanoTime() + }.let { + addedPackageApps.add(it) + it + }, + info + ) + } + return addedPackageApps + } +*/ + fun remove(packageName: String, user: UserHandle) { + val data = data + val iterator = data.iterator() + /*while (iterator.hasNext()) { + val item = iterator.next() + if (item.componentName.packageName == packageName && item.user == user) { + removed.add(item) + iterator.remove() + } + }*/ + } + + /*override fun updatedPackages( + packages: Array, + user: UserHandle, + quietMode: Boolean + ): List { + //TODO: Update icon cache for packages + val addedApps = ArrayList() + val modifiedApps = ArrayList() + + val removedPackages = HashSet() + val removedComponents = HashSet() + + packages.forEach { + if (!launcherApps.isPackageEnabledForProfile(it, user)) { + removedPackages.add(it) + } else { + val matches = launcherApps.getActivityList(it, user) + if (matches.isNotEmpty()) { + removedComponents.addAll( + removeIfNoActivityFound( + context, + matches, + it, + user + ) + ) + + matches.forEach { + var applicationItem = + findApplicationItem(it.componentName, user) + if (applicationItem == null) { + applicationItem = + ApplicationItem(it, user, quietMode) + add(applicationItem, it) + addedApps.add(applicationItem) + } else { + //TODO: update icon and title + modifiedApps.add(applicationItem) + } + } + } else { + removedPackages.add(it) + } + } + } + val flagOp = removeFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + itemsIdMap.forEach { + //TODO: If user and packageSet of icon resource equals, + //TODO: Update item flag here. + } + return modifiedApps + }*/ + + fun suspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun unsuspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + removeFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List { + val matcher: ItemInfoMatcher = Matcher.ofUser(user) + val flagOp = if (quietMode) addFlag else removeFlag + return updateDisabledFlags( + matcher, + flagOp(LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER) + ) + } + + fun makePackagesUnavailable( + packages: Array, + user: UserHandle + ): List { + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + ) + } + + fun removePackages(packages: Array, user: UserHandle): List { + val removedPackages = packages.toHashSet() + val matcher = Matcher.ofPackages(removedPackages, user) + // Remove any queued items from the install queue + //TODO: InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser) + return removePackages(matcher) + } + + @Synchronized + fun removeItem(context: Context, vararg items: LauncherItem) { + items.forEach { + when (it.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.remove(it.id) + //allItems.remove(it) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + //allItems.remove(it) + } + } + itemsIdMap.remove(it.id) + } + } + + @Synchronized + fun addItem(item: LauncherItem, newItem: Boolean): LauncherStateTemp { + val mutableAllItems = allItems.toMutableList() + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + /*if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + } + } else { + findOrMakeFolder(item.container).add(item as AppShortcutItem, false) + } + }*/ + } + } + return this + } + + fun findOrMakeFolder(id: Long): FolderItem { + var folderItem: FolderItem? = folders[id] + if (folderItem == null) { + folderItem = FolderItem() + folders[id] = folderItem + } + return folderItem + } + + /** + * Returns whether *apps* contains *component*. + */ + fun checkForComponent( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Finds an application item corresponding to the given component name and user. + */ + fun findApplicationItem( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in data) { + if (componentName == item.componentName && user == item.user) { + return item + } + } + return null + } + + /*fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { + if (findApplicationItem(item.componentName, item.user) != null) { + return + } + // TODO: Update icon from IconCache + val mutableData = data.toMutableList() + mutableData.add(item) + return copy(data = mutableData) + data.add(item) + addItem(item, true) + }*/ + + @Synchronized + fun updateDisabledFlags( + matcher: ItemInfoMatcher, + flagOp: (oldFlags: Int) -> Int + ): List { + val updatedItems = ArrayList() + itemsIdMap.filter { + it is LauncherItemWithIcon && matcher(it, it.getTargetComponent()!!) + }.forEach { + it as LauncherItemWithIcon + val oldFlags = it.runtimeStatusFlags + it.apply { flagOp(runtimeStatusFlags) } + if (it.runtimeStatusFlags != oldFlags) + updatedItems.add(it) + } + return updatedItems + } + + @Synchronized + fun removePackages( + matches: (item: LauncherItem, cn: ComponentName) -> Boolean + ): List { + val removedItems = HashSet() + itemsIdMap.forEach { + if (it is LauncherItemWithIcon) it.let { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } else if (it is FolderItem) it.let { folder -> + folder.contents.forEach { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } + } + } + //TODO: delete items from database sequentially and remove them from itemsIdMap + return removedItems.toList() + } + + @Synchronized + fun removeIfNoActivityFound( + context: Context, + matches: List, + packageName: String, + user: UserHandle + ): HashSet { + val removedComponents = HashSet() + val modified = ArrayList() + itemsIdMap.filter { it.itemType == LauncherConstants.ItemType.APPLICATION } + .forEach { + it as ApplicationItem + if (it.user == user && packageName == it.componentName.packageName) { + if (!findActivity(matches, it.componentName)) { + removedComponents.add(it.componentName) + } + } + } + return removedComponents + } + + /** + * Returns whether *apps* contains *component*. + */ + private fun findActivity( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Find an AppInfo object for the given componentName + * + * @return the corresponding AppInfo or null + */ + fun findAppInfo( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in itemsIdMap) { + if (item is ApplicationItem && + componentName == item.componentName && + user == item.user + ) { + return item + } + } + return null + } + + fun addItem( + item: LauncherItem, + mutableData: MutableList, + mutableAllItems: MutableList, + newItem: Boolean + ) { + mutableData.add(item as ApplicationItem) + itemsIdMap.put(item.id, item) + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + /*if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + } + } else { + findOrMakeFolder(item.container).add( + item as AppShortcutItem, + false + ) + } + }*/ + } + } + } + + companion object { + const val TAG = "LauncherModelStore" + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt new file mode 100644 index 0000000000000000000000000000000000000000..566df94a6b28e2298383fbef4b8ae60275625c2d --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.features.launcher + +import foundation.e.blisslauncher.features.LauncherStore +import foundation.e.blisslauncher.mvicore.component.MviView + +interface LauncherView : MviView \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt new file mode 100644 index 0000000000000000000000000000000000000000..fed0c205ca533fdcd63500819548503997cd788f --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt @@ -0,0 +1,432 @@ +package foundation.e.blisslauncher.features.launcher + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.LayoutTransition +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.app.WallpaperManager +import android.content.Context +import android.graphics.Color +import android.graphics.Point +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.WallpaperChangeReceiver +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.views.CellLayout +import foundation.e.blisslauncher.views.Insettable +import foundation.e.blisslauncher.views.PagedView +import timber.log.Timber +import javax.inject.Inject + +class Workspace @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : PagedView(context, attrs, defStyleAttr), Insettable { + + private val mTempXY: IntArray = IntArray(2) + private var mYDown: Float = 0.0f + private var mXDown: Float = 0.0f + private val FADE_EMPTY_SCREEN_DURATION: Int = 150 + private val SNAP_OFF_EMPTY_SCREEN_DURATION: Int = 400 + + private lateinit var wallpaperReceiver: WallpaperChangeReceiver + + @get:JvmName("getLayoutTransition_") + private val layoutTransition: LayoutTransition by lazy { + LayoutTransition().apply { + this.enableTransitionType(LayoutTransition.DISAPPEARING) + this.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING) + this.disableTransitionType(LayoutTransition.APPEARING) + this.disableTransitionType(LayoutTransition.CHANGE_APPEARING) + } + } + + private val wallpaperManager: WallpaperManager + + @Inject + lateinit var invariantDeviceProfile: InvariantDeviceProfile + lateinit var deviceProfile: DeviceProfile + + private var maxDistanceForFolderCreation: Float = 0.0f + private val screenOrder = ArrayList() + private val workspaceScreens = LongArrayMap() + + init { + + deviceProfile = LauncherActivity.getLauncher(context).deviceProfile + invariantDeviceProfile = deviceProfile.inv + + wallpaperReceiver = WallpaperChangeReceiver(this) + isHapticFeedbackEnabled = false + + wallpaperManager = WallpaperManager.getInstance(context) + currentPage = DEFAULT_PAGE + clipToPadding = false + + setLayoutTransition(layoutTransition) + + // Set the wallpaper dimensions when Launcher starts up + setWallpaperDimension() + isMotionEventSplittingEnabled = true + + //TODO: Set touch listener if required + } + + private fun setupLayoutTransition() { + // We want to show layout transitions when pages are deleted, to close the gap. + val layoutTransition = LayoutTransition() + layoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING) + layoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING) + layoutTransition.disableTransitionType(LayoutTransition.APPEARING) + layoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING) + } + + fun enableLayoutTransitions() { + setLayoutTransition(layoutTransition) + } + + fun disableLayoutTransitions() { + setLayoutTransition(null) + } + + private fun setWallpaperDimension() { + //TODO: Run it on a separate thread + val size: Point = invariantDeviceProfile.defaultWallpaperSize + if (size.x != wallpaperManager.desiredMinimumWidth || + size.y != wallpaperManager.desiredMinimumHeight + ) { + wallpaperManager.suggestDesiredDimensions(size.x, size.y) + } + } + + override fun setInsets(insets: Rect) { + this.insets.set(insets) + maxDistanceForFolderCreation = 0.55f * deviceProfile.iconSizePx + + val padding: Rect = deviceProfile.workspacePadding + setPadding(padding.left, padding.top, padding.right, padding.bottom) + } + + override fun onViewAdded(child: View?) { + require(child is CellLayout) { "A Workspace can only have CellLayout children." } + val CellLayout = child as CellLayout + super.onViewAdded(child) + } + + val isTouchActive: Boolean + get() = touchState != TOUCH_STATE_REST + + fun removeAllWorkspaceScreens() { + disableLayoutTransitions() + //removeFolderListners() + screenOrder.clear() + workspaceScreens.clear() + enableLayoutTransitions() + } + + fun insertNewWorkspaceScreenBeforeEmptyScreen(screenId: Long) { + var insertIndex = screenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID) + if (insertIndex < 0) { + insertIndex = screenOrder.size + } + + insertNewWorkspaceScreen(screenId, insertIndex) + } + + fun insertNewWorkspaceScreen(screenId: Long) { + insertNewWorkspaceScreen(screenId, childCount) + } + + fun insertNewWorkspaceScreen(screenId: Long, insertIndex: Int): CellLayout { + if (workspaceScreens.containsKey(screenId)) { + throw RuntimeException("Screen id $screenId already exists!") + } + + val newScreen = LayoutInflater.from(context) + .inflate(R.layout.layout_workspace_screen, this, false) as CellLayout + // TODO: Set padding if needed + newScreen.columnCount = invariantDeviceProfile.numColumns + newScreen.rowCount = invariantDeviceProfile.numRows + + val paddingLeftRight: Int = deviceProfile.cellLayoutPaddingLeftRightPx + val paddingBottom: Int = deviceProfile.cellLayoutBottomPaddingPx + newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom) + workspaceScreens.put(screenId, newScreen) + screenOrder.add(insertIndex, screenId) + addView(newScreen, insertIndex) + + // TODO: Apply state transition animation if needed + // TODO: Enable accessibilty + return newScreen + } + + fun addExtraEmptyScreenOnDrag() { + //TODO: Add once drag layer is done + } + + fun addExtraEmptyScreen(): Boolean { + if (!workspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { + insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID) + return true + } + return false + } + + fun convertFinalScreenToEmptyScreenIfNecessary() { + //TODO: Return if early if workspace is loading + + if (hasExtraEmptyScreen || screenOrder.size == 0) return + val finalScreenId = screenOrder[screenOrder.size - 1] + val finalScreen = workspaceScreens[finalScreenId] + // TODO: If the final screen is empty, convert it to extra empty screen + } + + fun removeExtraEmptyScreen( + animate: Boolean, + stripEmptyScreens: Boolean + ) { + removeExtraEmptyScreenDelayed(animate, null, 0, stripEmptyScreens) + } + + fun removeExtraEmptyScreenDelayed( + animate: Boolean, + onComplete: Runnable?, + delay: Int, + stripEmptyScreens: Boolean + ) { + //TODO: Return if early if workspace is loading + + if (delay > 0) { + postDelayed({ + removeExtraEmptyScreenDelayed(animate, onComplete, 0, stripEmptyScreens) + }, delay.toLong()) + return + } + + convertFinalScreenToEmptyScreenIfNecessary() + + if (hasExtraEmptyScreen) { + val emptyIndex: Int = + screenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID) + if (nextPage == emptyIndex) { + snapToPage( + nextPage - 1, + SNAP_OFF_EMPTY_SCREEN_DURATION + ) + fadeAndRemoveEmptyScreen( + SNAP_OFF_EMPTY_SCREEN_DURATION, + FADE_EMPTY_SCREEN_DURATION, + onComplete, + stripEmptyScreens + ) + } else { + snapToPage(nextPage, 0) + fadeAndRemoveEmptyScreen( + 0, FADE_EMPTY_SCREEN_DURATION, + onComplete, stripEmptyScreens + ) + } + return + } else if (stripEmptyScreens) { + // If we're not going to strip the empty screens after removing + // the extra empty screen, do it right away. + stripEmptyScreens() + } + onComplete?.run() + } + + private fun fadeAndRemoveEmptyScreen( + delay: Int, + duration: Int, + onComplete: Runnable?, + stripEmptyScreens: Boolean + ) { + // XXX: Do we need to update LM workspace screens below? + val alpha = PropertyValuesHolder.ofFloat("alpha", 0f) + val bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f) + val gl: CellLayout = + workspaceScreens.get(EXTRA_EMPTY_SCREEN_ID) + val oa: ObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(gl, alpha, bgAlpha) + oa.duration = duration.toLong() + oa.startDelay = delay.toLong() + oa.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + Runnable { + if (hasExtraEmptyScreen) { + workspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID) + screenOrder.remove(EXTRA_EMPTY_SCREEN_ID) + removeView(gl) + if (stripEmptyScreens) { + stripEmptyScreens() + } + // Update the page indicator to reflect the removed page. + //showPageIndicatorAtCurrentScroll() + } + }.run() + onComplete?.run() + } + }) + oa.start() + } + + val hasExtraEmptyScreen: Boolean + get() = workspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && childCount > 1 + + fun commitExtraEmptyScreen(): Long { + // TODO: Return -1 if launcher is loading + + // TODO: Update the model here + TODO() + } + + fun getScreenWithId(screenId: Long) = workspaceScreens[screenId] + + fun getIdForScreen(screen: CellLayout): Long { + val index = workspaceScreens.indexOfValue(screen) + return if (index != -1) workspaceScreens.keyAt(index) else -1 + } + + fun getPageIndexForScreen(screenId: Long) = indexOfChild(workspaceScreens[screenId]) + + fun getScreenIdForPageIndex(index: Int): Long { + if (0 <= index && index < screenOrder.size) { + screenOrder[index] + } + return -1 + } + + fun getScreenOrder() = screenOrder + + fun stripEmptyScreens() { + // TODO: Return early if launcher is loading + } + + fun addInScreenFromBind(child: View, item: LauncherItem) { + val x = item.cellX + val y = item.cellY + Timber.d("Child title is: ${item.title}") + Timber.d("Child screen is: ${item.screenId}") + addInScreen(child, item.container, item.screenId, x, y) + } + + fun addInScreen(child: View, item: LauncherItem) { + addInScreen(child, item.container, item.screenId, item.cellX, item.cellY) + } + + private fun addInScreen(child: View, container: Long, screenId: Long, x: Int, y: Int) { + if (container == LauncherConstants.ContainerType.CONTAINER_DESKTOP) { + if (getScreenWithId(screenId) == null) { + Timber.e("Skipping child, screenId $screenId not found") + // DEBUGGING - Print out the stack trace to see where we are adding from + Throwable().printStackTrace() + return + } + } + + if (screenId == EXTRA_EMPTY_SCREEN_ID) { + throw RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID") + } + + var layout: CellLayout + if (container == LauncherConstants.ContainerType.CONTAINER_HOTSEAT) { + //TODO: Hide folder title in hotseat + //layout = LauncherActivity.getLauncher(context).hotseat + } else { + layout = getScreenWithId(screenId) + layout.addView(child) + child.visibility = View.VISIBLE + child.isHapticFeedbackEnabled = false + child.setOnLongClickListener(null) + } + + val genericLp = child.layoutParams + } + + private fun shouldConsumeTouch(v: View): Boolean { + return (!workspaceIconsCanBeDragged || + !workspaceInModalState && indexOfChild(v) != currentPage) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + mXDown = ev.x + mYDown = ev.y + } + return super.onInterceptTouchEvent(ev) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + wallpaperReceiver.setWindowToken(windowToken) + //TODO: Set window token to drag layer + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + wallpaperReceiver.setWindowToken(null) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + //TODO: Update page alpha values + } + + override fun getDescendantFocusability(): Int { + if (workspaceInModalState) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS + } + return super.getDescendantFocusability() + } + + val workspaceInModalState = false //TODO: Change it with the state of launcher + + val workspaceIconsCanBeDragged = true // TODO: Change with the launcher State + + private fun updateChildrenLayersEnabled() { + /*val enableChildrenLayers = mIsSwitchingState || isPageInTransition() + + if (enableChildrenLayers != mChildrenLayersEnabled) { + mChildrenLayersEnabled = enableChildrenLayers + if (mChildrenLayersEnabled) { + enableHwLayersOnVisiblePages() + } else { + for (i in 0 until getPageCount()) { + val cl: CellLayout = getChildAt(i) as CellLayout + cl.enableHardwareLayer(false) + } + } + }*/ + } + + private fun enableHwLayersOnVisiblePages() { + } + + fun onWallpaperTap(ev: MotionEvent) { + val position: IntArray = mTempXY + getLocationOnScreen(position) + val pointerIndex = ev.actionIndex + position[0] += ev.getX(pointerIndex).toInt() + position[1] += ev.getY(pointerIndex).toInt() + wallpaperManager.sendWallpaperCommand( + windowToken, + if (ev.action == MotionEvent.ACTION_UP) WallpaperManager.COMMAND_TAP + else WallpaperManager.COMMAND_SECONDARY_TAP, + position[0], position[1], 0, null + ) + } + + companion object { + const val DEFAULT_PAGE = 0 + const val EXTRA_EMPTY_SCREEN_ID: Long = -201 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..82c7842995f7c0aa9e639cb2faa75ccf5dd25d8a --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.graphics + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Process +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Factory for creating new drawables. + */ +@Singleton +open class DrawableFactory @Inject constructor(context: Context){ + private val myUser: UserHandle = Process.myUserHandle() + private val userBadges = ArrayMap() + + /** + * Returns a FastBitmapDrawable with the icon. + */ + fun newIcon(info: LauncherItemWithIcon): FastBitmapDrawable { + val drawable = FastBitmapDrawable(info) + drawable.setIsDisabled(info.isDisabled()) + return drawable + } + + /** + * Returns a drawable that can be used as a badge for the user or null. + */ + fun getBadgeForUser(user: UserHandle, context: Context): Drawable? { + if (myUser == user) { + return null + } + val badgeBitmap = getUserBadge(user, context) + val d = FastBitmapDrawable(badgeBitmap) + d.isFilterBitmap = true + d.setBounds(0, 0, badgeBitmap!!.width, badgeBitmap.height) + return d + } + + @Synchronized + fun getUserBadge( + user: UserHandle, + context: Context + ): Bitmap? { + var badgeBitmap = userBadges[user] + if (badgeBitmap != null) { + return badgeBitmap + } + val res = context.applicationContext.resources + val badgeSize = + res.getDimensionPixelSize(R.dimen.profile_badge_size) + badgeBitmap = Bitmap.createBitmap( + badgeSize, + badgeSize, + Bitmap.Config.ARGB_8888 + ) + val drawable = context.packageManager.getUserBadgedDrawableForDensity( + BitmapDrawable(res, badgeBitmap), + user, + Rect(0, 0, badgeSize, badgeSize), + 0 + ) + if (drawable is BitmapDrawable) { + badgeBitmap = drawable.bitmap + } else { + badgeBitmap.eraseColor(Color.TRANSPARENT) + val c = Canvas(badgeBitmap) + drawable.setBounds(0, 0, badgeSize, badgeSize) + drawable.draw(c) + c.setBitmap(null) + } + userBadges[user] = badgeBitmap + return badgeBitmap + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..fcd74b229044ba50016d918030c6a05a5c4b5108 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.graphics + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DynamicDrawableFactory @Inject constructor(context: Context) : DrawableFactory(context) { + +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt new file mode 100644 index 0000000000000000000000000000000000000000..912065173140811918a88d59a1f4ce3de0f0d0f8 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.graphics + +import android.R +import android.animation.ObjectAnimator +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.Property +import android.util.SparseArray +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import kotlin.math.floor + +open class FastBitmapDrawable constructor( + protected var bitmap: Bitmap? +) : + Drawable() { + protected val mPaint = + Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG) + private var mIsPressed = false + private var mIsDisabled = false + private var mScaleAnimation: ObjectAnimator? = null + private var mScale = 1f + + // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and + // as a result, can be used to compose the key for the cached ColorMatrixColorFilters + private var mDesaturation = 0 + private var mBrightness = 0 + private var mAlpha = 255 + private var mPrevUpdateKey = Int.MAX_VALUE + + constructor(info: LauncherItemWithIcon) : this(info.iconBitmap) + + override fun draw(canvas: Canvas) { + if (mScaleAnimation != null) { + val count = canvas.save() + val bounds = bounds + canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY()) + drawInternal(canvas, bounds) + canvas.restoreToCount(count) + } else { + drawInternal(canvas, bounds) + } + } + + protected fun drawInternal( + canvas: Canvas, + bounds: Rect? + ) { + canvas.drawBitmap(bitmap, null, bounds, mPaint) + } + + override fun setColorFilter(cf: ColorFilter) { + // No op + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun setAlpha(alpha: Int) { + mAlpha = alpha + mPaint.alpha = alpha + } + + override fun setFilterBitmap(filterBitmap: Boolean) { + mPaint.isFilterBitmap = filterBitmap + mPaint.isAntiAlias = filterBitmap + } + + override fun getAlpha(): Int { + return mAlpha + } + + val animatedScale: Float + get() = if (mScaleAnimation == null) 1f else mScale + + override fun getIntrinsicWidth(): Int { + return bitmap!!.width + } + + override fun getIntrinsicHeight(): Int { + return bitmap!!.height + } + + override fun getMinimumWidth(): Int { + return bounds.width() + } + + override fun getMinimumHeight(): Int { + return bounds.height() + } + + override fun isStateful(): Boolean { + return true + } + + override fun getColorFilter(): ColorFilter { + return mPaint.colorFilter + } + + override fun onStateChange(state: IntArray): Boolean { + var isPressed = false + for (s in state) { + if (s == R.attr.state_pressed) { + isPressed = true + break + } + } + if (mIsPressed != isPressed) { + mIsPressed = isPressed + if (mScaleAnimation != null) { + mScaleAnimation!!.cancel() + mScaleAnimation = null + } + if (mIsPressed) { + // Animate when going to pressed state + mScaleAnimation = ObjectAnimator.ofFloat( + this, + SCALE, + PRESSED_SCALE + ) + mScaleAnimation!!.duration = CLICK_FEEDBACK_DURATION.toLong() + mScaleAnimation!!.start() + } else { + mScale = 1f + invalidateSelf() + } + return true + } + return false + } + + private fun invalidateDesaturationAndBrightness() { + desaturation = if (mIsDisabled) DISABLED_DESATURATION else 0f + brightness = if (mIsDisabled) DISABLED_BRIGHTNESS else 0f + } + + fun setIsDisabled(isDisabled: Boolean) { + if (mIsDisabled != isDisabled) { + mIsDisabled = isDisabled + invalidateDesaturationAndBrightness() + } + } + + /** + * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] + */ + var desaturation: Float + get() = mDesaturation.toFloat() / REDUCED_FILTER_VALUE_SPACE + private set(desaturation) { + val newDesaturation = + Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE.toDouble()).toInt() + if (mDesaturation != newDesaturation) { + mDesaturation = newDesaturation + updateFilter() + } + } + + /** + * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] + */ + private var brightness: Float + private get() = mBrightness.toFloat() / REDUCED_FILTER_VALUE_SPACE + private set(brightness) { + val newBrightness = + floor(brightness * REDUCED_FILTER_VALUE_SPACE.toDouble()).toInt() + if (mBrightness != newBrightness) { + mBrightness = newBrightness + updateFilter() + } + } + + /** + * Updates the paint to reflect the current brightness and saturation. + */ + private fun updateFilter() { + var usePorterDuffFilter = false + var key = -1 + if (mDesaturation > 0) { + key = mDesaturation shl 16 or mBrightness + } else if (mBrightness > 0) { + // Compose a key with a fully saturated icon if we are just animating brightness + key = 1 shl 16 or mBrightness + + // We found that in L, ColorFilters cause drawing artifacts with shadows baked into + // icons, so just use a PorterDuff filter when we aren't animating saturation + usePorterDuffFilter = true + } + + // Debounce multiple updates on the same frame + if (key == mPrevUpdateKey) { + return + } + mPrevUpdateKey = key + if (key != -1) { + var filter = sCachedFilter[key] + if (filter == null) { + val brightnessF = brightness + val brightnessI = (255 * brightnessF).toInt() + if (usePorterDuffFilter) { + filter = PorterDuffColorFilter( + Color.argb(brightnessI, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP + ) + } else { + val saturationF = 1f - desaturation + sTempFilterMatrix.setSaturation(saturationF) + if (mBrightness > 0) { + // Brightness: C-new = C-old*(1-amount) + amount + val scale = 1f - brightnessF + val mat = + sTempBrightnessMatrix.array + mat[0] = scale + mat[6] = scale + mat[12] = scale + mat[4] = brightnessI.toFloat() + mat[9] = brightnessI.toFloat() + mat[14] = brightnessI.toFloat() + sTempFilterMatrix.preConcat(sTempBrightnessMatrix) + } + filter = ColorMatrixColorFilter(sTempFilterMatrix) + } + sCachedFilter.append(key, filter) + } + mPaint.colorFilter = filter + } else { + mPaint.colorFilter = null + } + invalidateSelf() + } + + override fun getConstantState(): ConstantState { + return MyConstantState(bitmap) + } + + protected class MyConstantState(protected val mBitmap: Bitmap?) : + ConstantState() { + override fun newDrawable(): Drawable { + return FastBitmapDrawable(mBitmap) + } + + override fun getChangingConfigurations(): Int { + return 0 + } + } + + companion object { + private const val PRESSED_SCALE = 1.1f + private const val DISABLED_DESATURATION = 1f + private const val DISABLED_BRIGHTNESS = 0.5f + const val CLICK_FEEDBACK_DURATION = 200 + + // Since we don't need 256^2 values for combinations of both the brightness and saturation, we + // reduce the value space to a smaller value V, which reduces the number of cached + // ColorMatrixColorFilters that we need to keep to V^2 + private const val REDUCED_FILTER_VALUE_SPACE = 48 + + // A cache of ColorFilters for optimizing brightness and saturation animations + private val sCachedFilter = SparseArray() + + // Temporary matrices used for calculation + private val sTempBrightnessMatrix = ColorMatrix() + private val sTempFilterMatrix = ColorMatrix() + + // Animator and properties for the fast bitmap drawable's scale + private val SCALE: Property = object : + Property(java.lang.Float.TYPE, "scale") { + override fun get(fastBitmapDrawable: FastBitmapDrawable): Float { + return fastBitmapDrawable.mScale + } + + override fun set( + fastBitmapDrawable: FastBitmapDrawable, + value: Float + ) { + fastBitmapDrawable.mScale = value + fastBitmapDrawable.invalidateSelf() + } + } + } + + init { + isFilterBitmap = true + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ca4d456133895ce6946ad392944b2ca0fe8d4a6 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt @@ -0,0 +1,15 @@ +package foundation.e.blisslauncher.inject + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import foundation.e.blisslauncher.common.inject.PerActivity +import foundation.e.blisslauncher.features.launcher.LauncherActivity +import foundation.e.blisslauncher.features.launcher.LauncherActivityModule + +@Module +abstract class ActivityBindsModule { + + @PerActivity + @ContributesAndroidInjector(modules = [LauncherActivityModule::class]) + abstract fun contributesLauncherActivity(): LauncherActivity +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a8ce92651ebecc22f896d7d7124c79d625389e2 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt @@ -0,0 +1,30 @@ +package foundation.e.blisslauncher.inject + +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjectionModule +import dagger.android.AndroidInjector +import foundation.e.blisslauncher.BlissLauncher +import foundation.e.blisslauncher.domain.inject.DomainComponent +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + AppModule::class, + AndroidInjectionModule::class, + ActivityBindsModule::class + ], + dependencies = [ + DomainComponent::class + ] +) +interface AppComponent : AndroidInjector { + @Component.Factory + interface Factory { + fun create( + @BindsInstance application: BlissLauncher, + domainComponent: DomainComponent + ): AppComponent + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5c26eb4464fd58c61f8f38e3fe1815622a04c98 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt @@ -0,0 +1,18 @@ +package foundation.e.blisslauncher.inject + +import android.content.Context +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.BlissLauncher +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.features.LauncherStore + +@Module +class AppModule { + + @Provides + fun provideContext(application: BlissLauncher): Context = application.applicationContext + + @Provides + fun provideIdp(context: Context): InvariantDeviceProfile = InvariantDeviceProfile(context) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4c2ec6f9019a32d7ab949115c0c4c4159fcd44c --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.inject + +import javax.inject.Qualifier + +@Qualifier +annotation class ThemeRef \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..d77aeea69fc06ba5eddc1eac7c04d2e5b8497a0e --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.touch + +import android.view.View +import android.view.View.OnLongClickListener + +class CheckLongPressHelper { + var mView: View + var mListener: OnLongClickListener? = null + var mHasPerformedLongPress = false + private var mLongPressTimeout = + DEFAULT_LONG_PRESS_TIMEOUT + private var mPendingCheckForLongPress: CheckForLongPress? = + null + + internal inner class CheckForLongPress : Runnable { + override fun run() { + if (mView.parent != null && mView.hasWindowFocus() + && !mHasPerformedLongPress + ) { + val handled: Boolean = if (mListener != null) { + mListener!!.onLongClick(mView) + } else { + mView.performLongClick() + } + if (handled) { + mView.isPressed = false + mHasPerformedLongPress = true + } + } + } + } + + constructor(v: View) { + mView = v + } + + constructor(v: View, listener: OnLongClickListener?) { + mView = v + mListener = listener + } + + /** + * Overrides the default long press timeout. + */ + fun setLongPressTimeout(longPressTimeout: Int) { + mLongPressTimeout = longPressTimeout + } + + fun postCheckForLongPress() { + mHasPerformedLongPress = false + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = + CheckForLongPress() + } + mView.postDelayed(mPendingCheckForLongPress, mLongPressTimeout.toLong()) + } + + fun cancelLongPress() { + mHasPerformedLongPress = false + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress) + mPendingCheckForLongPress = null + } + } + + fun hasPerformedLongPress(): Boolean { + return mHasPerformedLongPress + } + + companion object { + const val DEFAULT_LONG_PRESS_TIMEOUT = 300 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java new file mode 100644 index 0000000000000000000000000000000000000000..ebeede0ac151d89ef59b0d3743e965c65a3163bc --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java @@ -0,0 +1,40 @@ +package foundation.e.blisslauncher.touch; + +/** + * Utility methods for overscroll damping and related effect. + */ +public class OverScroll { + + private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; + + /** + * This curve determines how the effect of scrolling over the limits of the page diminishes + * as the user pulls further and further from the bounds + * + * @param f The percentage of how much the user has overscrolled. + * @return A transformed percentage based on the influence curve. + */ + private static float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + /** + * @param amount The original amount overscrolled. + * @param max The maximum amount that the View can overscroll. + * @return The dampened overscroll amount. + */ + public static int dampedScroll(float amount, int max) { + if (Float.compare(amount, 0) == 0) return 0; + + float f = amount / max; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + return Math.round(OVERSCROLL_DAMP_FACTOR * f * max); + } +} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt new file mode 100644 index 0000000000000000000000000000000000000000..e739ba7b0ff265475a30867040ce1c44b0b71731 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt @@ -0,0 +1,91 @@ +package foundation.e.blisslauncher.views + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.widget.GridLayout +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.LauncherConstants +import foundation.e.blisslauncher.features.launcher.LauncherActivity + +open class CellLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : GridLayout(context, attrs, defStyleAttr), Insettable { + + private val TAG = "CellLayout" + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + private val dp = launcher.deviceProfile + val countX = dp.inv.numColumns + val countY = dp.inv.numRows + + open var containerType = LauncherConstants.ContainerType.CONTAINER_DESKTOP + + private var cellWidth: Int = 0 + private var cellHeight: Int = 0 + + init { + setWillNotDraw(false) + clipToPadding = false + } + + override fun setInsets(insets: Rect) { + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + Log.d( + TAG, + "$this onMeasure() called with: widthSpec = $widthSpec, heightSpec = $heightSpec" + ) + val widthSpecMode = MeasureSpec.getMode(widthSpec) + val heightSpecMode = MeasureSpec.getMode(heightSpec) + val widthSize = MeasureSpec.getSize(widthSpec) + val heightSize = MeasureSpec.getSize(heightSpec) + val childWidthSize = widthSize - (paddingLeft + paddingRight) + val childHeightSize = heightSize - (paddingTop + paddingBottom) + cellWidth = DeviceProfile.calculateCellWidth(childWidthSize, countX) + cellHeight = DeviceProfile.calculateCellHeight(childHeightSize, countY) + Log.d(TAG, "cellWidth: $cellWidth") + setMeasuredDimension(widthSize, heightSize) + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility != View.GONE) { + measureChild(child) + } + } + } + + private fun measureChild(child: View) { + val lp = child.layoutParams as LayoutParams + lp.rowSpec = spec(UNDEFINED) + lp.columnSpec = spec(UNDEFINED) + lp.width = cellWidth + lp.height = cellHeight + // Center the icon/folder + val cHeight: Int = dp.cellHeightPx + val cellPaddingY = 0f.coerceAtLeast((lp.height - cHeight) / 2f).toInt() + var cellPaddingX: Int + if (containerType == LauncherConstants.ContainerType.CONTAINER_DESKTOP) { + cellPaddingX = dp.workspaceCellPaddingXPx + } else { + cellPaddingX = (dp.edgeMarginPx / 2f).toInt() + } + child.setPadding(cellPaddingX, cellPaddingY, cellPaddingX, 0) + val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY) + val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) + child.measure(childWidthMeasureSpec, childHeightMeasureSpec) + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + Log.d(TAG, "onViewAdded() called with: child = $child") + } + + fun addViewToCellLayout(child: View, index: Int, childId: Int, params: LayoutParams) { + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt new file mode 100644 index 0000000000000000000000000000000000000000..c6287565081aaca2b0827609b0c817d42cd605be --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt @@ -0,0 +1,90 @@ +package foundation.e.blisslauncher.views + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.TextUtils.TruncateAt +import android.util.TypedValue +import android.widget.TextView +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.ShortcutItem +import foundation.e.blisslauncher.features.launcher.LauncherActivity +import foundation.e.blisslauncher.graphics.DrawableFactory +import kotlin.math.ceil + +/** + * A text view which displays an icon on top side of it. + */ +@SuppressLint("AppCompatCustomView") +class IconTextView @JvmOverloads constructor(context: Context) : TextView(context) { + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + private val dp = launcher.deviceProfile + val defaultIconSize = dp.iconSizePx + + private var disableRelayout = false + private var mIcon: Drawable? = null + + init { + setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.iconTextSizePx.toFloat()) + compoundDrawablePadding = dp.iconDrawablePaddingPx + ellipsize = TruncateAt.END + } + + override fun onFocusChanged( + focused: Boolean, + direction: Int, + previouslyFocusedRect: Rect? + ) { + // Disable marques when not focused to that, so that updating text does not cause relayout. + ellipsize = if (focused) TruncateAt.MARQUEE else TruncateAt.END + super.onFocusChanged(focused, direction, previouslyFocusedRect) + } + + fun reset() {} + + fun applyFromShortcutItem(item: ShortcutItem) { + applyIconAndLabel(item) + tag = item + applyBadgeState(item, false) + } + + private fun applyIconAndLabel(item: LauncherItemWithIcon) { + val icon = DrawableFactory(context).newIcon(item) + disableRelayout = mIcon != null + icon.setBounds(0, 0, defaultIconSize, defaultIconSize) + setCompoundDrawables(null, icon, null, null) + disableRelayout = false + mIcon = icon + text = item.title + } + + private fun applyBadgeState(item: ShortcutItem, animate: Boolean) { + } + + override fun setTag(tag: Any?) { + if (tag != null) { + //TODO: Check Item info locked + } + super.setTag(tag) + } + + override fun requestLayout() { + if (!disableRelayout) { + super.requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val fm = paint.fontMetrics + val cellHeightPx: Int = defaultIconSize + compoundDrawablePadding + + ceil(fm.bottom - fm.top.toDouble()).toInt() + val height = MeasureSpec.getSize(heightMeasureSpec) + setPadding( + paddingLeft, (height - cellHeightPx) / 2, paddingRight, + paddingBottom + ) + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3238fb57c4fdc28a30e0fd210e87770f0806fa2 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.views + +import android.graphics.Rect +/** + * Allows the implementing [View] to not draw underneath system bars. + * e.g., notification bar on top and home key area on the bottom. + */ +interface Insettable { + fun setInsets(insets: Rect) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt new file mode 100644 index 0000000000000000000000000000000000000000..1392b1140a0b106d1aa2e462c0cb7d06b1d17b4e --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt @@ -0,0 +1,97 @@ +package foundation.e.blisslauncher.views + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.widget.FrameLayout +import foundation.e.blisslauncher.R +import timber.log.Timber + +class InsettableFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), Insettable { + + private val insets = Rect() + + override fun setInsets(newInsets: Rect) { + val n = childCount + for (i in 0 until n) { + val child = getChildAt(i) + setFrameLayoutChildInsets(child, insets, newInsets) + } + insets.set(insets) + } + + override fun onApplyWindowInsets(newInsets: WindowInsets): WindowInsets { + Timber.d("this is called") + insets.set(0, newInsets.systemWindowInsetTop, 0, newInsets.systemWindowInsetBottom) + setInsets(insets) + return newInsets + } + + fun setFrameLayoutChildInsets( + child: View, + newInsets: Rect, + oldInsets: Rect + ) { + val lp: LayoutParams = + child.layoutParams as LayoutParams + if (child is Insettable) { + (child as Insettable).setInsets(newInsets) + } else if (!lp.ignoreInsets) { + lp.topMargin += newInsets.top - oldInsets.top + lp.leftMargin += newInsets.left - oldInsets.left + lp.rightMargin += newInsets.right - oldInsets.right + lp.bottomMargin += newInsets.bottom - oldInsets.bottom + } + child.layoutParams = lp + } + + override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { + return LayoutParams(context, attrs) + } + + override fun generateDefaultLayoutParams(): LayoutParams { + return LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + // Override to allow type-checking of LayoutParams. + override fun checkLayoutParams(p: ViewGroup.LayoutParams): Boolean { + return p is LayoutParams + } + + override fun generateLayoutParams(p: ViewGroup.LayoutParams): LayoutParams { + return LayoutParams(p) + } + + class LayoutParams : FrameLayout.LayoutParams { + var ignoreInsets = false + + constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { + val a = c.obtainStyledAttributes( + attrs, + R.styleable.InsettableFrameLayout_Layout + ) + ignoreInsets = a.getBoolean( + R.styleable.InsettableFrameLayout_Layout_layout_ignoreInsets, false + ) + a.recycle() + } + + constructor(width: Int, height: Int) : super(width, height) {} + constructor(lp: ViewGroup.LayoutParams?) : super(lp) {} + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + setFrameLayoutChildInsets(child!!, insets, Rect()) + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff48ca2fc0ca21fb68e19ca93d9a1669be725cd8 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt @@ -0,0 +1,1353 @@ +package foundation.e.blisslauncher.views + +import android.animation.LayoutTransition +import android.animation.TimeInterpolator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Matrix +import android.graphics.Rect +import android.util.AttributeSet +import android.util.Log +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.widget.Scroller +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.touch.OverScroll +import foundation.e.blisslauncher.views.pageindicators.PageIndicatorDots +import java.util.ArrayList +import kotlin.math.sin + +typealias ComputePageScrollsLogic = (View) -> Boolean + +open class PagedView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr) { + + private var freeScroll = false + private var settleOnPageInFreeScroll = false + + private val flingThresholdVelocity: Int + private val minFlingVelocity: Int + private val minSnapVelocity: Int + + protected var firstLayout = true + + var currentPage = 0 + set(value) { + if (!scroller.isFinished) { + abortScrollerAnimation(true) + } + // don't introduce any checks like currentPage == currentPage here-- if we change the + // the default + if (childCount == 0) { + return + } + val prevPage: Int = currentPage + field = validateNewPage(value) + updateCurrentPageScroll() + notifyPageSwitchListener(prevPage) + invalidate() + } + + var nextPage: Int = INVALID_PAGE + get() { + return if (field != INVALID_PAGE) field else currentPage + } + + protected var maxScrollX = 0 + protected val scroller: Scroller = Scroller(context) + private var velocityTracker: VelocityTracker? = null + protected var pageSpacing = 0 + set(value) { + field = value + requestLayout() + } + + private var downMotionX = 0f + private var downMotionY = 0f + private var lastMotionX = 0f + private var lastMotionXRemainder = 0f + private var totalMotionX = 0f + + protected var pageScrolls: IntArray = IntArray(0) + + protected var touchState: Int = TOUCH_STATE_REST + + private val touchSlop: Int + private val maximumVelocity: Int + protected var mAllowOverScroll = true + + protected val INVALID_POINTER = -1 + + protected var activePointerId = INVALID_POINTER + + protected var isPageInTransition = false + + protected var wasInOverscroll = false + + protected var overScrollX = 0 + + protected var unboundedScrollX = 0 + + protected var pageIndicator: PageIndicatorDots? = null + + // Convenience/caching + private val sTmpInvMatrix = Matrix() + private val sTmpPoint = FloatArray(2) + private val sTmpRect = Rect() + + protected val insets = Rect() + protected var isRtl = false + + // Similar to the platform implementation of isLayoutValid(); + protected var mIsLayoutValid = false + + init { + isHapticFeedbackEnabled = false + currentPage = 0 + val configuration = ViewConfiguration.get(getContext()) + touchSlop = configuration.scaledPagingTouchSlop + maximumVelocity = configuration.scaledMaximumFlingVelocity + + val density = resources.displayMetrics.density + flingThresholdVelocity = (FLING_THRESHOLD_VELOCITY * density).toInt() + minFlingVelocity = (MIN_FLING_VELOCITY * density).toInt() + minSnapVelocity = (MIN_SNAP_VELOCITY * density).toInt() + } + + fun getPageCount() = childCount + + fun getPageAt(index: Int) = getChildAt(index) + + fun indexToPage(index: Int) = index + + fun scrollAndForceFinish(scrollX: Int) { + scrollTo(scrollX, 0) + scroller.finalX = scrollX + forceFinishScroller(true) + } + + private fun forceFinishScroller(resetNextPage: Boolean) { + scroller.forceFinished(true) + // We need to clean up the next page here to avoid computeScrollHelper from + // updating current page on the pass. + if (resetNextPage) { + nextPage = INVALID_PAGE + pageEndTransition() + } + } + + /** + * Updates the scroll of the current page immediately to its final scroll position. We use this + * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of + * the previous tab page. + */ + private fun updateCurrentPageScroll() { + // If the current page is invalid, just reset the scroll position to zero + var newX = 0 + if (0 <= currentPage && currentPage < getPageCount()) { + newX = getScrollForPage(currentPage) + } + scrollAndForceFinish(newX) + } + + private fun abortScrollerAnimation(resetNextPage: Boolean) { + scroller.abortAnimation() + // We need to clean up the next page here to avoid computeScrollHelper from + // updating current page on the pass. + if (resetNextPage) { + nextPage = INVALID_PAGE + pageEndTransition() + } + } + + private fun validateNewPage(newPage: Int): Int { + // Ensure that it is clamped by the actual set of children in all cases + return Utilities.boundToRange(newPage, 0, getPageCount() - 1) + } + + /** + * Should be called whenever the page changes. In the case of a scroll, we wait until the page + * has settled. + */ + protected fun notifyPageSwitchListener(prevPage: Int) { + updatePageIndicator() + } + + private fun updatePageIndicator() { + if (pageIndicator != null) { + pageIndicator!!.setActiveMarker(nextPage) + } + } + + protected fun pageBeginTransition() { + if (!isPageInTransition) { + isPageInTransition = true + onPageBeginTransition() + } + } + + protected fun pageEndTransition() { + if (isPageInTransition) { + isPageInTransition = false + onPageEndTransition() + } + } + + /** + * Called when the page starts moving as part of the scroll. Subclasses can override this + * to provide custom behavior during animation. + */ + protected fun onPageBeginTransition() {} + + /** + * Called when the page ends moving as part of the scroll. Subclasses can override this + * to provide custom behavior during animation. + */ + protected fun onPageEndTransition() { + wasInOverscroll = false + } + + override fun scrollBy(x: Int, y: Int) { + scrollTo(unboundedScrollX + x, scrollY + y) + } + + override fun scrollTo(x: Int, y: Int) { + // In free scroll mode, we clamp the scrollX + var x = x + if (freeScroll) { + // If the scroller is trying to move to a location beyond the maximum allowed + // in the free scroll mode, we make sure to end the scroll operation. + if (!scroller.isFinished && (x > maxScrollX || x < 0)) { + forceFinishScroller(false) + } + x = Utilities.boundToRange(x, 0, maxScrollX) + } + unboundedScrollX = x + val isXBeforeFirstPage = if (isRtl) x > maxScrollX else x < 0 + val isXAfterLastPage = if (isRtl) x < 0 else x > maxScrollX + if (isXBeforeFirstPage) { + super.scrollTo(if (isRtl) maxScrollX else 0, y) + if (mAllowOverScroll) { + wasInOverscroll = true + if (isRtl) { + overScroll(x - maxScrollX.toFloat()) + } else { + overScroll(x.toFloat()) + } + } + } else if (isXAfterLastPage) { + super.scrollTo(if (isRtl) 0 else maxScrollX, y) + if (mAllowOverScroll) { + wasInOverscroll = true + if (isRtl) { + overScroll(x.toFloat()) + } else { + overScroll(x - maxScrollX.toFloat()) + } + } + } else { + if (wasInOverscroll) { + overScroll(0f) + wasInOverscroll = false + } + overScrollX = x + super.scrollTo(x, y) + } + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected fun computeScrollHelper(): Boolean { + return computeScrollHelper(true) + } + + protected fun computeScrollHelper(shouldInvalidate: Boolean): Boolean { + if (scroller.computeScrollOffset()) { + // Don't bother scrolling if the page does not need to be moved + if (unboundedScrollX != scroller.getCurrX() || scrollY != scroller.getCurrY() || overScrollX != scroller.getCurrX() + ) { + scrollTo(scroller.getCurrX(), scroller.getCurrY()) + } + if (shouldInvalidate) { + invalidate() + } + return true + } else if (nextPage != INVALID_PAGE && shouldInvalidate) { + val prevPage: Int = currentPage + currentPage = validateNewPage(nextPage) + nextPage = INVALID_PAGE + notifyPageSwitchListener(prevPage) + + // We don't want to trigger a page end moving unless the page has settled + // and the user has stopped scrolling + if (touchState == TOUCH_STATE_REST) { + pageEndTransition() + } + } + return false + } + + override fun computeScroll() { + computeScrollHelper() + } + + fun getExpectedHeight(): Int { + return measuredHeight + } + + fun getNormalChildHeight(): Int { + return (getExpectedHeight() - paddingTop - paddingBottom - + insets.top - insets.bottom) + } + + fun getExpectedWidth(): Int { + return measuredWidth + } + + fun getNormalChildWidth(): Int { + return (getExpectedWidth() - paddingLeft - paddingRight - + insets.left - insets.right) + } + + override fun requestLayout() { + mIsLayoutValid = false + super.requestLayout() + } + + override fun forceLayout() { + mIsLayoutValid = false + super.forceLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (childCount == 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + // We measure the dimensions of the PagedView to be larger than the pages so that when we + // zoom out (and scale down), the view is still contained in the parent + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + // Return early if we aren't given a proper dimension + if (widthSize <= 0 || heightSize <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + Log.d( + TAG, + "PagedView.onMeasure(): $widthSize, $heightSize" + ) + val myWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - insets.left - insets.right, MeasureSpec.EXACTLY + ) + val myHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - insets.top - insets.bottom, MeasureSpec.EXACTLY + ) + + // measureChildren takes accounts for content padding, we only need to care about extra + // space due to insets. + measureChildren(myWidthSpec, myHeightSpec) + setMeasuredDimension(widthSize, heightSize) + } + + protected fun restoreScrollOnLayout() { + currentPage = nextPage + } + + @SuppressLint("DrawAllocation") + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + mIsLayoutValid = true + val childCount = childCount + var pageScrollChanged = false + if (childCount != pageScrolls.size) { + pageScrolls = IntArray(childCount) + pageScrollChanged = true + } + if (childCount == 0) { + return + } + Log.d(TAG, "PagedView.onLayout()") + if (getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC)) { + pageScrollChanged = true + } + val transition = layoutTransition + // If the transition is running defer updating max scroll, as some empty pages could + // still be present, and a max scroll change could cause sudden jumps in scroll. + if (transition != null && transition.isRunning) { + transition.addTransitionListener(object : LayoutTransition.TransitionListener { + override fun startTransition( + transition: LayoutTransition, + container: ViewGroup, + view: View, + transitionType: Int + ) { + } + + override fun endTransition( + transition: LayoutTransition, + container: ViewGroup, + view: View, + transitionType: Int + ) { + // Wait until all transitions are complete. + if (!transition.isRunning) { + transition.removeTransitionListener(this) + updateMaxScrollX() + } + } + }) + } else { + updateMaxScrollX() + } + if (firstLayout && currentPage >= 0 && currentPage < childCount) { + updateCurrentPageScroll() + firstLayout = false + } + if (scroller.isFinished() && pageScrollChanged) { + restoreScrollOnLayout() + } + } + + val SIMPLE_SCROLL_LOGIC: ComputePageScrollsLogic = { v -> v.visibility != View.GONE } + + /** + * Initializes `outPageScrolls` with scroll positions for view at that index. The length + * of `outPageScrolls` should be same as the the childCount + * + */ + protected fun getPageScrolls( + outPageScrolls: IntArray, + layoutChildren: Boolean, + scrollLogic: ComputePageScrollsLogic + ): Boolean { + val childCount = childCount + val startIndex = if (isRtl) childCount - 1 else 0 + val endIndex = if (isRtl) -1 else childCount + val delta = if (isRtl) -1 else 1 + val verticalCenter = + (paddingTop + measuredHeight + insets.top - insets.bottom - paddingBottom) / 2 + val scrollOffsetLeft = insets.left + paddingLeft + var pageScrollChanged = false + var i = startIndex + var childLeft = scrollOffsetLeft + offsetForPageScrolls() + while (i != endIndex) { + val child = getPageAt(i) + if (scrollLogic(child)) { + val childTop = verticalCenter - child.measuredHeight / 2 + val childWidth = child.measuredWidth + if (layoutChildren) { + val childHeight = child.measuredHeight + child.layout( + childLeft, childTop, + childLeft + child.measuredWidth, childTop + childHeight + ) + } + val pageScroll = childLeft - scrollOffsetLeft + if (outPageScrolls[i] != pageScroll) { + pageScrollChanged = true + outPageScrolls[i] = pageScroll + } + childLeft += childWidth + pageSpacing + getChildGap() + } + i += delta + } + return pageScrollChanged + } + + protected fun getChildGap(): Int { + return 0 + } + + private fun updateMaxScrollX() { + maxScrollX = computeMaxScrollX() + } + + protected fun computeMaxScrollX(): Int { + val childCount = childCount + return if (childCount > 0) { + val index = if (isRtl) 0 else childCount - 1 + getScrollForPage(index) + } else { + 0 + } + } + + protected fun offsetForPageScrolls(): Int { + return 0 + } + + private fun dispatchPageCountChanged() { + if (pageIndicator != null) { + pageIndicator!!.setMarkersCount(childCount) + } + // This ensures that when children are added, they get the correct transforms / alphas + // in accordance with any scroll effects. + invalidate() + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + dispatchPageCountChanged() + } + + override fun onViewRemoved(child: View?) { + super.onViewRemoved(child) + currentPage = validateNewPage(currentPage) + dispatchPageCountChanged() + } + + protected fun getChildOffset(index: Int): Int { + return if (index < 0 || index > childCount - 1) 0 else getPageAt(index).left + } + + override fun requestChildRectangleOnScreen( + child: View?, + rectangle: Rect?, + immediate: Boolean + ): Boolean { + val page = indexToPage(indexOfChild(child)) + if (page != currentPage || !scroller.isFinished()) { + if (immediate) { + currentPage = page + } else { + snapToPage(page) + } + return true + } + return false + } + + override fun onRequestFocusInDescendants( + direction: Int, + previouslyFocusedRect: Rect? + ): Boolean { + val focusablePage: Int + if (nextPage != INVALID_PAGE) { + focusablePage = nextPage + } else { + focusablePage = currentPage + } + val v = getPageAt(focusablePage) + return v?.requestFocus(direction, previouslyFocusedRect) ?: false + } + + override fun dispatchUnhandledMove(focused: View?, direction: Int): Boolean { + var direction = direction + if (super.dispatchUnhandledMove(focused, direction)) { + return true + } + if (isRtl) { + if (direction == View.FOCUS_LEFT) { + direction = View.FOCUS_RIGHT + } else if (direction == View.FOCUS_RIGHT) { + direction = View.FOCUS_LEFT + } + } + if (direction == View.FOCUS_LEFT) { + if (currentPage > 0) { + snapToPage(currentPage - 1) + return true + } + } else if (direction == View.FOCUS_RIGHT) { + if (currentPage < getPageCount() - 1) { + snapToPage(currentPage + 1) + return true + } + } + return false + } + + override fun addFocusables( + views: ArrayList?, + direction: Int, + focusableMode: Int + ) { + if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) { + return + } + + // XXX-RTL: This will be fixed in a future CL + if (currentPage >= 0 && currentPage < getPageCount()) { + getPageAt(currentPage).addFocusables(views, direction, focusableMode) + } + if (direction == View.FOCUS_LEFT) { + if (currentPage > 0) { + getPageAt(currentPage - 1).addFocusables(views, direction, focusableMode) + } + } else if (direction == View.FOCUS_RIGHT) { + if (currentPage < getPageCount() - 1) { + getPageAt(currentPage + 1).addFocusables(views, direction, focusableMode) + } + } + } + + /** + * If one of our descendant views decides that it could be focused now, only + * pass that along if it's on the current page. + * + * This happens when live folders requery, and if they're off page, they + * end up calling requestFocus, which pulls it on page. + */ + override fun focusableViewAvailable(focused: View) { + val current = getPageAt(currentPage) + var v = focused + while (true) { + if (v === current) { + super.focusableViewAvailable(focused) + return + } + if (v === this) { + return + } + val parent = v.parent + v = if (parent is View) { + v.parent as View + } else { + return + } + } + } + + /** + * {@inheritDoc} + */ + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + if (disallowIntercept) { + // We need to make sure to cancel our long press if + // a scrollable widget takes over touch events + val currentPage = getPageAt(currentPage) + currentPage.cancelLongPress() + } + super.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + /** Returns whether x and y originated within the buffered viewport */ + private fun isTouchPointInViewportWithBuffer(x: Int, y: Int): Boolean { + sTmpRect.set( + -measuredWidth / 2, + 0, + 3 * measuredWidth / 2, + measuredHeight + ) + return sTmpRect.contains(x, y) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + acquireVelocityTrackerAndAddMovement(ev) + + // Skip touch handling if there are no pages to swipe + if (childCount <= 0) return super.onInterceptTouchEvent(ev) + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + val action = ev.action + if (action == MotionEvent.ACTION_MOVE && + touchState == TOUCH_STATE_SCROLLING + ) { + return true + } + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_MOVE -> { + + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */if (activePointerId != INVALID_POINTER) { + determineScrollingStart(ev) + } + } + MotionEvent.ACTION_DOWN -> { + val x = ev.x + val y = ev.y + // Remember location of down touch + downMotionX = x + downMotionY = y + lastMotionX = x + lastMotionXRemainder = 0f + totalMotionX = 0f + activePointerId = ev.getPointerId(0) + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. scroller.isFinished should be false when + * being flinged. + */ + val xDist: Int = Math.abs(scroller.getFinalX() - scroller.getCurrX()) + val finishedScrolling = + scroller.isFinished() || xDist < touchSlop / 3 + if (finishedScrolling) { + touchState = TOUCH_STATE_REST + if (!scroller.isFinished() && !freeScroll) { + currentPage = nextPage + pageEndTransition() + } + } else { + touchState = if (isTouchPointInViewportWithBuffer( + downMotionX.toInt(), + downMotionY.toInt() + ) + ) { + TOUCH_STATE_SCROLLING + } else { + TOUCH_STATE_REST + } + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> resetTouchState() + MotionEvent.ACTION_POINTER_UP -> { + onSecondaryPointerUp(ev) + releaseVelocityTracker() + } + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */return touchState != TOUCH_STATE_REST + } + + fun isHandlingTouch(): Boolean { + return touchState != TOUCH_STATE_REST + } + + fun determineScrollingStart(ev: MotionEvent) { + determineScrollingStart(ev, 1.0f) + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected fun determineScrollingStart( + ev: MotionEvent, + touchSlopScale: Float + ) { + // Disallow scrolling if we don't have a valid pointer index + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex == -1) return + + // Disallow scrolling if we started the gesture from outside the viewport + val x = ev.getX(pointerIndex) + val y = ev.getY(pointerIndex) + if (!isTouchPointInViewportWithBuffer(x.toInt(), y.toInt())) return + val xDiff = Math.abs(x - lastMotionX).toInt() + val touchSlop = Math.round(touchSlopScale * touchSlop).toInt() + val xMoved = xDiff > touchSlop + if (xMoved) { + // Scroll if the user moved far enough along the X axis + touchState = TOUCH_STATE_SCROLLING + totalMotionX += Math.abs(lastMotionX - x) + lastMotionX = x + lastMotionXRemainder = 0f + onScrollInteractionBegin() + pageBeginTransition() + // Stop listening for things like pinches. + requestDisallowInterceptTouchEvent(true) + } + } + + protected fun cancelCurrentPageLongPress() { + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + val currentPage = getPageAt(currentPage) + currentPage?.cancelLongPress() + } + + protected fun getScrollProgress( + screenCenter: Int, + v: View, + page: Int + ): Float { + val halfScreenSize = measuredWidth / 2 + val delta = screenCenter - (getScrollForPage(page) + halfScreenSize) + val count = childCount + val totalDistance: Int + var adjacentPage = page + 1 + if (delta < 0 && !isRtl || delta > 0 && isRtl) { + adjacentPage = page - 1 + } + totalDistance = if (adjacentPage < 0 || adjacentPage > count - 1) { + v.measuredWidth + pageSpacing + } else { + Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)) + } + var scrollProgress = delta / (totalDistance * 1.0f) + scrollProgress = + Math.min(scrollProgress, MAX_SCROLL_PROGRESS) + scrollProgress = + Math.max(scrollProgress, -MAX_SCROLL_PROGRESS) + return scrollProgress + } + + fun getScrollForPage(index: Int): Int { + return if (index >= pageScrolls.size || index < 0) { + 0 + } else { + pageScrolls[index] + } + } + + // While layout transitions are occurring, a child's position may stray from its baseline + // position. This method returns the magnitude of this stray at any given time. + fun getLayoutTransitionOffsetForPage(index: Int): Int { + return if (index >= pageScrolls.size || index < 0) { + 0 + } else { + val child = getChildAt(index) + val scrollOffset = if (isRtl) paddingRight else paddingLeft + val baselineX = pageScrolls[index] + scrollOffset + (child.x - baselineX).toInt() + } + } + + protected fun dampedOverScroll(amount: Float) { + if (java.lang.Float.compare(amount, 0f) == 0) return + val overScrollAmount: Int = OverScroll.dampedScroll(amount, measuredWidth) + if (amount < 0) { + overScrollX = overScrollAmount + super.scrollTo(overScrollX, scrollY) + } else { + overScrollX = maxScrollX + overScrollAmount + super.scrollTo(overScrollX, scrollY) + } + invalidate() + } + + protected fun overScroll(amount: Float) { + dampedOverScroll(amount) + } + + protected fun enableFreeScroll(settleOnPageInFreeScroll: Boolean) { + setEnableFreeScroll(true) + this.settleOnPageInFreeScroll = settleOnPageInFreeScroll + } + + private fun setEnableFreeScroll(freeScroll: Boolean) { + val wasFreeScroll = this.freeScroll + this.freeScroll = freeScroll + if (this.freeScroll) { + currentPage = nextPage + } else if (wasFreeScroll) { + snapToPage(nextPage) + } + setEnableOverscroll(!freeScroll) + } + + protected fun setEnableOverscroll(enable: Boolean) { + mAllowOverScroll = enable + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + super.onTouchEvent(ev) + + // Skip touch handling if there are no pages to swipe + if (childCount <= 0) return super.onTouchEvent(ev) + acquireVelocityTrackerAndAddMovement(ev) + val action = ev.action + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */if (!scroller.isFinished()) { + abortScrollerAnimation(false) + } + run { + lastMotionX = ev.x + downMotionX = lastMotionX + } + downMotionY = ev.y + lastMotionXRemainder = 0f + totalMotionX = 0f + activePointerId = ev.getPointerId(0) + if (touchState == TOUCH_STATE_SCROLLING) { + onScrollInteractionBegin() + pageBeginTransition() + } + } + MotionEvent.ACTION_MOVE -> if (touchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex == -1) return true + val x = ev.getX(pointerIndex) + val deltaX = lastMotionX + lastMotionXRemainder - x + totalMotionX += Math.abs(deltaX) + + // Only scroll and update mLastMotionX if we have moved some discrete amount. We + // keep the remainder because we are actually testing if we've moved from the last + // scrolled position (which is discrete). + if (Math.abs(deltaX) >= 1.0f) { + scrollBy(deltaX.toInt(), 0) + lastMotionX = x + lastMotionXRemainder = deltaX - deltaX.toInt() + } else { + awakenScrollBars() + } + } else { + determineScrollingStart(ev) + } + MotionEvent.ACTION_UP -> { + if (touchState == TOUCH_STATE_SCROLLING) { + val activePointerId = activePointerId + val pointerIndex = ev.findPointerIndex(activePointerId) + val x = ev.getX(pointerIndex) + val velocityTracker = velocityTracker!! + velocityTracker.computeCurrentVelocity(1000, maximumVelocity.toFloat()) + val velocityX = velocityTracker.getXVelocity(activePointerId).toInt() + val deltaX = (x - downMotionX).toInt() + val pageWidth = getPageAt(currentPage).measuredWidth + val isSignificantMove: Boolean = Math.abs(deltaX) > pageWidth * + SIGNIFICANT_MOVE_THRESHOLD + totalMotionX += Math.abs(lastMotionX + lastMotionXRemainder - x) + val isFling = + totalMotionX > touchSlop && shouldFlingForVelocity(velocityX) + if (!freeScroll) { + // In the case that the page is moved far to one direction and then is flung + // in the opposite direction, we use a threshold to determine whether we should + // just return to the starting page, or if we should skip one further. + var returnToOriginalPage = false + if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && Math.signum( + velocityX.toFloat() + ) != Math.signum( + deltaX.toFloat() + ) && isFling + ) { + returnToOriginalPage = true + } + val finalPage: Int + // We give flings precedence over large moves, which is why we short-circuit our + // test for a large move if a fling has been registered. That is, a large + // move to the left and fling to the right will register as a fling to the right. + val isDeltaXLeft = if (isRtl) deltaX > 0 else deltaX < 0 + val isVelocityXLeft = + if (isRtl) velocityX > 0 else velocityX < 0 + if ((isSignificantMove && !isDeltaXLeft && !isFling || + isFling && !isVelocityXLeft) && currentPage > 0 + ) { + finalPage = if (returnToOriginalPage) currentPage else currentPage - 1 + snapToPageWithVelocity(finalPage, velocityX) + } else if ((isSignificantMove && isDeltaXLeft && !isFling || + isFling && isVelocityXLeft) && + currentPage < childCount - 1 + ) { + finalPage = if (returnToOriginalPage) currentPage else currentPage + 1 + snapToPageWithVelocity(finalPage, velocityX) + } else { + snapToDestination() + } + } else { + if (!scroller.isFinished()) { + abortScrollerAnimation(true) + } + val scaleX = scaleX + val vX = (-velocityX * scaleX).toInt() + val initialScrollX = (scrollX * scaleX).toInt() + scroller.fling( + initialScrollX, + scrollY, + vX, + 0, + Int.MIN_VALUE, + Int.MAX_VALUE, + 0, + 0 + ) + val unscaledScrollX = (scroller.getFinalX() / scaleX) as Int + nextPage = getPageNearestToCenterOfScreen(unscaledScrollX) + val firstPageScroll = + getScrollForPage(if (!isRtl) 0 else getPageCount() - 1) + val lastPageScroll = + getScrollForPage(if (!isRtl) getPageCount() - 1 else 0) + if (settleOnPageInFreeScroll && unscaledScrollX > 0 && unscaledScrollX < maxScrollX + ) { + // If scrolling ends in the half of the added space that is closer to the + // end, settle to the end. Otherwise snap to the nearest page. + // If flinging past one of the ends, don't change the velocity as it will + // get stopped at the end anyway. + val finalX = + if (unscaledScrollX < firstPageScroll / 2) 0 else if (unscaledScrollX > (lastPageScroll + maxScrollX) / 2) maxScrollX else getScrollForPage( + nextPage + ) + scroller.setFinalX((finalX * getScaleX()).toInt()) + // Ensure the scroll/snap doesn't happen too fast; + val extraScrollDuration: Int = + (OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - + scroller.getDuration()) + if (extraScrollDuration > 0) { + scroller.extendDuration(extraScrollDuration) + } + } + invalidate() + } + onScrollInteractionEnd() + } else if (touchState == TOUCH_STATE_PREV_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + val nextPage = Math.max(0, currentPage - 1) + if (nextPage != currentPage) { + snapToPage(nextPage) + } else { + snapToDestination() + } + } else if (touchState == TOUCH_STATE_NEXT_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + val nextPage = Math.min(childCount - 1, currentPage + 1) + if (nextPage != currentPage) { + snapToPage(nextPage) + } else { + snapToDestination() + } + } + + // End any intermediate reordering states + resetTouchState() + } + MotionEvent.ACTION_CANCEL -> { + if (touchState == TOUCH_STATE_SCROLLING) { + snapToDestination() + onScrollInteractionEnd() + } + resetTouchState() + } + MotionEvent.ACTION_POINTER_UP -> { + onSecondaryPointerUp(ev) + releaseVelocityTracker() + } + } + return true + } + + protected fun shouldFlingForVelocity(velocityX: Int): Boolean { + return Math.abs(velocityX) > flingThresholdVelocity + } + + private fun resetTouchState() { + releaseVelocityTracker() + touchState = TOUCH_STATE_REST + activePointerId = INVALID_POINTER + } + + /** + * Triggered by scrolling via touch + */ + protected fun onScrollInteractionBegin() {} + + protected fun onScrollInteractionEnd() {} + + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + when (event.action) { + MotionEvent.ACTION_SCROLL -> { + + // Handle mouse (or ext. device) by shifting the page depending on the scroll + val vscroll: Float + val hscroll: Float + if (event.metaState and KeyEvent.META_SHIFT_ON != 0) { + vscroll = 0f + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL) + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) + } + if (hscroll != 0f || vscroll != 0f) { + val isForwardScroll = + if (isRtl) hscroll < 0 || vscroll < 0 else hscroll > 0 || vscroll > 0 + if (isForwardScroll) { + scrollRight() + } else { + scrollLeft() + } + return true + } + } + } + } + return super.onGenericMotionEvent(event) + } + + private fun acquireVelocityTrackerAndAddMovement(ev: MotionEvent) { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker!!.addMovement(ev) + } + + private fun releaseVelocityTracker() { + if (velocityTracker != null) { + velocityTracker!!.clear() + velocityTracker!!.recycle() + velocityTracker = null + } + } + + private fun onSecondaryPointerUp(ev: MotionEvent) { + val pointerIndex = + ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK shr + MotionEvent.ACTION_POINTER_INDEX_SHIFT + val pointerId = ev.getPointerId(pointerIndex) + if (pointerId == activePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + val newPointerIndex = if (pointerIndex == 0) 1 else 0 + downMotionX = ev.getX(newPointerIndex) + lastMotionX = downMotionX + lastMotionXRemainder = 0f + activePointerId = ev.getPointerId(newPointerIndex) + if (velocityTracker != null) { + velocityTracker!!.clear() + } + } + } + + override fun requestChildFocus( + child: View?, + focused: View? + ) { + super.requestChildFocus(child, focused) + val page = indexToPage(indexOfChild(child)) + if (page >= 0 && page != currentPage && !isInTouchMode) { + snapToPage(page) + } + } + + fun getPageNearestToCenterOfScreen(): Int { + return getPageNearestToCenterOfScreen(scrollX) + } + + private fun getPageNearestToCenterOfScreen(scaledScrollX: Int): Int { + val screenCenter = scaledScrollX + measuredWidth / 2 + var minDistanceFromScreenCenter = Int.MAX_VALUE + var minDistanceFromScreenCenterIndex = -1 + val childCount = childCount + for (i in 0 until childCount) { + val layout = getPageAt(i) + val childWidth = layout.measuredWidth + val halfChildWidth = childWidth / 2 + val childCenter: Int = getChildOffset(i) + halfChildWidth + val distanceFromScreenCenter = Math.abs(childCenter - screenCenter) + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter + minDistanceFromScreenCenterIndex = i + } + } + return minDistanceFromScreenCenterIndex + } + + protected fun snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()) + } + + protected fun isInOverScroll(): Boolean { + return overScrollX > maxScrollX || overScrollX < 0 + } + + protected fun getPageSnapDuration(): Int { + return if (isInOverScroll()) { + OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION + } else PAGE_SNAP_ANIMATION_DURATION + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + private fun distanceInfluenceForSnapDuration(f: Float): Float { + var f = f + f -= 0.5f // center the values about 0. + f *= (0.3f * Math.PI / 2.0f).toFloat() + return sin(f.toDouble()).toFloat() + } + + protected fun snapToPageWithVelocity(whichPage: Int, velocity: Int): Boolean { + var whichPage = whichPage + var velocity = velocity + whichPage = validateNewPage(whichPage) + val halfScreenSize = measuredWidth / 2 + val newX: Int = getScrollForPage(whichPage) + val delta: Int = newX - unboundedScrollX + var duration = 0 + if (Math.abs(velocity) < minFlingVelocity) { + // If the velocity is low enough, then treat this more as an automatic page advance + // as opposed to an apparent physical response to flinging + return snapToPage( + whichPage, + PAGE_SNAP_ANIMATION_DURATION + ) + } + + // Here we compute a "distance" that will be used in the computation of the overall + // snap duration. This is a function of the actual distance that needs to be traveled; + // we keep this value close to half screen size in order to reduce the variance in snap + // duration as a function of the distance the page needs to travel. + val distanceRatio = + Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)) + val distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio) + velocity = Math.abs(velocity) + velocity = Math.max(minSnapVelocity, velocity) + + // we want the page's snap velocity to approximately match the velocity at which the + // user flings, so we scale the duration by a value near to the derivative of the scroll + // interpolator at zero, ie. 5. We use 4 to make it a little slower. + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)) + return snapToPage(whichPage, delta, duration) + } + + fun snapToPage(whichPage: Int): Boolean { + return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION) + } + + fun snapToPageImmediately(whichPage: Int): Boolean { + return snapToPage( + whichPage, + PAGE_SNAP_ANIMATION_DURATION, + true, + null + ) + } + + fun snapToPage(whichPage: Int, duration: Int): Boolean { + return snapToPage(whichPage, duration, false, null) + } + + fun snapToPage( + whichPage: Int, + duration: Int, + interpolator: TimeInterpolator? + ): Boolean { + return snapToPage(whichPage, duration, false, interpolator) + } + + protected fun snapToPage( + whichPage: Int, + duration: Int, + immediate: Boolean, + interpolator: TimeInterpolator? + ): Boolean { + var whichPage = whichPage + whichPage = validateNewPage(whichPage) + val newX: Int = getScrollForPage(whichPage) + val delta: Int = newX - unboundedScrollX + return snapToPage(whichPage, delta, duration, immediate, interpolator) + } + + protected fun snapToPage(whichPage: Int, delta: Int, duration: Int): Boolean { + return snapToPage(whichPage, delta, duration, false, null) + } + + protected fun snapToPage( + whichPage: Int, + delta: Int, + duration: Int, + immediate: Boolean, + interpolator: TimeInterpolator? + ): Boolean { + var whichPage = whichPage + var duration = duration + if (firstLayout) { + currentPage = whichPage + return false + } + whichPage = validateNewPage(whichPage) + nextPage = whichPage + awakenScrollBars(duration) + if (immediate) { + duration = 0 + } else if (duration == 0) { + duration = Math.abs(delta) + } + if (duration != 0) { + pageBeginTransition() + } + if (!scroller.isFinished()) { + abortScrollerAnimation(false) + } + scroller.startScroll(unboundedScrollX, 0, delta, 0, duration) + updatePageIndicator() + + // Trigger a compute() to finish switching pages if necessary + if (immediate) { + computeScroll() + pageEndTransition() + } + invalidate() + return Math.abs(delta) > 0 + } + + fun scrollLeft(): Boolean { + if (nextPage > 0) { + snapToPage(nextPage - 1) + return true + } + return false + } + + fun scrollRight(): Boolean { + if (nextPage < childCount - 1) { + snapToPage(nextPage + 1) + return true + } + return false + } + + companion object { + private const val TAG = "PagedView" + + const val INVALID_PAGE = -1 + + const val PAGE_SNAP_ANIMATION_DURATION = 750 + private const val OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270 + + private const val RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f + + // Move to next page on touch up if page is moved more than this threshold + private const val SIGNIFICANT_MOVE_THRESHOLD = 0.4f + + private const val MAX_SCROLL_PROGRESS = 1.0f + + // Scaled based on density + private const val FLING_THRESHOLD_VELOCITY = 500 + private const val MIN_SNAP_VELOCITY = 1500 + private const val MIN_FLING_VELOCITY = 250 + + const val TOUCH_STATE_REST = 0 + const val TOUCH_STATE_SCROLLING = 1 + const val TOUCH_STATE_PREV_PAGE = 2 + const val TOUCH_STATE_NEXT_PAGE = 3 + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt similarity index 62% rename from app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt index d75d0800112b731871d64206554ccbcac8f585d6..cd4af6494cd16bd2d10fb3c5ed84e7c4a5470044 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt @@ -1,7 +1,7 @@ -package foundation.e.blisslauncher.core.customviews.pageindicators +package foundation.e.blisslauncher.views.pageindicators /** - * Base class for a page indicator. + * Interface for a page indicator. */ interface PageIndicator { fun setScroll(currentScroll: Int, totalScroll: Int) diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt similarity index 76% rename from app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt index 1272c8c71e861fa57caeccc237e4234f80dd5879..e3f2e1b38bacf6ac65c101776228ea3e19475bf1 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.core.customviews.pageindicators +package foundation.e.blisslauncher.views.pageindicators import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -23,15 +23,21 @@ import kotlin.math.abs * [PageIndicator] which shows dots per page. The active page is shown with the current * accent color. */ -class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : - View(context, attrs, defStyleAttr), PageIndicator { - private val mCirclePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) - private val mDotRadius: Float - private val mActiveColor: Int - private val mInActiveColor: Int - private val mIsRtl: Boolean = false - private var mNumPages = 0 - private var mActivePage = 0 +class PageIndicatorDots @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), PageIndicator { + private val circlePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val dotRadius: Float + private val activeColor: Int + private val inActiveColor: Int + private val isRtl: Boolean = false + private val sTempRect: RectF = RectF() + + private var numPages = 0 + private var activePage = 0 + /** * The current position of the active dot including the animation progress. * For ex: @@ -46,17 +52,23 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I private var mAnimator: ObjectAnimator? = null private var mEntryAnimationRadiusFactors: FloatArray? = null - constructor(context: Context?) : this(context, null) - constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) + init { + circlePaint.style = Paint.Style.FILL + dotRadius = resources.getDimension(R.dimen.dotSize) / 2 + outlineProvider = MyOutlineProver() + activeColor = resources.getColor(R.color.dot_on_color) + inActiveColor = resources.getColor(R.color.dot_on_color) + //mIsRtl = Utilities.isRtl(getResources()) + } override fun setScroll(currentScroll: Int, totalScroll: Int) { var currentScroll = currentScroll - if (mNumPages > 1) { + if (numPages > 1) { // Ignore this as of now. - if (mIsRtl) { + if (isRtl) { currentScroll = totalScroll - currentScroll } - val scrollPerPage = totalScroll / (mNumPages - 1) + val scrollPerPage = totalScroll / (numPages - 1) val pageToLeft = currentScroll / scrollPerPage val pageToLeftScroll = pageToLeft * scrollPerPage val pageToRightScroll = pageToLeftScroll + scrollPerPage @@ -83,12 +95,9 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } if (mAnimator == null && mCurrentPosition.compareTo(mFinalPosition) != 0) { val positionForThisAnim = - if (mCurrentPosition > mFinalPosition) mCurrentPosition - SHIFT_PER_ANIMATION else mCurrentPosition + SHIFT_PER_ANIMATION - mAnimator = ObjectAnimator.ofFloat( - this, - CURRENT_POSITION, - positionForThisAnim - ).apply { + if (mCurrentPosition > mFinalPosition) mCurrentPosition - SHIFT_PER_ANIMATION + else mCurrentPosition + SHIFT_PER_ANIMATION + mAnimator = ObjectAnimator.ofFloat(this, CURRENT_POSITION, positionForThisAnim).apply { addListener(AnimationCycleListener()) duration = ANIMATION_DURATION } @@ -101,7 +110,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I mAnimator!!.cancel() mAnimator = null } - mFinalPosition = mActivePage.toFloat() + mFinalPosition = activePage.toFloat() CURRENT_POSITION.set(this, mFinalPosition) } @@ -110,7 +119,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I * [.playEntryAnimation] must be called after this. */ fun prepareEntryAnimation() { - mEntryAnimationRadiusFactors = FloatArray(mNumPages) + mEntryAnimationRadiusFactors = FloatArray(numPages) invalidate() } @@ -148,59 +157,62 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } override fun setActiveMarker(activePage: Int) { - if (mActivePage != activePage) { - mActivePage = activePage + if (this.activePage != activePage) { + this.activePage = activePage } } override fun setMarkersCount(numMarkers: Int) { - mNumPages = numMarkers + numPages = numMarkers requestLayout() } override fun onMeasure( widthMeasureSpec: Int, heightMeasureSpec: Int - ) { // Add extra spacing of mDotRadius on all sides so that entry animation could be run. + ) { + // Add extra spacing of mDotRadius on all sides so that entry animation could be run. val width = if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) MeasureSpec.getSize( widthMeasureSpec - ) else ((mNumPages * 3 + 2) * mDotRadius).toInt() + ) else ((numPages * 3 + 2) * dotRadius).toInt() val height = if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) MeasureSpec.getSize( heightMeasureSpec - ) else (4 * mDotRadius).toInt() + ) else (4 * dotRadius).toInt() setMeasuredDimension(width, height) } - override fun onDraw(canvas: Canvas) { // Draw all page indicators; - var circleGap = 3 * mDotRadius - val startX = (width - mNumPages * circleGap + mDotRadius) / 2 - var x = startX + mDotRadius + override fun onDraw(canvas: Canvas) { + // Draw all page indicators; + var circleGap = 3 * dotRadius + val startX = (width - numPages * circleGap + dotRadius) / 2 + var x = startX + dotRadius val y = height / 2.toFloat() - if (mEntryAnimationRadiusFactors != null) { // During entry animation, only draw the circles - if (mIsRtl) { + if (mEntryAnimationRadiusFactors != null) { + // During entry animation, only draw the circles + if (isRtl) { x = getWidth() - x circleGap = -circleGap } for (i in mEntryAnimationRadiusFactors!!.indices) { - mCirclePaint.setColor(if (i == mActivePage) mActiveColor else mInActiveColor) + circlePaint.setColor(if (i == activePage) activeColor else inActiveColor) canvas.drawCircle( x, y, - mDotRadius * mEntryAnimationRadiusFactors!![i], - mCirclePaint + dotRadius * mEntryAnimationRadiusFactors!![i], + circlePaint ) x += circleGap } } else { - mCirclePaint.color = mInActiveColor - for (i in 0 until mNumPages) { - canvas.drawCircle(x, y, mDotRadius, mCirclePaint) + circlePaint.color = inActiveColor + for (i in 0 until numPages) { + canvas.drawCircle(x, y, dotRadius, circlePaint) x += circleGap } - mCirclePaint.setColor(mActiveColor) - canvas.drawRoundRect(activeRect, mDotRadius, mDotRadius, mCirclePaint) + circlePaint.color = activeColor + canvas.drawRoundRect(activeRect, dotRadius, dotRadius, circlePaint) } } // Dot is leaving the left circle. @@ -209,11 +221,11 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I get() { val startCircle: Float = mCurrentPosition var delta = mCurrentPosition - startCircle - val diameter = 2 * mDotRadius - val circleGap = 3 * mDotRadius - val startX = (width - mNumPages * circleGap + mDotRadius) / 2 - sTempRect!!.top = height * 0.5f - mDotRadius - sTempRect.bottom = height * 0.5f + mDotRadius + val diameter = 2 * dotRadius + val circleGap = 3 * dotRadius + val startX = (width - numPages * circleGap + dotRadius) / 2 + sTempRect!!.top = height * 0.5f - dotRadius + sTempRect.bottom = height * 0.5f + dotRadius sTempRect.left = startX + startCircle * circleGap sTempRect.right = sTempRect.left + diameter @@ -224,7 +236,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I delta -= SHIFT_PER_ANIMATION sTempRect.left += delta * circleGap * 2 } - if (mIsRtl) { + if (isRtl) { val rectWidth = sTempRect.width() sTempRect.right = width - sTempRect.left @@ -243,7 +255,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I activeRect.top.toInt(), activeRect.right.toInt(), activeRect.bottom.toInt(), - mDotRadius + dotRadius ) } } @@ -273,9 +285,9 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I private const val ENTER_ANIMATION_START_DELAY = 300 private const val ENTER_ANIMATION_STAGGERED_DELAY = 150 private const val ENTER_ANIMATION_DURATION = 400 + // This value approximately overshoots to 1.5 times the original size. private const val ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f - private val sTempRect: RectF? = RectF() private val CURRENT_POSITION: Property = object : Property( Float::class.java, "current_position" @@ -291,13 +303,4 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } } } - - init { - mCirclePaint.style = Paint.Style.FILL - mDotRadius = resources.getDimension(R.dimen.dotSize) / 2 - outlineProvider = MyOutlineProver() - mActiveColor = resources.getColor(R.color.dot_on_color) - mInActiveColor = resources.getColor(R.color.dot_on_color) - //mIsRtl = Utilities.isRtl(getResources()) - } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml b/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000000000000000000000000000000000..1f6bb290603d7caa16c5fb6f61bbfdc750622f5c --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml b/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..4150bf8dc676f7d4b0096878fd45ab82974feb0d --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml b/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d025f9bf6b67c63044a36a9ff44fbc69e5c5822 --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blisslauncherv2/src/main/res/layout/activity_launcher.xml b/blisslauncherv2/src/main/res/layout/activity_launcher.xml new file mode 100644 index 0000000000000000000000000000000000000000..ee197b7fc3fb2997f8dbdda1bf88ffbebdcd13b0 --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/activity_launcher.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/layout/cell_view.xml b/blisslauncherv2/src/main/res/layout/cell_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..352d797925099d8e8c0e18a694cdcbd144a7286b --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/cell_view.xml @@ -0,0 +1,4 @@ + + diff --git a/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml new file mode 100644 index 0000000000000000000000000000000000000000..516a786d909f3e5f7637dd7a66c53b3232b463dc --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000000000000000000000000000000000..eca70cfe52eac1ba66ba280a68ca7be8fcf88a16 --- /dev/null +++ b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000000000000000000000000000000000..eca70cfe52eac1ba66ba280a68ca7be8fcf88a16 --- /dev/null +++ b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..898f3ed59ac9f3248734a00e5902736c9367d455 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dffca3601eba7bf5f409bdd520820e2eb5122c75 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..64ba76f75e9ce021aa3d95c213491f73bcacb597 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dae5e082342fcdeee5db8a6e0b27028e2d2808f5 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e5ed46597ea8447d91ab1786a34e30f1c26b18bd Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..14ed0af35023e4f1901cf03487b6c524257b8483 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b0907cac3bfd8fbfdc46e1108247f0a1055387ec Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ae03154975f397f8ed1b84f2d4bf9783ecfa26 Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c18de9e66108411737e910f5c1972476f03ddbf Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..beed3cdd2c32af5114a7dc70b9ef5b698eb8797e Binary files /dev/null and b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/blisslauncherv2/src/main/res/values/attrs.xml b/blisslauncherv2/src/main/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7ad834d3f54f7cb4a8df85026b327fa01bc368f --- /dev/null +++ b/blisslauncherv2/src/main/res/values/attrs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/colors.xml b/blisslauncherv2/src/main/res/values/colors.xml new file mode 100644 index 0000000000000000000000000000000000000000..f6fbd75afb1631ce71a171d3a4847f277b121a2e --- /dev/null +++ b/blisslauncherv2/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #008577 + #00574B + #D81B60 + #88FFFFFF + #FFFFFFFF + diff --git a/blisslauncherv2/src/main/res/values/dimens.xml b/blisslauncherv2/src/main/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..fe49c304adf1f703a15d289d85450a1f377e51c9 --- /dev/null +++ b/blisslauncherv2/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 8dp + 24dp + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/strings.xml b/blisslauncherv2/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..45446b00bd726c55eaf621a3e62f5574b4928dde --- /dev/null +++ b/blisslauncherv2/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + BlissLauncherV2 + Downloaded app disabled in Safe mode + App isn\'t installed + Default Scroll + diff --git a/blisslauncherv2/src/main/res/values/styles.xml b/blisslauncherv2/src/main/res/values/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..10e52c84348479d51fa4c978e6bfc531cdbb7274 --- /dev/null +++ b/blisslauncherv2/src/main/res/values/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/build.gradle b/build.gradle deleted file mode 100755 index b16585da3cd92cc633a807572210d032cffcbd41..0000000000000000000000000000000000000000 --- a/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -import foundation.e.blisslauncher.buildsrc.Libs - -buildscript { - ext.kotlin_version = '1.3.61' - repositories { - google() - jcenter() - maven { - url 'https://maven.fabric.io/public' - } - } - dependencies { - classpath Libs.androidGradlePlugin - - classpath Libs.Kotlin.gradlePlugin - classpath Libs.Kotlin.extensions - - classpath Libs.Google.fabricPlugin - classpath Libs.Google.gmsGoogleServices - - classpath Libs.dexcountGradlePlugin - } -} - -plugins { - id "com.diffplug.gradle.spotless" version "3.14.0" - id 'com.github.ben-manes.versions' version "0.25.0" -} - -allprojects { - repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } - mavenCentral() - } -} - -subprojects { - apply plugin: 'com.diffplug.gradle.spotless' - spotless { - java { - target '**/*.java' - removeUnusedImports() // removes any unused imports - } - kotlin { - target "**/*.kt" - ktlint() - } - kotlinGradle { - // same as kotlin, but for .gradle.kts files (defaults to '*.gradle.kts') - target '*.gradle.kts', 'additionalScripts/*.gradle.kts' - ktlint() - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100755 index 0000000000000000000000000000000000000000..d6d16769ab4fe00db163f541b2e94e80e0229d04 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +buildscript { + repositories { + google() + jcenter() + maven { + url = uri("https://maven.fabric.io/public") + } + } + dependencies { + classpath(foundation.e.blisslauncher.buildsrc.Libs.androidGradlePlugin) + classpath(foundation.e.blisslauncher.buildsrc.Libs.Kotlin.gradlePlugin) + classpath(foundation.e.blisslauncher.buildsrc.Libs.Kotlin.extensions) + classpath(foundation.e.blisslauncher.buildsrc.Libs.dexcountGradlePlugin) + } +} + +plugins { + id("com.diffplug.gradle.spotless") version "3.14.0" + id("com.github.ben-manes.versions") version "0.25.0" +} + +allprojects { + repositories { + google() + jcenter() + maven { url = uri("https://jitpack.io") } + mavenCentral() + } +} + +subprojects { + apply(plugin = ("com.diffplug.gradle.spotless")) + spotless { + java { + target("**/*.java") + removeUnusedImports() // removes any unused imports + } + kotlin { + target("**/*.kt") + ktlint() + } + kotlinGradle { + // same as kotlin, but for .gradle.kts files (defaults to '*.gradle.kts') + target("*.gradle.kts", "additionalScripts/*.gradle.kts") + ktlint() + } + } +} diff --git a/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt b/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt index d7f98933d3bfd2bc734956c498ec57e8239faefd..942c7f85a377e337e6f4e7baf4a8c0e657c9bc7a 100644 --- a/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt +++ b/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt @@ -11,10 +11,6 @@ object Versions { const val junit = "4.12" const val robolectric = "4.3" const val mockK = "1.9.3" - const val firebase_core = "17.1.0" - const val crashlytics = "2.10.1" - const val google_services = "4.3.0" - const val fabric = "1.31.0" const val okhttp = "4.1.0" const val retrofit = "2.6.1" const val dagger = "2.24" @@ -34,13 +30,6 @@ object Libs { const val robolectric = "org.robolectric:robolectric:${Versions.robolectric}" const val mockK = "io.mockk:mockk:${Versions.mockK}" - object Google { - const val firebaseCore = "com.google.firebase:firebase-core:${Versions.firebase_core}" - const val crashlytics = "com.crashlytics.sdk.android:crashlytics:${Versions.crashlytics}" - const val gmsGoogleServices = "com.google.gms:google-services:${Versions.google_services}" - const val fabricPlugin = "io.fabric.tools:gradle:${Versions.fabric}" - } - object Kotlin { const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" @@ -94,7 +83,7 @@ object Libs { } object Room { - private const val version = "2.2.0-beta01" + private const val version = "2.2.3" const val common = "androidx.room:room-common:$version" const val runtime = "androidx.room:room-runtime:$version" const val compiler = "androidx.room:room-compiler:$version" @@ -111,7 +100,7 @@ object Libs { object Dagger { const val dagger = "com.google.dagger:dagger:${Versions.dagger}" - const val androidSupport = "com.google.dagger:dagger-android-support:${Versions.dagger}" + const val android = "com.google.dagger:dagger-android:${Versions.dagger}" const val compiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" const val androidProcessor = "com.google.dagger:dagger-android-processor:${Versions.dagger}" } diff --git a/bump-version.sh b/bump-version.sh index f6d197c70f4ee7733a2c0ff82bc0e7ed9f471e61..0854b0fa234b984e0acd180295c2978762a444d5 100755 --- a/bump-version.sh +++ b/bump-version.sh @@ -34,7 +34,7 @@ usage() { echo -e " " echo -e " Used to increment the version of ${RED}BlissLauncher${NOCOLOR} safely by performing predefined actions: " echo " 1. Checks the category of the revision (major,minor or patch) based on the argument (revision_type) passed into the script." - echo -e " 2. Overwrite the version name and version code in app module level build.gradle based on the following logic" + echo -e " 2. Overwrite the version name and version code in app module level build.gradle.kts based on the following logic" echo -e " * - If this upgrade is a major, new version name will be ${CYAN}{old_major + 1}.0.0${NOCOLOR}" echo -e " * - If upgrade type is minor, updated version name will be ${CYAN}old_major.{old_minor + 1}.0${NOCOLOR}" echo -e " * - If it is a patch (hotfix), updated version name will be ${CYAN}old_major.old_minor.{old_patch + 1}.0${NOCOLOR}" diff --git a/common.gradle b/common.gradle new file mode 100644 index 0000000000000000000000000000000000000000..9189c8ec5df008673fc859f67eea0a3d07d1eb58 --- /dev/null +++ b/common.gradle @@ -0,0 +1,38 @@ +import foundation.e.blisslauncher.buildsrc.Libs +import foundation.e.blisslauncher.buildsrc.Versions + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion Versions.compile_sdk + + defaultConfig { + minSdkVersion Versions.min_sdk + targetSdkVersion Versions.target_sdk + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + lintOptions { + abortOnError false + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation Libs.Kotlin.stdlib + implementation Libs.AndroidX.coreKtx + + // Dagger + implementation Libs.Dagger.dagger + kapt Libs.Dagger.compiler +} \ No newline at end of file diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..7b54df9d772ad826612b7dbfe40218609edaf0ff --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,4 @@ +apply from: "../common.gradle" +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..f1b424510da51fd82143bc74a0a801ae5a1e2fcd --- /dev/null +++ b/common/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..146618cd5ddccd9d92e4562db6ea61e9cd5823c8 --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt b/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt new file mode 100644 index 0000000000000000000000000000000000000000..c2000100918e1521dfb12d14d313456aa04be20f --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt @@ -0,0 +1,223 @@ +package foundation.e.blisslauncher.common + +import android.content.Context +import android.graphics.Color +import android.graphics.RectF +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.util.SparseIntArray +import androidx.core.graphics.ColorUtils +import foundation.e.blisslauncher.common.compat.AdaptiveIconCompat +import foundation.e.blisslauncher.common.graphics.ColorExtractor + +class AdaptiveIconGenerator(private val context: Context, private val icon: Drawable) { + private var ranLoop = false + private val shouldWrap = false + private var backgroundColor = Color.WHITE + private val useWhiteBackground = true + private var isFullBleed = false + private var noMixinNeeded = false + private var fullBleedChecked = false + private val matchesMaskShape = false + private val isBackgroundWhite = false + private var scale = 0f + private var height = 0 + private var aHeight = 0f + private var width = 0 + private var aWidth = 0f + private var result: Drawable? = null + private fun loop() { + val extractee = icon + if (extractee == null) { + Log.e("AdaptiveIconGenerator", "extractee is null, skipping.") + onExitLoop() + return + } + val bounds = RectF() + scale = 1.0f + if (extractee is ColorDrawable) { + isFullBleed = true + fullBleedChecked = true + } + width = extractee.intrinsicWidth + height = extractee.intrinsicHeight + aWidth = width * (1 - (bounds.left + bounds.right)) + aHeight = height * (1 - (bounds.top + bounds.bottom)) + + // Check if the icon is squarish + val ratio = aHeight / aWidth + val isSquarish = 0.999 < ratio && ratio < 1.0001 + val almostSquarish = isSquarish || 0.97 < ratio && ratio < 1.005 + if (!isSquarish) { + isFullBleed = false + fullBleedChecked = true + } + val bitmap = + Utilities.drawableToBitmap(extractee) + if (bitmap == null) { + onExitLoop() + return + } + if (!bitmap.hasAlpha()) { + isFullBleed = true + fullBleedChecked = true + } + val size = height * width + val rgbScoreHistogram = + SparseIntArray(NUMBER_OF_COLORS_GUESSTIMATE) + val pixels = IntArray(size) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + /* + * Calculate the number of padding pixels around the actual icon (i) + * +----------------+ + * | top | + * +---+--------+---+ + * | | | | + * | l | i | r | + * | | | | + * +---+--------+---+ + * | bottom | + * +----------------+ + */ + val adjHeight = height - bounds.top - bounds.bottom + val l = bounds.left * width * adjHeight + val top = bounds.top * height * width + val r = bounds.right * width * adjHeight + val bottom = bounds.bottom * height * width + val addPixels = Math.round(l + top + r + bottom) + + // Any icon with less than 10% transparent pixels (padding excluded) is considered "full-bleed-ish" + val maxTransparent = (Math.round(size * .10) + addPixels).toInt() + // Any icon with less than 27% transparent pixels (padding excluded) doesn't need a color mix-in + val noMixinScore = (Math.round(size * .27) + addPixels).toInt() + var highScore = 0 + var bestRGB = 0 + var transparentScore = 0 + for (pixel in pixels) { + val alpha = 0xFF and (pixel shr 24) + if (alpha < MIN_VISIBLE_ALPHA) { + // Drop mostly-transparent pixels. + transparentScore++ + if (transparentScore > maxTransparent) { + isFullBleed = false + fullBleedChecked = true + } + continue + } + // Reduce color complexity. + val rgb: Int = ColorExtractor.posterize(pixel) + if (rgb < 0) { + // Defensively avoid array bounds violations. + continue + } + val currentScore = rgbScoreHistogram[rgb] + 1 + rgbScoreHistogram.append(rgb, currentScore) + if (currentScore > highScore) { + highScore = currentScore + bestRGB = rgb + } + } + // add back the alpha channel + bestRGB = bestRGB or (0xff shl 24) + + // not yet checked = not set to false = has to be full bleed, isBackgroundWhite = true = is adaptive + isFullBleed = isFullBleed or (!fullBleedChecked && !isBackgroundWhite) + + // return early if a mix-in isnt needed + noMixinNeeded = + !isFullBleed && !isBackgroundWhite && almostSquarish && transparentScore <= noMixinScore + + // Currently, it's set to true so a white background is used for all the icons. + if (useWhiteBackground) { + //backgroundColor = Color.WHITE; + backgroundColor = Color.WHITE and -0x7f000001 + onExitLoop() + return + } + if (isFullBleed || noMixinNeeded) { + backgroundColor = bestRGB + onExitLoop() + return + } + + // "single color" + val numColors = rgbScoreHistogram.size() + val singleColor = + numColors <= SINGLE_COLOR_LIMIT + + // Convert to HSL to get the lightness and adjust the color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(bestRGB, hsl) + val lightness = hsl[2] + val light = lightness > .5 + // Apply dark background to mostly white icons + val veryLight = lightness > .75 && singleColor + // Apply light background to mostly dark icons + val veryDark = lightness < .35 && singleColor + + // Adjust color to reach suitable contrast depending on the relationship between the colors + val opaqueSize = size - transparentScore + val pxPerColor = opaqueSize / numColors.toFloat() + val mixRatio = + Math.min(Math.max(pxPerColor / highScore, .15f), .7f) + + // Vary color mix-in based on lightness and amount of colors + val fill = if (light && !veryLight || veryDark) -0x1 else -0xcccccd + backgroundColor = ColorUtils.blendARGB(bestRGB, fill, mixRatio) + onExitLoop() + } + + private fun onExitLoop() { + ranLoop = true + result = genResult() + } + + private fun genResult(): Drawable { + val tmp = AdaptiveIconCompat( + ColorDrawable(), + FixedScaleDrawable() + ) + (tmp.getForeground() as FixedScaleDrawable).setDrawable(icon) + if (isFullBleed || noMixinNeeded) { + val scale: Float + scale = if (noMixinNeeded) { + val upScale = Math.min(width / aWidth, height / aHeight) + NO_MIXIN_ICON_SCALE * upScale + } else { + val upScale = Math.max(width / aWidth, height / aHeight) + FULL_BLEED_ICON_SCALE * upScale + } + (tmp.getForeground() as FixedScaleDrawable).setScale(scale) + } else { + (tmp.getForeground() as FixedScaleDrawable).setScale(scale) + } + (tmp.getBackground() as ColorDrawable).color = backgroundColor + return tmp + } + + fun getResult(): Drawable? { + if (!ranLoop) { + loop() + } + return result + } + + companion object { + // Average number of derived colors (based on averages with ~100 icons and performance testing) + private const val NUMBER_OF_COLORS_GUESSTIMATE = 45 + + // Found after some experimenting, might be improved with some more testing + private const val FULL_BLEED_ICON_SCALE = 1.44f + + // Found after some experimenting, might be improved with some more testing + private const val NO_MIXIN_ICON_SCALE = 1.40f + + // Icons with less than 5 colors are considered as "single color" + private const val SINGLE_COLOR_LIMIT = 5 + + // Minimal alpha to be considered opaque + private const val MIN_VISIBLE_ALPHA = 0xEF + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt b/common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt new file mode 100644 index 0000000000000000000000000000000000000000..707123794f6728b453a626317daad5913d06b4c2 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.common + +import android.annotation.TargetApi +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Picture +import android.os.Build + +object BitmapRenderer { + val USE_HARDWARE_BITMAP = Utilities.ATLEAST_P + + fun createSoftwareBitmap( + width: Int, + height: Int, + renderer: Renderer + ): Bitmap { + val result = Bitmap.createBitmap( + width, + height, + Bitmap.Config.ARGB_8888 + ) + renderer.draw(Canvas(result)) + return result + } + + @TargetApi(Build.VERSION_CODES.P) + fun createHardwareBitmap( + width: Int, + height: Int, + renderer: Renderer + ): Bitmap { + if (!USE_HARDWARE_BITMAP) { + return createSoftwareBitmap( + width, + height, + renderer + ) + } + val picture = Picture() + renderer.draw(picture.beginRecording(width, height)) + picture.endRecording() + return Bitmap.createBitmap(picture) + } + + /** + * Interface representing a bitmap draw operation. + */ + interface Renderer { + fun draw(out: Canvas) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt new file mode 100644 index 0000000000000000000000000000000000000000..28545b913f673ce7692b469c71e7b3fff70e9eb0 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.common + +import android.appwidget.AppWidgetHostView +import android.content.ComponentName +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.util.DisplayMetrics +import android.util.Log + +class DeviceProfile( + context: Context, + inv: InvariantDeviceProfile, + minSize: Point, + maxSize: Point, + width: Int, + height: Int +) { + val inv: InvariantDeviceProfile + + // Device properties + val isTablet: Boolean + val isLargeTablet: Boolean + val isPhone: Boolean + val transposeLayoutWithOrientation: Boolean + + val widthPx: Int + val heightPx: Int + var availableWidthPx = 0 + var availableHeightPx = 0 + + // Workspace + val desiredWorkspaceLeftRightMarginPx: Int + val cellLayoutPaddingLeftRightPx: Int + val cellLayoutBottomPaddingPx: Int + val edgeMarginPx: Int + val defaultWidgetPadding: Rect + val defaultPageSpacingPx: Int + private val topWorkspacePadding: Int + + // Workspace icons + var iconSizePx = 0 + var iconTextSizePx = 0 + var iconDrawablePaddingPx = 0 + var iconDrawablePaddingOriginalPx: Int + var cellWidthPx = 0 + var cellHeightPx = 0 + var workspaceCellPaddingXPx: Int + + // Folder + var folderIconSizePx = 0 + var folderIconOffsetYPx = 0 + + // Folder cell + var folderCellWidthPx = 0 + var folderCellHeightPx = 0 + + // Folder child + var folderChildIconSizePx = 0 + var folderChildTextSizePx = 0 + var folderChildDrawablePaddingPx = 0 + + // Hotseat + var hotseatCellHeightPx = 0 + + // In portrait: size = height, in landscape: size = width + var hotseatBarSizePx: Int + val hotseatBarTopPaddingPx: Int + val hotseatBarBottomPaddingPx: Int + val hotseatBarSidePaddingPx: Int + + // Widgets + val appWidgetScale = PointF(1.0f, 1.0f) + + // Drop Target + var dropTargetBarSizePx: Int + + private val TAG = "DeviceProfile" + + // Insets + val insets = Rect() + val workspacePadding = Rect() + private val mHotseatPadding = Rect() + private var mIsSeascape = false + + fun copy(context: Context): DeviceProfile { + val size = + Point(availableWidthPx, availableHeightPx) + return DeviceProfile( + context, inv, size, size, widthPx, heightPx + ) + } + + /** + * Inverse of [.getMultiWindowProfile] + * @return device profile corresponding to the current orientation in non multi-window mode. + */ + var fullScreenProfile: DeviceProfile? = null + get() = inv.portraitProfile + + private fun updateAvailableDimensions( + dm: DisplayMetrics, + res: Resources + ) { + updateIconSize(1f, res, dm) + // Check to see if the icons fit within the available height. If not, then scale down. + val usedHeight = (cellHeightPx * inv.numRows).toFloat() + val maxHeight = availableHeightPx - totalWorkspacePadding.y + if (usedHeight > maxHeight) { + val scale = maxHeight / usedHeight + updateIconSize(scale, res, dm) + } + updateAvailableFolderCellDimensions(dm, res) + } + + private fun updateIconSize( + scale: Float, + res: Resources, + dm: DisplayMetrics + ) { + val invIconSizePx = inv.iconSize + iconSizePx = + (Utilities.pxFromDp( + invIconSizePx, + dm + ) * scale).toInt() + iconTextSizePx = (Utilities.pxFromSp( + inv.iconTextSize, + dm + ) * scale).toInt() + iconDrawablePaddingPx = + (availableWidthPx - iconSizePx * inv.numColumns) / (inv.numColumns + 1) + cellHeightPx = (iconSizePx + iconDrawablePaddingPx + + Utilities.calculateTextHeight(iconTextSizePx.toFloat())) + + val cellYPadding = (cellSize.y - cellHeightPx) / 2 + if (iconDrawablePaddingPx > cellYPadding) { + cellHeightPx -= iconDrawablePaddingPx - cellYPadding + iconDrawablePaddingPx = cellYPadding + } + cellWidthPx = iconSizePx + iconDrawablePaddingPx + hotseatCellHeightPx = iconSizePx + + // Folder icon + folderIconSizePx = iconSizePx + folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2 + } + + private fun updateAvailableFolderCellDimensions( + dm: DisplayMetrics, + res: Resources + ) { + val folderBottomPanelSize = + (res.getDimensionPixelSize(R.dimen.folder_label_padding_top) + + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom) + + Utilities.calculateTextHeight( + res.getDimension( + R.dimen.folder_label_text_size + ) + )) + updateFolderCellSize(1f, dm, res) + // Don't let the folder get too close to the edges of the screen. + val folderMargin = edgeMarginPx + val totalWorkspacePadding = totalWorkspacePadding + // Check if the icons fit within the available height. + val usedHeight = + folderCellHeightPx * inv.numFolderRows + folderBottomPanelSize.toFloat() + val maxHeight = availableHeightPx - totalWorkspacePadding.y - folderMargin + val scaleY = maxHeight / usedHeight + // Check if the icons fit within the available width. + val usedWidth = folderCellWidthPx * inv.numFolderColumns.toFloat() + val maxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin + val scaleX = maxWidth / usedWidth + val scale = Math.min(scaleX, scaleY) + if (scale < 1f) { + updateFolderCellSize(scale, dm, res) + } + } + + private fun updateFolderCellSize( + scale: Float, + dm: DisplayMetrics, + res: Resources + ) { + folderChildIconSizePx = + (Utilities.pxFromDp( + inv.iconSize, + dm + ) * scale).toInt() + folderChildTextSizePx = + (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale).toInt() + val textHeight = + Utilities.calculateTextHeight( + folderChildTextSizePx.toFloat() + ) + val cellPaddingX = + (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale).toInt() + val cellPaddingY = + (res.getDimensionPixelSize(R.dimen.folder_cell_y_padding) * scale).toInt() + folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX + folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight + folderChildDrawablePaddingPx = Math.max( + 0, + (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3 + ) + } + + fun updateInsets(insets: Rect?) { + insets!!.set(insets) + updateWorkspacePadding() + } + + // Since we are only concerned with the overall padding, layout direction does not matter. + val cellSize: Point + get() { + val result = Point() + val padding = totalWorkspacePadding + result.x = + calculateCellWidth( + availableWidthPx - padding.x - + cellLayoutPaddingLeftRightPx * 2, inv.numColumns + ) + result.y = + calculateCellHeight( + availableHeightPx - padding.y - + cellLayoutBottomPaddingPx, inv.numRows + ) + return result + } + + val totalWorkspacePadding: Point + get() { + updateWorkspacePadding() + return Point( + workspacePadding.left + workspacePadding.right, + workspacePadding.top + workspacePadding.bottom + ) + } + + /** + * Updates [.workspacePadding] as a result of any internal value change to reflect the + * new workspace padding + */ + private fun updateWorkspacePadding() { + val padding = workspacePadding + val paddingBottom = hotseatBarSizePx + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing + // between all icons + // The amount of screen space available for left/right padding. + var availablePaddingX = Math.max( + 0, widthPx - (inv.numColumns * cellWidthPx + + (inv.numColumns - 1) * cellWidthPx) + ) + availablePaddingX = Math.min( + availablePaddingX.toFloat(), + widthPx * MAX_HORIZONTAL_PADDING_PERCENT + ).toInt() + val availablePaddingY = Math.max( + 0, heightPx - topWorkspacePadding - paddingBottom - + 2 * inv.numRows * cellHeightPx - hotseatBarTopPaddingPx - + hotseatBarBottomPaddingPx + ) + padding[availablePaddingX / 2, topWorkspacePadding + availablePaddingY / 2, availablePaddingX / 2] = + paddingBottom + availablePaddingY / 2 + } else { // Pad the top and bottom of the workspace with search/hotseat bar sizes + padding[desiredWorkspaceLeftRightMarginPx, topWorkspacePadding, desiredWorkspaceLeftRightMarginPx] = + paddingBottom + } + } + + val hotseatLayoutPadding: Rect + get() { + // We want the edges of the hotseat to line up with the edges of the workspace, but the + // icons in the hotseat are a different size, and so don't line up perfectly. To account + // for this, we pad the left and right of the hotseat with half of the difference of a + // workspace cell vs a hotseat cell. + val workspaceCellWidth = widthPx.toFloat() / inv.numColumns + val hotseatCellWidth = widthPx.toFloat() / inv.numHotseatIcons + val hotseatAdjustment = + Math.round((workspaceCellWidth - hotseatCellWidth) / 2) + mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, hotseatBarTopPaddingPx, hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = + hotseatBarBottomPaddingPx + insets.bottom + cellLayoutBottomPaddingPx + return mHotseatPadding + } // Folders should only appear below the drop target bar and above the hotseat// Folders should only appear right of the drop target bar and left of the hotseat + + /** + * @return the bounds for which the open folders should be contained within + */ + val absoluteOpenFolderBounds: Rect + get() = Rect( + insets.left + edgeMarginPx, + insets.top + dropTargetBarSizePx + edgeMarginPx, + insets.left + availableWidthPx - edgeMarginPx, + insets.top + availableHeightPx - hotseatBarSizePx - -edgeMarginPx + ) + + fun getCellHeight(containerType: Long): Int { + return when (containerType) { + LauncherConstants.ContainerType.CONTAINER_DESKTOP -> cellHeightPx + LauncherConstants.ContainerType.CONTAINER_HOTSEAT -> hotseatCellHeightPx + else -> 0 + } + } + + /** + * Callback when a component changes the DeviceProfile associated with it, as a result of + * configuration change + */ + interface OnDeviceProfileChangeListener { + /** + * Called when the device profile is reassigned. Note that for layout and measurements, it + * is sufficient to listen for inset changes. Use this callback when you need to perform + * a one time operation. + */ + fun onDeviceProfileChanged(dp: DeviceProfile?) + } + + companion object { + /** + * The maximum amount of left/right workspace padding as a percentage of the screen width. + * To be clear, this means that up to 7% of the screen width can be used as left padding, and + * 7% of the screen width can be used as right padding. + */ + private const val MAX_HORIZONTAL_PADDING_PERCENT = 0.14f + private const val TALL_DEVICE_ASPECT_RATIO_THRESHOLD = 2.0f + + fun calculateCellWidth(width: Int, countX: Int): Int { + return width / countX + } + + fun calculateCellHeight(height: Int, countY: Int): Int { + return height / countY + } + + private fun getContext( + c: Context, + orientation: Int + ): Context { + val context = + Configuration(c.resources.configuration) + context.orientation = orientation + return c.createConfigurationContext(context) + } + } + + init { + var context = context + this.inv = inv + var res = context.resources + val dm = res.displayMetrics + // Constants from resources + isTablet = res.getBoolean( + R.bool.is_tablet + ) + isLargeTablet = res.getBoolean(R.bool.is_large_tablet) + isPhone = !isTablet && !isLargeTablet + // Some more constants + transposeLayoutWithOrientation = + res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation) + context = + getContext( + context, Configuration.ORIENTATION_PORTRAIT + ) + res = context.resources + val cn = ComponentName( + context.packageName, + this.javaClass.name + ) + defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null) + edgeMarginPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin) + desiredWorkspaceLeftRightMarginPx = edgeMarginPx + cellLayoutPaddingLeftRightPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding) + cellLayoutBottomPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_bottom_padding) + defaultPageSpacingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing) + topWorkspacePadding = + res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_top_padding) + iconDrawablePaddingOriginalPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding) + dropTargetBarSizePx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size) + workspaceCellPaddingXPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x) + hotseatBarTopPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding) + hotseatBarBottomPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding) + hotseatBarSidePaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding) + hotseatBarSizePx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_size) + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx + // Determine sizes. + widthPx = width + heightPx = height + availableWidthPx = minSize.x + availableHeightPx = maxSize.y + // Calculate all of the remaining variables. + updateAvailableDimensions(dm, res) + // Now that we have all of the variables calculated, we can tune certain sizes. + val aspectRatio = + Math.max(widthPx, heightPx).toFloat() / Math.min( + widthPx, + heightPx + ) + val isTallDevice = aspectRatio.compareTo(TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0 + if (isPhone && isTallDevice) { + // We increase the hotseat size when there is extra space. + // ie. For a display with a large aspect ratio, we can keep the icons on the workspace + // in portrait mode closer together by adding more height to the hotseat. + // Note: This calculation was created after noticing a pattern in the design spec. + val extraSpace = cellSize.y - iconSizePx - iconDrawablePaddingPx + hotseatBarSizePx += extraSpace + // Recalculate the available dimensions using the new hotseat size. + updateAvailableDimensions(dm, res) + } + updateWorkspacePadding() + // This is done last, after iconSizePx is calculated above. + //TODO: mBadgeRenderer = BadgeRenderer(iconSizePx) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt b/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt new file mode 100644 index 0000000000000000000000000000000000000000..764c32c710129be58400951e76a5b00957f58b59 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt @@ -0,0 +1,66 @@ +package foundation.e.blisslauncher.common + +import android.annotation.TargetApi +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.DrawableWrapper +import android.os.Build +import android.util.AttributeSet +import org.xmlpull.v1.XmlPullParser + +/** + * Extension of [DrawableWrapper] which scales the child drawables by a fixed amount. + */ +@TargetApi(Build.VERSION_CODES.N) +class FixedScaleDrawable : + DrawableWrapper(ColorDrawable()) { + private var mScaleX: Float + private var mScaleY: Float + override fun draw(canvas: Canvas) { + val saveCount = canvas.save() + canvas.scale( + mScaleX, mScaleY, + bounds.exactCenterX(), bounds.exactCenterY() + ) + super.draw(canvas) + canvas.restoreToCount(saveCount) + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet + ) { + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet, + theme: Resources.Theme + ) { + } + + fun setScale(scale: Float) { + val h = intrinsicHeight.toFloat() + val w = intrinsicWidth.toFloat() + mScaleX = scale * LEGACY_ICON_SCALE + mScaleY = scale * LEGACY_ICON_SCALE + if (h > w && w > 0) { + mScaleX *= w / h + } else if (w > h && h > 0) { + mScaleY *= h / w + } + } + + companion object { + // TODO b/33553066 use the constant defined in MaskableIconDrawable + const val LEGACY_ICON_SCALE = .7f * .6667f + } + + init { + mScaleX = LEGACY_ICON_SCALE + mScaleY = LEGACY_ICON_SCALE + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt new file mode 100644 index 0000000000000000000000000000000000000000..b265b4da2ce5f47772650032bde8275ed250a642 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.common + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Point +import android.util.DisplayMetrics +import android.util.Log +import android.util.Xml +import android.view.WindowManager +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.util.ArrayList +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.hypot + +@Singleton +open class InvariantDeviceProfile { + // Profile-defining invariant properties + var name: String? = null + var minWidthDps = 0f + var minHeightDps = 0f + + /** + * Number of icons per row and column in the workspace. + */ + var numRows = 0 + var numColumns = 0 + + /** + * Number of icons per row and column in the folder. + */ + var numFolderRows = 0 + var numFolderColumns = 0 + + var iconSize = 0f + var landscapeIconSize = 0f + var iconBitmapSize = 0 + var fillResIconDpi = 0 + var iconTextSize = 0f + + /** + * Number of icons inside the hotseat area. + */ + var numHotseatIcons = 0 + var defaultLayoutId = 0 + var demoModeLayoutId = 0 + lateinit var landscapeProfile: DeviceProfile + lateinit var portraitProfile: DeviceProfile + lateinit var defaultWallpaperSize: Point + + constructor() + + @Inject + constructor(context: Context) { + val wm = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val display = wm.defaultDisplay + val dm = DisplayMetrics() + display.getMetrics(dm) + val smallestSize = Point() + val largestSize = Point() + display.getCurrentSizeRange(smallestSize, largestSize) + // This guarantees that width < height + minWidthDps = Utilities.dpiFromPx( + Math.min( + smallestSize.x, + smallestSize.y + ), dm + ) + minHeightDps = Utilities.dpiFromPx( + Math.min( + largestSize.x, + largestSize.y + ), dm + ) + val closestProfiles = + findClosestDeviceProfiles( + minWidthDps, minHeightDps, getPredefinedDeviceProfiles(context) + ) + val interpolatedDeviceProfileOut = + invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles) + val closestProfile = closestProfiles[0] + Log.d("InvariantDevice", "rows and col: ${closestProfile.numRows} * ${closestProfile.numColumns}") + numRows = closestProfile.numRows + numColumns = closestProfile.numColumns + numHotseatIcons = closestProfile.numHotseatIcons + defaultLayoutId = closestProfile.defaultLayoutId + demoModeLayoutId = closestProfile.demoModeLayoutId + numFolderRows = closestProfile.numFolderRows + numFolderColumns = closestProfile.numFolderColumns + iconSize = interpolatedDeviceProfileOut.iconSize + landscapeIconSize = interpolatedDeviceProfileOut.landscapeIconSize + iconBitmapSize = + Utilities.pxFromDp(iconSize, dm) + iconTextSize = interpolatedDeviceProfileOut.iconTextSize + fillResIconDpi = getLauncherIconDensity(iconBitmapSize) + // If the partner customization apk contains any grid overrides, apply them + // Supported overrides: numRows, numColumns, iconSize + //applyPartnerDeviceProfileOverrides(context, dm); + val realSize = Point() + display.getRealSize(realSize) + // The real size never changes. smallSide and largeSide will remain the + // same in any orientation. + val smallSide = Math.min(realSize.x, realSize.y) + val largeSide = Math.max(realSize.x, realSize.y) + landscapeProfile = DeviceProfile( + context, this, smallestSize, largestSize, + largeSide, smallSide + ) + portraitProfile = DeviceProfile( + context, this, smallestSize, largestSize, + smallSide, largeSide + ) + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + defaultWallpaperSize = Point(smallSide, largeSide) + } + + private constructor(p: InvariantDeviceProfile) : this( + p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, + p.numFolderRows, p.numFolderColumns, + p.iconSize, p.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, + p.defaultLayoutId, p.demoModeLayoutId + ) + + private constructor( + n: String?, + w: Float, + h: Float, + r: Int, + c: Int, + fr: Int, + fc: Int, + `is`: Float, + lis: Float, + its: Float, + hs: Int, + dlId: Int, + dmlId: Int + ) { + name = n + minWidthDps = w + minHeightDps = h + numRows = r + numColumns = c + numFolderRows = fr + numFolderColumns = fc + iconSize = `is` + landscapeIconSize = lis + iconTextSize = its + numHotseatIcons = hs + defaultLayoutId = dlId + demoModeLayoutId = dmlId + } + + private fun getPredefinedDeviceProfiles(context: Context): ArrayList { + val profiles = + ArrayList() + try { + context.resources.getXml(R.xml.device_profiles).use { parser -> + val depth = parser.depth + var type: Int + while ((parser.next().also { + type = it + } != XmlPullParser.END_TAG || + parser.depth > depth) && type != XmlPullParser.END_DOCUMENT + ) { + if (type == XmlPullParser.START_TAG && "profile" == parser.name) { + val a = context.obtainStyledAttributes( + Xml.asAttributeSet(parser), + R.styleable.InvariantDeviceProfile + ) + val numRows = a.getInt( + R.styleable.InvariantDeviceProfile_numRows, + 0 + ) + val numColumns = a.getInt( + R.styleable.InvariantDeviceProfile_numColumns, + 5 + ) + Log.d("Invariant", "Num columns here: $numColumns") + val iconSize = a.getFloat( + R.styleable.InvariantDeviceProfile_iconSize, + 0f + ) + val name = a.getString(R.styleable.InvariantDeviceProfile_name) + Log.d("Invariant", "Parsing profile name: $name") + profiles.add( + InvariantDeviceProfile( + a.getString(R.styleable.InvariantDeviceProfile_name), + a.getFloat( + R.styleable.InvariantDeviceProfile_minWidthDps, + 0f + ), + a.getFloat( + R.styleable.InvariantDeviceProfile_minHeightDps, + 0f + ), + numRows, + numColumns, + a.getInt( + R.styleable.InvariantDeviceProfile_numFolderRows, + numRows + ), + a.getInt( + R.styleable.InvariantDeviceProfile_numFolderColumns, + numColumns + ), + iconSize, + a.getFloat( + R.styleable.InvariantDeviceProfile_landscapeIconSize, + iconSize + ), + a.getFloat( + R.styleable.InvariantDeviceProfile_iconTextSize, + 0f + ), + a.getInt( + R.styleable.InvariantDeviceProfile_numHotseatIcons, + numColumns + ), + a.getResourceId( + R.styleable.InvariantDeviceProfile_defaultLayoutId, + 0 + ), + a.getResourceId( + R.styleable.InvariantDeviceProfile_demoModeLayoutId, + 0 + ) + ) + ) + a.recycle() + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: XmlPullParserException) { + throw RuntimeException(e) + } + return profiles + } + + private fun getLauncherIconDensity(requiredSize: Int): Int { // Densities typically defined by an app. + val densityBuckets = intArrayOf( + DisplayMetrics.DENSITY_LOW, + DisplayMetrics.DENSITY_MEDIUM, + DisplayMetrics.DENSITY_TV, + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XHIGH, + DisplayMetrics.DENSITY_XXHIGH, + DisplayMetrics.DENSITY_XXXHIGH + ) + var density = DisplayMetrics.DENSITY_XXXHIGH + for (i in densityBuckets.indices.reversed()) { + val expectedSize = + (ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] / + DisplayMetrics.DENSITY_DEFAULT) + if (expectedSize >= requiredSize) { + density = densityBuckets[i] + } + } + return density + } + + fun dist(x0: Float, y0: Float, x1: Float, y1: Float): Float { + return hypot(x1 - x0.toDouble(), y1 - y0.toDouble()).toFloat() + } + + /** + * Returns the closest device profiles ordered by closeness to the specified width and height + */ + fun findClosestDeviceProfiles( + width: Float, + height: Float, + points: ArrayList + ): ArrayList { // Sort the profiles by their closeness to the dimensions + points.sortWith(Comparator { a, b -> + dist(width, height, a.minWidthDps, a.minHeightDps).compareTo( + dist( + width, + height, + b.minWidthDps, + b.minHeightDps + ) + ) + }) + return points + } + + // Package private visibility for testing. + fun invDistWeightedInterpolate( + width: Float, + height: Float, + points: ArrayList + ): InvariantDeviceProfile { + var weights = 0f + var p = points[0] + if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0f) { + return p + } + val out = InvariantDeviceProfile() + var i = 0 + while (i < points.size && i < KNEARESTNEIGHBOR) { + p = + InvariantDeviceProfile(points[i]) + val w = weight( + width, + height, + p.minWidthDps, + p.minHeightDps, + WEIGHT_POWER + ) + weights += w + out += p * w + ++i + } + return out * (1.0f / weights) + } + + operator fun plusAssign(p: InvariantDeviceProfile) { + iconSize += p.iconSize + landscapeIconSize += p.landscapeIconSize + iconTextSize += p.iconTextSize + } + + operator fun times(w: Float): InvariantDeviceProfile { + iconSize *= w + landscapeIconSize *= w + iconTextSize *= w + return this + } + + val allAppsButtonRank: Int + get() = numHotseatIcons / 2 + + fun isAllAppsButtonRank(rank: Int): Boolean { + return rank == allAppsButtonRank + } + + fun getDeviceProfile(context: Context): DeviceProfile { + return if (context.resources.configuration.orientation + == Configuration.ORIENTATION_LANDSCAPE + ) landscapeProfile else portraitProfile + } + + private fun weight( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + pow: Float + ): Float { + val d = dist(x0, y0, x1, y1) + return if (d.compareTo(0f) == 0) { + Float.POSITIVE_INFINITY + } else (WEIGHT_EFFICIENT / Math.pow( + d.toDouble(), + pow.toDouble() + )).toFloat() + } + + companion object { + // This is a static that we use for the default icon size on a 4/5-inch phone + private const val DEFAULT_ICON_SIZE_DP = 60f + private const val ICON_SIZE_DEFINED_IN_APP_DP = 48f + + // Constants that affects the interpolation curve between statically defined device profile + // buckets. + private const val KNEARESTNEIGHBOR = 3f + private const val WEIGHT_POWER = 5f + + // used to offset float not being able to express extremely small weights in extreme cases. + private const val WEIGHT_EFFICIENT = 100000f + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt b/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..1615945e13e144c9aba08194660fb35842e3863b --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.common + +class LauncherConstants { + + object ItemType { + + const val APPLICATION = 0 + const val SHORTCUT = 1 + const val FOLDER = 2 + const val APPWIDGET = 4 + const val CUSTOM_APPWIDGET = 5 + const val DEEP_SHORTCUT = 6 + + fun itemTypeToString(type: Int): String = when (type) { + APPLICATION -> "APP" + SHORTCUT -> "SHORTCUT" + FOLDER -> "FOLDER" + APPWIDGET -> "WIDGET" + CUSTOM_APPWIDGET -> "CUSTOMWIDGET" + DEEP_SHORTCUT -> "DEEPSHORTCUT" + else -> type.toString() + } + } + + object ContainerType { + + const val CONTAINER_DESKTOP: Long = -100 + const val CONTAINER_HOTSEAT: Long = -101 + + fun containerToString(container: Long): String = when (container) { + CONTAINER_DESKTOP -> "desktop" + CONTAINER_HOTSEAT -> "hotseat" + else -> container.toString() + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java b/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java new file mode 100755 index 0000000000000000000000000000000000000000..c04efde585f58457414bc06e9d4b47927db978ba --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java @@ -0,0 +1,267 @@ +package foundation.e.blisslauncher.common; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Utilities { + + private static final String TAG = "Utilities"; + + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + + /** + * Use hard coded values to compile with android source. + */ + public static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + public static final boolean ATLEAST_OREO = + Build.VERSION.SDK_INT >= 26; + + public static final boolean ATLEAST_NOUGAT_MR1 = + Build.VERSION.SDK_INT >= 25; + + + // These values are same as that in {@link AsyncTask}. + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE = 1; + /** + * An {@link Executor} to be used with async task with no limit on the queue size. + */ + public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, new LinkedBlockingQueue()); + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + public static float dpiFromPx(int size, DisplayMetrics metrics){ + float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; + return (size / densityRatio); + } + public static int pxFromDp(float size, DisplayMetrics metrics) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, metrics)); + } + + public static float pxFromDp(int dp, Context context) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return dp * (metrics.densityDpi / 160f); + } + + public static int pxFromSp(float size, DisplayMetrics metrics) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, metrics)); + } + + /** + * Calculates the height of a given string at a specific text size. + */ + public static int calculateTextHeight(float textSizePx) { + Paint p = new Paint(); + p.setTextSize(textSizePx); + Paint.FontMetrics fm = p.getFontMetrics(); + return (int) Math.ceil(fm.bottom - fm.top); + } + + public static String convertMonthToString(int month) { + switch (month) { + case Calendar.JANUARY: + return "JAN"; + case Calendar.FEBRUARY: + return "FEB"; + case Calendar.MARCH: + return "MAR"; + case Calendar.APRIL: + return "APR"; + case Calendar.MAY: + return "MAY"; + case Calendar.JUNE: + return "JUN"; + case Calendar.JULY: + return "JUL"; + case Calendar.AUGUST: + return "AUG"; + case Calendar.SEPTEMBER: + return "SEP"; + case Calendar.OCTOBER: + return "OCT"; + case Calendar.NOVEMBER: + return "NOV"; + case Calendar.DECEMBER: + return "DEC"; + default: + return ""; + } + } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + if (s == null) { + return null; + } + + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } + + public static ArrayList getAllChildrenViews(View view) { + if (!(view instanceof ViewGroup)) { + ArrayList viewArrayList = new ArrayList(); + viewArrayList.add(view); + + return viewArrayList; + } + + ArrayList result = new ArrayList(); + + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + + View child = viewGroup.getChildAt(i); + + ArrayList viewArrayList = new ArrayList(); + viewArrayList.add(view); + viewArrayList.addAll(getAllChildrenViews(child)); + + result.addAll(viewArrayList); + } + + return result; + } + + public static Bitmap drawableToBitmap(Drawable drawable) { + return Utilities.drawableToBitmap(drawable, true); + } + + public static Bitmap drawableToBitmap(Drawable drawable, boolean forceCreate) { + return drawableToBitmap(drawable, forceCreate, 0); + } + + public static Bitmap drawableToBitmap(Drawable drawable, boolean forceCreate, int fallbackSize) { + if (!forceCreate && drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + if (width <= 0 || height <= 0) { + if (fallbackSize > 0) { + width = height = fallbackSize; + } else { + return null; + } + } + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + public static boolean isBootCompleted() { + return "1".equals(getSystemProperty("sys.boot_completed", "1")); + } + + public static String getSystemProperty(String property, String defaultValue) { + try { + Class clazz = Class.forName("android.os.SystemProperties"); + Method getter = clazz.getDeclaredMethod("get", String.class); + String value = (String) getter.invoke(null, property); + if (!TextUtils.isEmpty(value)) { + return value; + } + } catch (Exception e) { + Log.d(TAG, "Unable to read system properties"); + } + return defaultValue; + } + + /** + * Ensures that a value is within given bounds. Specifically: + * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound, + * return upperBound; else return value unchanged. + */ + public static int boundToRange(int value, int lowerBound, int upperBound) { + return Math.max(lowerBound, Math.min(value, upperBound)); + } + + public static boolean isSystemApp(Context context, Intent intent) { + PackageManager pm = context.getPackageManager(); + ComponentName cn = intent.getComponent(); + String packageName = null; + if (cn == null) { + ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if ((info != null) && (info.activityInfo != null)) { + packageName = info.activityInfo.packageName; + } + } else { + packageName = cn.getPackageName(); + } + if (packageName != null) { + try { + PackageInfo info = pm.getPackageInfo(packageName, 0); + return (info != null) && (info.applicationInfo != null) && + ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } else { + return false; + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java b/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java new file mode 100755 index 0000000000000000000000000000000000000000..8c7b00de290f46dd20be4e8671f8e29be618a56b --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java @@ -0,0 +1,1096 @@ +package foundation.e.blisslauncher.common.compat; + +import android.annotation.SuppressLint; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Shader; +import android.graphics.Shader.TileMode; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import androidx.core.graphics.PathParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + *

This class can also be created via XML inflation using <adaptive-icon> tag + * in addition to dynamic creation. + * + *

This drawable supports two drawable layers: foreground and background. The layers are clipped + * when rendering using the mask defined in the device configuration. + * + *

    + *
  • Both foreground and background layers should be sized at 108 x 108 dp.
  • + *
  • The inner 72 x 72 dp of the icon appears within the masked viewport.
  • + *
  • The outer 18 dp on each of the 4 sides of the layers is reserved for use by the system UI + * surfaces to create interesting visual effects, such as parallax or pulsing.
  • + *
+ *

+ * Such motion effect is achieved by internally setting the bounds of the foreground and + * background layer as following: + *

+ * Rect(getBounds().left - getBounds().getWidth() * #getExtraInsetFraction(),
+ *      getBounds().top - getBounds().getHeight() * #getExtraInsetFraction(),
+ *      getBounds().right + getBounds().getWidth() * #getExtraInsetFraction(),
+ *      getBounds().bottom + getBounds().getHeight() * #getExtraInsetFraction())
+ * 
+ */ +public class AdaptiveIconCompat extends Drawable implements Drawable.Callback { + + private static final String path = "M142,180H38c-21,0 -38,-17 -38,-38V38C0,17 17,0 38,0h104c21,0 38,17 38,38v104C180,163 163,180 142,180z"; + + /** + * Mask path is defined inside device configuration in following dimension: [100 x 100] + */ + public static float MASK_SIZE = 180f; + + /** + * Launcher icons design guideline + */ + private static final float SAFEZONE_SCALE = 66f / 72f; + + /** + * All four sides of the layers are padded with extra inset so as to provide + * extra content to reveal within the clip path when performing affine transformations on the + * layers. + *

+ * Each layers will reserve 25% of it's width and height. + *

+ * As a result, the view port of the layers is smaller than their intrinsic width and height. + */ + private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f; + private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE); + + /** + * Clip path defined in R.string.config_icon_mask. + */ + private static Path sMask; + + /** + * Scaled mask based on the view bounds. + */ + private final Path mMask; + private final Matrix mMaskMatrix; + private final Region mTransparentRegion; + + private Bitmap mMaskBitmap; + + private static final int BACKGROUND_ID = 0; + private static final int FOREGROUND_ID = 1; + + /** + * State variable that maintains the {@link ChildDrawable} array. + */ + LayerState mLayerState; + + private Shader mLayersShader; + private Bitmap mLayersBitmap; + + private final Rect mTmpOutRect = new Rect(); + private Rect mHotspotBounds; + private boolean mMutated; + + private boolean mSuspendChildInvalidation; + private boolean mChildRequestedInvalidation; + private final Canvas mCanvas; + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | + Paint.FILTER_BITMAP_FLAG); + + private Method methodCreatePathFromPathData; + private Method methodExtractThemeAttrs; + + private boolean mUseMyUglyWorkaround = true; + + private static final String TAG = "AdaptiveIconDrawable"; + + /** + * Constructor used for xml inflation. + */ + public AdaptiveIconCompat() { + this((LayerState) null, null); + } + + /** + * The one constructor to rule them all. This is called by all public + * constructors to set the state and initialize local properties. + */ + AdaptiveIconCompat(LayerState state, Resources res) { + initReflections(); + + mLayerState = createConstantState(state, res); + + if (sMask == null) { + sMask = PathParser.createPathFromPathData(getMaskPath()); + } + mMask = PathParser.createPathFromPathData(getMaskPath()); + //mMask = DeviceProfile.path; + mMaskMatrix = new Matrix(); + mCanvas = new Canvas(); + mTransparentRegion = new Region(); + } + + @SuppressLint("PrivateApi") + private void initReflections() { + try { + Class pathParser = getClass().getClassLoader().loadClass("android.util.PathParser"); + methodCreatePathFromPathData = pathParser.getDeclaredMethod("createPathFromPathData", + String.class); + methodExtractThemeAttrs = TypedArray.class.getDeclaredMethod("extractThemeAttrs"); + } catch (ClassNotFoundException | NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private int getInt(Field field, Object obj) { + try { + return field.getInt(obj); + } catch (IllegalAccessException e) { + return 0; + } + } + + private T invoke(Method method, Object obj, Object... params) { + try { + return (T) method.invoke(obj, params); + } catch (IllegalAccessException | InvocationTargetException e) { + return null; + } + } + + private String getMaskPath() { + return path; + } + + private ChildDrawable createChildDrawable(Drawable drawable) { + final ChildDrawable layer = new ChildDrawable(mLayerState.mDensity); + layer.mDrawable = drawable; + layer.mDrawable.setCallback(this); + mLayerState.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + return layer; + } + + LayerState createConstantState(LayerState state, Resources res) { + return new LayerState(state, this, res); + } + + /** + * Constructor used to dynamically create this drawable. + * + * @param backgroundDrawable drawable that should be rendered in the background + * @param foregroundDrawable drawable that should be rendered in the foreground + */ + public AdaptiveIconCompat(Drawable backgroundDrawable, + Drawable foregroundDrawable) { + this(backgroundDrawable, foregroundDrawable, true); + } + + public AdaptiveIconCompat(Drawable backgroundDrawable, + Drawable foregroundDrawable, boolean useMyUglyWorkaround) { + this((LayerState) null, null); + if (backgroundDrawable != null) { + addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); + } + if (foregroundDrawable != null) { + addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable)); + } + mUseMyUglyWorkaround = useMyUglyWorkaround; + } + + /** + * Sets the layer to the {@param index} and invalidates cache. + * + * @param index The index of the layer. + * @param layer The layer to add. + */ + private void addLayer(int index, ChildDrawable layer) { + mLayerState.mChildren[index] = layer; + mLayerState.invalidateCache(); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, + AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + super.inflate(r, parser, attrs, theme); + + final LayerState state = mLayerState; + if (state == null) { + return; + } + + // The density may have changed since the last update. This will + // apply scaling to any existing constant state properties. + final int deviceDensity = resolveDensity(r, 0); + //state.setDensity(deviceDensity); + + final ChildDrawable[] array = state.mChildren; + for (int i = 0; i < state.mChildren.length; i++) { + final ChildDrawable layer = array[i]; + //layer.setDensity(deviceDensity); + } + + inflateLayers(r, parser, attrs, theme); + } + + static int resolveDensity(Resources r, int parentDensity) { + final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi; + return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; + } + + /** + * All four sides of the layers are padded with extra inset so as to provide + * extra content to reveal within the clip path when performing affine transformations on the + * layers. + * + * @see #getForeground() and #getBackground() for more info on how this value is used + */ + public static float getExtraInsetFraction() { + return EXTRA_INSET_PERCENTAGE; + } + + public static float getExtraInsetPercentage() { + return EXTRA_INSET_PERCENTAGE; + } + + /** + * When called before the bound is set, the returned path is identical to + * R.string.config_icon_mask. After the bound is set, the + * returned path's computed bound is same as the #getBounds(). + * + * @return the mask path object used to clip the drawable + */ + public Path getIconMask() { + return mMask; + } + + /** + * Returns the foreground drawable managed by this class. The bound of this drawable is + * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by + * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides. + * + * @return the foreground drawable managed by this drawable + */ + public Drawable getForeground() { + return mLayerState.mChildren[FOREGROUND_ID].mDrawable; + } + + /** + * Returns the foreground drawable managed by this class. The bound of this drawable is + * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by + * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides. + * + * @return the background drawable managed by this drawable + */ + public Drawable getBackground() { + return mLayerState.mChildren[BACKGROUND_ID].mDrawable; + } + + @Override + protected void onBoundsChange(Rect bounds) { + if (bounds.isEmpty()) { + return; + } + updateLayerBounds(bounds); + } + + private void updateLayerBounds(Rect bounds) { + try { + suspendChildInvalidation(); + updateLayerBoundsInternal(bounds); + updateMaskBoundsInternal(bounds); + } finally { + resumeChildInvalidation(); + } + } + + /** + * Set the child layer bounds bigger than the view port size by {@link #DEFAULT_VIEW_PORT_SCALE} + */ + private void updateLayerBoundsInternal(Rect bounds) { + int cX = bounds.width() / 2; + int cY = bounds.height() / 2; + + for (int i = 0, count = LayerState.N_CHILDREN; i < count; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r == null) { + continue; + } + final Drawable d = r.mDrawable; + if (d == null) { + continue; + } + + int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2)); + int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2)); + final Rect outRect = mTmpOutRect; + outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight); + + d.setBounds(outRect); + } + } + + private void updateMaskBoundsInternal(Rect b) { + mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE); + sMask.transform(mMaskMatrix, mMask); + + if (mMaskBitmap == null || mMaskBitmap.getWidth() != b.width() || + mMaskBitmap.getHeight() != b.height()) { + mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8); + mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); + } + // mMaskBitmap bound [0, w] x [0, h] + mCanvas.setBitmap(mMaskBitmap); + mPaint.setShader(null); + mPaint.setColor(0xFFFFFFFF); + mCanvas.drawPath(mMask, mPaint); + + // mMask bound [left, top, right, bottom] + mMaskMatrix.postTranslate(b.left, b.top); + mMask.reset(); + sMask.transform(mMaskMatrix, mMask); + // reset everything that depends on the view bounds + mTransparentRegion.setEmpty(); + mLayersShader = null; + } + + @Override + public void draw(Canvas canvas) { + if (mLayersBitmap == null) { + return; + } + if (mLayersShader == null) { + mCanvas.setBitmap(mLayersBitmap); + mCanvas.drawColor(Color.BLACK); + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + if (mLayerState.mChildren[i] == null) { + continue; + } + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.draw(mCanvas); + } + } + mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP); + if (mUseMyUglyWorkaround) { + // TODO: remove this ugly and slow code + if (mMaskBitmap != null) { + int width = mLayersBitmap.getWidth(); + int height = mLayersBitmap.getHeight(); + int[] colors = new int[width * height]; + int[] alphas = new int[width * height]; + mLayersBitmap.getPixels(colors, 0, width, 0, 0, width, height); + mMaskBitmap.getPixels(alphas, 0, width, 0, 0, width, height); + int color, alpha, index; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + index = i * height + j; + color = colors[index]; + alpha = alphas[index]; + colors[index] = color & 0x00FFFFFF | alpha & 0xFF000000; + } + } + mLayersBitmap.setPixels(colors, 0, width, 0, 0, width, height); + } + } else { + mPaint.setShader(mLayersShader); + } + } + if (mMaskBitmap != null) { + Rect bounds = getBounds(); + canvas.drawBitmap(mUseMyUglyWorkaround ? mLayersBitmap : mMaskBitmap, bounds.left, + bounds.top, mPaint); + } + } + + @Override + public void invalidateSelf() { + mLayersShader = null; + super.invalidateSelf(); + } + + @Override + public void getOutline(Outline outline) { + outline.setConvexPath(mMask); + } + + public Region getSafeZone() { + mMaskMatrix.reset(); + mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), + getBounds().centerY()); + Path p = new Path(); + mMask.transform(mMaskMatrix, p); + Region safezoneRegion = new Region(getBounds()); + safezoneRegion.setPath(p, safezoneRegion); + return safezoneRegion; + } + + @Override + public Region getTransparentRegion() { + if (mTransparentRegion.isEmpty()) { + mMask.toggleInverseFillType(); + mTransparentRegion.set(getBounds()); + mTransparentRegion.setPath(mMask, mTransparentRegion); + mMask.toggleInverseFillType(); + } + return mTransparentRegion; + } + + /** + * Inflates child layers using the specified parser. + */ + private void inflateLayers(Resources r, XmlPullParser parser, + AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + final LayerState state = mLayerState; + + final int innerDepth = parser.getDepth() + 1; + int type; + int depth; + int childIndex; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (depth > innerDepth) { + continue; + } + String tagName = parser.getName(); + switch (tagName) { + case "background": + childIndex = BACKGROUND_ID; + break; + case "foreground": + childIndex = FOREGROUND_ID; + break; + default: + continue; + } + + final ChildDrawable layer = new ChildDrawable(state.mDensity); + final TypedArray a = obtainAttributes(r, theme, attrs, + new int[]{android.R.attr.drawable}); + updateLayerFromTypedArray(layer, a); + a.recycle(); + + // If the layer doesn't have a drawable or unresolved theme + // attribute for a drawable, attempt to parse one from the child + // element. If multiple child elements exist, we'll only use the + // first one. + if (layer.mDrawable == null && (layer.mThemeAttrs == null)) { + while ((type = parser.next()) == XmlPullParser.TEXT) { + } + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException(parser.getPositionDescription() + + ": or tag requires a 'drawable'" + + "attribute or child tag defining a drawable"); + } + + // We found a child drawable. Take ownership. + layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme); + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + addLayer(childIndex, layer); + } + } + + private void updateLayerFromTypedArray(ChildDrawable layer, TypedArray a) { + final LayerState state = mLayerState; + + // Account for any configuration changes. + state.mChildrenChangingConfigurations |= a.getChangingConfigurations(); + + // Extract the theme attributes, if any. + layer.mThemeAttrs = invoke(methodExtractThemeAttrs, a); + + @SuppressLint("ResourceType") Drawable dr = getDrawable(a, 0); + if (dr != null) { + if (layer.mDrawable != null) { + // It's possible that a drawable was already set, in which case + // we should clear the callback. We may have also integrated the + // drawable's changing configurations, but we don't have enough + // information to revert that change. + layer.mDrawable.setCallback(null); + } + + // Take ownership of the new drawable. + layer.mDrawable = dr; + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + } + + private Drawable getDrawable(TypedArray a, int index) { + final TypedValue value = new TypedValue(); + a.getValue(index, value); + if (value.resourceId != 0) { + return a.getResources().getDrawableForDensity(value.resourceId, DisplayMetrics.DENSITY_DEFAULT, null); + } + return null; + } + + @Override + public boolean canApplyTheme() { + return (mLayerState != null && mLayerState.canApplyTheme()) || super.canApplyTheme(); + } + + /** + * Temporarily suspends child invalidation. + * + * @see #resumeChildInvalidation() + */ + private void suspendChildInvalidation() { + mSuspendChildInvalidation = true; + } + + /** + * Resumes child invalidation after suspension, immediately performing an + * invalidation if one was requested by a child during suspension. + * + * @see #suspendChildInvalidation() + */ + private void resumeChildInvalidation() { + mSuspendChildInvalidation = false; + + if (mChildRequestedInvalidation) { + mChildRequestedInvalidation = false; + invalidateSelf(); + } + } + + @Override + public void invalidateDrawable(Drawable who) { + if (mSuspendChildInvalidation) { + mChildRequestedInvalidation = true; + } else { + invalidateSelf(); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() | mLayerState.getChangingConfigurations(); + } + + @Override + public void setHotspot(float x, float y) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspot(x, y); + } + } + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspotBounds(left, top, right, bottom); + } + } + + if (mHotspotBounds == null) { + mHotspotBounds = new Rect(left, top, right, bottom); + } else { + mHotspotBounds.set(left, top, right, bottom); + } + } + + @Override + public void getHotspotBounds(Rect outRect) { + if (mHotspotBounds != null) { + outRect.set(mHotspotBounds); + } else { + super.getHotspotBounds(outRect); + } + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + final boolean changed = super.setVisible(visible, restart); + final ChildDrawable[] array = mLayerState.mChildren; + + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setVisible(visible, restart); + } + } + + return changed; + } + + @Override + public void setDither(boolean dither) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setDither(dither); + } + } + } + + @Override + public void setAlpha(int alpha) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAlpha(alpha); + } + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setColorFilter(colorFilter); + } + } + } + + @Override + public void setTintList(ColorStateList tint) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = LayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintList(tint); + } + } + } + + @Override + public void setTintMode(Mode tintMode) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = LayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintMode(tintMode); + } + } + } + + public void setOpacity(int opacity) { + mLayerState.mOpacityOverride = opacity; + } + + @Override + public int getOpacity() { + if (mLayerState.mOpacityOverride != PixelFormat.UNKNOWN) { + return mLayerState.mOpacityOverride; + } + return mLayerState.getOpacity(); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mLayerState.mAutoMirrored = mirrored; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAutoMirrored(mirrored); + } + } + } + + @Override + public boolean isAutoMirrored() { + return mLayerState.mAutoMirrored; + } + + @Override + public void jumpToCurrentState() { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.jumpToCurrentState(); + } + } + } + + @Override + public boolean isStateful() { + return mLayerState.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful() && dr.setState(state)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + protected boolean onLevelChange(int level) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.setLevel(level)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + public int getIntrinsicWidth() { + return (int) (getMaxIntrinsicWidth() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicWidth() { + int width = -1; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r.mDrawable == null) { + continue; + } + final int w = r.mDrawable.getIntrinsicWidth(); + if (w > width) { + width = w; + } + } + return width; + } + + @Override + public int getIntrinsicHeight() { + return (int) (getMaxIntrinsicHeight() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicHeight() { + int height = -1; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r.mDrawable == null) { + continue; + } + final int h = r.mDrawable.getIntrinsicHeight(); + if (h > height) { + height = h; + } + } + return height; + } + + @Override + public ConstantState getConstantState() { + if (mLayerState.canConstantState()) { + mLayerState.mChangingConfigurations = getChangingConfigurations(); + return mLayerState; + } + return null; + } + + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mLayerState = createConstantState(mLayerState, null); + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.mutate(); + } + } + mMutated = true; + } + return this; + } + + protected static TypedArray obtainAttributes(Resources res, + Theme theme, AttributeSet set, int[] attrs) { + if (theme == null) { + return res.obtainAttributes(set, attrs); + } + return theme.obtainStyledAttributes(set, attrs, 0, 0); + } + + static class ChildDrawable { + public Drawable mDrawable; + public int[] mThemeAttrs; + public int mDensity; + + ChildDrawable(int density) { + mDensity = density; + } + + ChildDrawable(ChildDrawable orig, AdaptiveIconCompat owner, + Resources res) { + + final Drawable dr = orig.mDrawable; + final Drawable clone; + if (dr != null) { + final ConstantState cs = dr.getConstantState(); + if (cs == null) { + clone = dr; + } else if (res != null) { + clone = cs.newDrawable(res); + } else { + clone = cs.newDrawable(); + } + clone.setCallback(owner); + clone.setBounds(dr.getBounds()); + clone.setLevel(dr.getLevel()); + } else { + clone = null; + } + + mDrawable = clone; + mThemeAttrs = orig.mThemeAttrs; + + mDensity = resolveDensity(res, orig.mDensity); + } + + public boolean canApplyTheme() { + return mThemeAttrs != null + || (mDrawable != null && mDrawable.canApplyTheme()); + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + } + + static class LayerState extends ConstantState { + private int[] mThemeAttrs; + + final static int N_CHILDREN = 2; + ChildDrawable[] mChildren; + + // The density at which to render the drawable and its children. + int mDensity; + + // The density to use when inflating/looking up the children drawables. A value of 0 means + // use the system's density. + int mSrcDensityOverride = 0; + + int mOpacityOverride = PixelFormat.UNKNOWN; + + int mChangingConfigurations; + int mChildrenChangingConfigurations; + + private boolean mCheckedOpacity; + private int mOpacity; + + private boolean mCheckedStateful; + private boolean mIsStateful; + private boolean mAutoMirrored = false; + + LayerState(LayerState orig, AdaptiveIconCompat owner, + Resources res) { + mDensity = resolveDensity(res, orig != null ? orig.mDensity : 0); + mChildren = new ChildDrawable[N_CHILDREN]; + if (orig != null) { + final ChildDrawable[] origChildDrawable = orig.mChildren; + + mChangingConfigurations = orig.mChangingConfigurations; + mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations; + + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable or = origChildDrawable[i]; + mChildren[i] = new ChildDrawable(or, owner, res); + } + + mCheckedOpacity = orig.mCheckedOpacity; + mOpacity = orig.mOpacity; + mCheckedStateful = orig.mCheckedStateful; + mIsStateful = orig.mIsStateful; + mAutoMirrored = orig.mAutoMirrored; + mThemeAttrs = orig.mThemeAttrs; + mOpacityOverride = orig.mOpacityOverride; + mSrcDensityOverride = orig.mSrcDensityOverride; + } else { + for (int i = 0; i < N_CHILDREN; i++) { + mChildren[i] = new ChildDrawable(mDensity); + } + } + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + + @Override + public boolean canApplyTheme() { + if (mThemeAttrs != null || super.canApplyTheme()) { + return true; + } + + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable layer = array[i]; + if (layer.canApplyTheme()) { + return true; + } + } + return false; + } + + + @Override + public Drawable newDrawable() { + return new AdaptiveIconCompat(this, null); + } + + + @Override + public Drawable newDrawable(Resources res) { + return new AdaptiveIconCompat(this, null); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations + | mChildrenChangingConfigurations; + } + + public final int getOpacity() { + if (mCheckedOpacity) { + return mOpacity; + } + + final ChildDrawable[] array = mChildren; + + // Seek to the first non-null drawable. + int firstIndex = -1; + for (int i = 0; i < N_CHILDREN; i++) { + if (array[i].mDrawable != null) { + firstIndex = i; + break; + } + } + + int op; + if (firstIndex >= 0) { + op = array[firstIndex].mDrawable.getOpacity(); + } else { + op = PixelFormat.TRANSPARENT; + } + + // Merge all remaining non-null drawables. + for (int i = firstIndex + 1; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + op = Drawable.resolveOpacity(op, dr.getOpacity()); + } + } + + mOpacity = op; + mCheckedOpacity = true; + return op; + } + + public final boolean isStateful() { + if (mCheckedStateful) { + return mIsStateful; + } + + final ChildDrawable[] array = mChildren; + boolean isStateful = false; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful()) { + isStateful = true; + break; + } + } + + mIsStateful = isStateful; + mCheckedStateful = true; + return isStateful; + } + + public final boolean canConstantState() { + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.getConstantState() == null) { + return false; + } + } + + // Don't cache the result, this method is not called very often. + return true; + } + + public void invalidateCache() { + mCheckedOpacity = false; + mCheckedStateful = false; + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt new file mode 100644 index 0000000000000000000000000000000000000000..e7a98f02b041ad710983ed424ecda6c7d03ec691 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt @@ -0,0 +1,131 @@ +package foundation.e.blisslauncher.common.compat + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.graphics.Rect +import android.os.Bundle +import android.os.UserHandle + +/** + * Interface repository of [android.content.pm.LauncherApps] + */ +interface LauncherAppsCompat { + + /** + * Wrapper callback for [android.content.pm.LauncherApps.Callback] + */ + interface OnAppsChangedCallbackCompat { + fun onPackageRemoved( + packageName: String, + user: UserHandle + ) + + fun onPackageAdded( + packageName: String, + user: UserHandle + ) + + fun onPackageChanged( + packageName: String, + user: UserHandle + ) + + fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) + + fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) + + fun onPackagesSuspended( + packageNames: Array, + user: UserHandle + ) + + fun onPackagesUnsuspended( + packageNames: Array, + user: UserHandle + ) + + fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) + } + + /** + * Returns a list of Activities for a given package and user handle. + * + * If packageName is null, then it returns all the activities + * installed on device for the given user. + */ + fun getActivityList( + packageName: String?, + user: UserHandle? + ): List + + /** + * @see android.content.pm.LauncherApps.resolveActivity + */ + fun resolveActivity( + intent: Intent?, + user: UserHandle? + ): LauncherActivityInfo? + + fun startActivityForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) + + /** + * @see android.content.pm.LauncherApps.getApplicationInfo + */ + fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? + + fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) + + fun addOnAppsChangedCallback(listener: OnAppsChangedCallbackCompat) + + fun removeOnAppsChangedCallback(listener: OnAppsChangedCallbackCompat) + + fun isPackageEnabledForProfile( + packageName: String?, + user: UserHandle? + ): Boolean + + fun isActivityEnabledForProfile( + component: ComponentName?, + user: UserHandle? + ): Boolean + + //TODO + /*abstract fun getCustomShortcutActivityList( + packageUser: PackageUserKey? + ): List?*/ + + fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle? + ) { + showAppDetailsForProfile(component, user, null, null) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b34c4d7a7b7eb83140c168ac642319ff8e51d66 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt @@ -0,0 +1,74 @@ +package foundation.e.blisslauncher.common.compat + +import android.annotation.TargetApi +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.os.Build +import android.os.UserHandle + +/** + * Wrapper class for [android.content.pm.ShortcutInfo] representing deep shortcuts into apps. + */ +@TargetApi(Build.VERSION_CODES.N_MR1) +class ShortcutInfoCompat(private val shortcutInfo: ShortcutInfo) { + + @TargetApi(Build.VERSION_CODES.N) + fun makeIntent(): Intent = Intent(Intent.ACTION_MAIN) + .addCategory(INTENT_CATEGORY) + .setComponent(getActivity()) + .setPackage(getPackage()) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + .putExtra(EXTRA_SHORTCUT_ID, getId()) + + fun getShortcutInfo(): ShortcutInfo = shortcutInfo + + fun getPackage(): String = shortcutInfo.`package` + + //@RequiresApi(Build.VERSION_CODES.N_MR1) + fun getBadgePackage(context: Context): String? { + val whitelistedPkg = "" + return if (whitelistedPkg == getPackage() && shortcutInfo.extras.containsKey( + EXTRA_BADGEPKG + ) + ) { + shortcutInfo.getExtras() + .getString(EXTRA_BADGEPKG) + } else getPackage() + } + + fun getId(): String = shortcutInfo.id + + fun getShortLabel(): CharSequence? = shortcutInfo.shortLabel + + fun getLongLabel(): CharSequence? = shortcutInfo.longLabel + + fun getLastChangedTimestamp(): Long = shortcutInfo.getLastChangedTimestamp() + + fun getActivity(): ComponentName? = shortcutInfo.getActivity() + + fun getUserHandle(): UserHandle = shortcutInfo.getUserHandle() + + fun hasKeyFieldsOnly(): Boolean = shortcutInfo.hasKeyFieldsOnly() + + fun isPinned(): Boolean = shortcutInfo.isPinned() + + fun isDeclaredInManifest(): Boolean = shortcutInfo.isDeclaredInManifest() + + fun isEnabled(): Boolean = shortcutInfo.isEnabled() + + fun isDynamic(): Boolean = shortcutInfo.isDynamic() + + fun getRank(): Int = shortcutInfo.getRank() + + fun getDisabledMessage(): CharSequence? = shortcutInfo.getDisabledMessage() + + override fun toString(): String = shortcutInfo.toString() + + companion object { + private const val INTENT_CATEGORY = "com.android.launcher3.DEEP_SHORTCUT" + private const val EXTRA_BADGEPKG = "badge_package" + const val EXTRA_SHORTCUT_ID = "shortcut_id" + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt new file mode 100644 index 0000000000000000000000000000000000000000..fb312f2111d255f546511db460893fc518d93e43 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt @@ -0,0 +1,5 @@ +package foundation.e.blisslauncher.common.executors + +import java.util.concurrent.Executor + +data class AppExecutors(val io: Executor, val computation: Executor, val main: Executor) diff --git a/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt b/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt new file mode 100644 index 0000000000000000000000000000000000000000..0aa7a57f9009ff6aacabfa8e9bc00d87f9a0e514 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.common.executors + +import android.os.Handler +import android.os.Looper +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.TimeUnit + +class MainThreadExecutor : AbstractExecutorService() { + + private val mHandler: Handler = Handler(Looper.getMainLooper()) + + override fun shutdown(): Unit = throw UnsupportedOperationException() + + override fun shutdownNow(): List = throw UnsupportedOperationException() + + override fun isShutdown(): Boolean = false + + override fun isTerminated(): Boolean = false + + @Throws(InterruptedException::class) + override fun awaitTermination( + timeout: Long, + unit: TimeUnit + ): Boolean = throw UnsupportedOperationException() + + override fun execute(runnable: Runnable) { + if (mHandler.looper == Looper.myLooper()) { + runnable.run() + } else { + mHandler.post(runnable) + } + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt b/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..56971cc9d3a1d1150ef6e108413d488978baac9e --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.common.extensions diff --git a/common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt b/common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..273c385a2a814cd8c10ae62715606899766c776a --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.common.graphics + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.SparseArray +import foundation.e.blisslauncher.common.Utilities + +/** + * Utility class for extracting colors from a bitmap. + */ +object ColorExtractor { + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + * @param samples The approximate max number of samples to use. + */ + @JvmOverloads + fun findDominantColorByHue(bitmap: Bitmap, samples: Int = 20): Int { + val height = bitmap.height + val width = bitmap.width + var sampleStride = Math.sqrt(height * width / samples.toDouble()).toInt() + if (sampleStride < 1) { + sampleStride = 1 + } + + // This is an out-param, for getting the hsv values for an rgb + val hsv = FloatArray(3) + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + val hueScoreHistogram = FloatArray(360) + var highScore = -1f + var bestHue = -1 + val pixels = IntArray(samples) + var pixelCount = 0 + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + val argb = bitmap.getPixel(x, y) + val alpha = 0xFF and (argb shr 24) + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + x += sampleStride + continue + } + // Remove the alpha channel. + val rgb = argb or -0x1000000 + Color.colorToHSV(rgb, hsv) + // Bucket colors by the 360 integer hues. + val hue = hsv[0].toInt() + if (hue < 0 || hue >= hueScoreHistogram.size) { + // Defensively avoid array bounds violations. + x += sampleStride + continue + } + if (pixelCount < samples) { + pixels[pixelCount++] = rgb + } + val score = hsv[1] * hsv[2] + hueScoreHistogram[hue] += score + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue] + bestHue = hue + } + x += sampleStride + } + y += sampleStride + } + val rgbScores = SparseArray() + var bestColor = -0x1000000 + highScore = -1f + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (i in 0 until pixelCount) { + val rgb = pixels[i] + Color.colorToHSV(rgb, hsv) + val hue = hsv[0].toInt() + if (hue == bestHue) { + val s = hsv[1] + val v = hsv[2] + val bucket = (s * 100).toInt() + (v * 10000).toInt() + // Score by cumulative saturation * value. + val score = s * v + val oldTotal = rgbScores[bucket] + val newTotal = if (oldTotal == null) score else oldTotal + score + rgbScores.put(bucket, newTotal) + if (newTotal > highScore) { + highScore = newTotal + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb + } + } + } + return bestColor + } + + fun isSingleColor(drawable: Drawable?, color: Int): Boolean { + if (drawable == null) return true + val testColor = posterize(color) + if (drawable is ColorDrawable) { + return posterize(drawable.color) == testColor + } + val bitmap: Bitmap = Utilities.drawableToBitmap(drawable) ?: return false + val height = bitmap.height + val width = bitmap.width + val pixels = IntArray(height * width) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + val set: Set = HashSet(pixels.asList()) + val distinctPixels = set.toIntArray() + for (pixel in distinctPixels) { + if (testColor != posterize(pixel)) { + return false + } + } + return true + } + + private const val MAGIC_NUMBER = 25 + + /* + * References: + * https://www.cs.umb.edu/~jreyes/csit114-fall-2007/project4/filters.html#posterize + * https://github.com/gitgraghu/image-processing/blob/master/src/Effects/Posterize.java + */ + fun posterize(rgb: Int): Int { + var red = 0xff and (rgb shr 16) + var green = 0xff and (rgb shr 8) + var blue = 0xff and rgb + red -= red % MAGIC_NUMBER + green -= green % MAGIC_NUMBER + blue -= blue % MAGIC_NUMBER + if (red < 0) { + red = 0 + } + if (green < 0) { + green = 0 + } + if (blue < 0) { + blue = 0 + } + return red shl 16 or (green shl 8) or blue + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt b/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0a9ebad9fffa0981a650ab557b1b862877c82d9 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.common.inject + +import javax.inject.Scope + +@Scope +annotation class PerActivity diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt b/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt new file mode 100644 index 0000000000000000000000000000000000000000..c15827349f9de9c1efba09eb774dd6248a22a315 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.common.util + +import java.text.Collator + +class LabelComparator : Comparator { + + private val collator = Collator.getInstance() + override fun compare(titleA: String, titleB: String): Int { + + // Ensure that we de-prioritize any titles that don't start with a + // linguistic letter or digit + val aStartsWithLetter = titleA.isNotEmpty() && + Character.isLetterOrDigit(titleA.codePointAt(0)) + val bStartsWithLetter = titleB.isNotEmpty() && + Character.isLetterOrDigit(titleB.codePointAt(0)) + if (aStartsWithLetter && !bStartsWithLetter) { + return -1 + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1 + } + + // Order by the title in the current locale + return collator.compare(titleA, titleB) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f010295282e26c8dffdecbc71b8774baf51ad4a --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt @@ -0,0 +1,39 @@ +package foundation.e.blisslauncher.common.util + +import android.util.LongSparseArray + +/** + * Extension of [LongSparseArray] with some utility methods. + */ +class LongArrayMap : LongSparseArray(), + Iterable { + fun containsKey(key: Long): Boolean { + return indexOfKey(key) >= 0 + } + + val isEmpty: Boolean + get() = size() <= 0 + + override fun clone(): LongArrayMap { + return super.clone() as LongArrayMap + } + + override fun iterator(): Iterator { + return ValueIterator() + } + + internal inner class ValueIterator : MutableIterator { + private var mNextIndex = 0 + override fun hasNext(): Boolean { + return mNextIndex < size() + } + + override fun next(): E { + return valueAt(mNextIndex++) + } + + override fun remove() { + throw UnsupportedOperationException() + } + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java new file mode 100644 index 0000000000000000000000000000000000000000..8626a5b1c3279c5bbed3c2ed41968cc205f2e4ef --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.common.util; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A utility map from keys to an ArrayList of values. + */ +public class MultiHashMap extends HashMap> { + + public MultiHashMap() { } + + public MultiHashMap(int size) { + super(size); + } + + public void addToList(K key, V value) { + ArrayList list = get(key); + if (list == null) { + list = new ArrayList<>(); + list.add(value); + put(key, list); + } else { + list.add(value); + } + } + + @Override + public MultiHashMap clone() { + MultiHashMap map = new MultiHashMap<>(size()); + for (Entry> entry : entrySet()) { + map.put(entry.getKey(), new ArrayList(entry.getValue())); + } + return map; + } +} diff --git a/common/src/main/res/values-sw340dp/dimens.xml b/common/src/main/res/values-sw340dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..f7e3f58ed0592b4b7ed1fafdfef4522e9d9daa6e --- /dev/null +++ b/common/src/main/res/values-sw340dp/dimens.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values-sw600dp/config.xml b/common/src/main/res/values-sw600dp/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb9af979382b174400d42527cd7058fd2b3131e5 --- /dev/null +++ b/common/src/main/res/values-sw600dp/config.xml @@ -0,0 +1,4 @@ + + true + true + diff --git a/common/src/main/res/values-sw600dp/dimens.xml b/common/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..15b6193b366b61dc59180cfabbd0d224d9a8ebcb --- /dev/null +++ b/common/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/common/src/main/res/values-sw720dp/config.xml b/common/src/main/res/values-sw720dp/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..c536dbe88b6c0481f157dc500cdbeadfd202b4b1 --- /dev/null +++ b/common/src/main/res/values-sw720dp/config.xml @@ -0,0 +1,14 @@ + + true + true + + + + + true + diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..c30c90dbd4bcac76b9874d75820c28c4903b9dd9 --- /dev/null +++ b/common/src/main/res/values/attrs.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values/config.xml b/common/src/main/res/values/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5c1d0b20afebd0ef6b31dd9524c0d1c12a9f399 --- /dev/null +++ b/common/src/main/res/values/config.xml @@ -0,0 +1,12 @@ + + + false + false + false + false + + + true + 90 + + \ No newline at end of file diff --git a/common/src/main/res/values/dimens.xml b/common/src/main/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..ade66cc90b3d48063dd94cc8702ff40e4372794d --- /dev/null +++ b/common/src/main/res/values/dimens.xml @@ -0,0 +1,30 @@ + + + + 8dp + 1dp + 8dp + 8dp + 8dp + + 8dp + + 5.5dp + 0dp + 8dp + + 8dp + 2dp + 80dp + 0dp + + 9dp + 6dp + 13sp + 4dp + 12dp + 14sp + 48dp + 24dp + + \ No newline at end of file diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..9e44af55ba463da4596227252f9aa4e192a195f8 --- /dev/null +++ b/common/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + common + diff --git a/common/src/main/res/xml/device_profiles.xml b/common/src/main/res/xml/device_profiles.xml new file mode 100644 index 0000000000000000000000000000000000000000..73d8156a4d11c5afab53d08fa469e1826e81b356 --- /dev/null +++ b/common/src/main/res/xml/device_profiles.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/xml/dw_hotseat_3.xml b/common/src/main/res/xml/dw_hotseat_3.xml new file mode 100644 index 0000000000000000000000000000000000000000..3306993fa1d6557e96b1f71b83293ba98afecabd --- /dev/null +++ b/common/src/main/res/xml/dw_hotseat_3.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/dw_hotseat_4.xml b/common/src/main/res/xml/dw_hotseat_4.xml new file mode 100644 index 0000000000000000000000000000000000000000..a3139d361d89bdcd920e3097d1c7c92b0045bd71 --- /dev/null +++ b/common/src/main/res/xml/dw_hotseat_4.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/dw_hotseat_5.xml b/common/src/main/res/xml/dw_hotseat_5.xml new file mode 100644 index 0000000000000000000000000000000000000000..74f73869481de586610bca79cb6953cd4b94d4ff --- /dev/null +++ b/common/src/main/res/xml/dw_hotseat_5.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/dw_hotseat_6.xml b/common/src/main/res/xml/dw_hotseat_6.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce33eadefd34eecbdfca5c6f26b576e2bd7c74a6 --- /dev/null +++ b/common/src/main/res/xml/dw_hotseat_6.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data-bridge/.gitignore b/data-bridge/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/data-bridge/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data-bridge/build.gradle b/data-bridge/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..d475358d6b917b5fdb15605e6c2ba2c55d1c1a0b --- /dev/null +++ b/data-bridge/build.gradle @@ -0,0 +1,5 @@ +apply from: '../common.gradle' + +dependencies { + implementation project(path: ':data') +} diff --git a/data-bridge/src/main/AndroidManifest.xml b/data-bridge/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b6809ed3686ee717ef2f072038d3dc485667c48 --- /dev/null +++ b/data-bridge/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt b/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..820d15ca4082444ebb8bf5314fa89ea1da593275 --- /dev/null +++ b/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.databridge + +import android.content.Context +import foundation.e.blisslauncher.data.DataLayerInitializer + +object DataBridgeInitializer { + private val dataInitializer: DataLayerInitializer by lazy { DataLayerInitializer() } + + fun initialize(appContext: Context) { + dataInitializer.initialize(appContext) + } +} \ No newline at end of file diff --git a/data/build.gradle b/data/build.gradle index 2e3d8bef1a129ba62261e72536de0005069fd18a..b6c74afcddaca0b97819184309e445df0bde537a 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -29,19 +29,34 @@ android { } } } + + lintOptions { + abortOnError false + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(path: ':domain') + implementation project(path: ':common') + implementation Libs.Kotlin.stdlib implementation Libs.AndroidX.appcompat implementation Libs.AndroidX.coreKtx + implementation Libs.RxJava.rxKotlin + implementation Libs.AndroidX.Room.runtime + implementation Libs.AndroidX.Room.ktx kapt Libs.AndroidX.Room.compiler + implementation Libs.Dagger.dagger kapt Libs.Dagger.compiler + // Timber + implementation Libs.timber + testImplementation Libs.junit testImplementation Libs.robolectric testImplementation Libs.mockK diff --git a/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt b/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..0047c0299aa54fa435647f8dbcc6373083fce61a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt @@ -0,0 +1,18 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import foundation.e.blisslauncher.data.inject.DaggerDataComponent +import foundation.e.blisslauncher.data.inject.DataComponent +import foundation.e.blisslauncher.domain.inject.DomainComponent + +class DataLayerInitializer { + fun initialize(appContext: Context): DataComponent { + return initializeDataComponent(appContext) + } + + private fun initializeDataComponent(appContext: Context): DataComponent { + val dataComponent = DaggerDataComponent.factory().create(appContext) + DomainComponent.INSTANCE = dataComponent + return dataComponent + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt new file mode 100644 index 0000000000000000000000000000000000000000..c2eb0ad959bd20c63ada2eb433732057746f140e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -0,0 +1,153 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.content.SharedPreferences +import androidx.room.Room +import androidx.room.RoomDatabase.Callback +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteDatabase +import foundation.e.blisslauncher.data.database.BlissLauncherDatabase +import foundation.e.blisslauncher.data.database.BlissLauncherFiles +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +class LauncherDatabaseGateway @Inject constructor( + private val context: Context, + private val sharedPrefs: SharedPreferences +) { + + private lateinit var db: BlissLauncherDatabase + + private var maxItemId: Long = -1 + private var maxScreenId: Long = -1 + + private fun initIds() { + if (maxItemId == -1L) { + initializeMaxItemId() + } + + if (maxScreenId == -1L) { + initializeMaxScreenId() + } + } + + private fun initializeMaxItemId() { + getMaxId(db, "launcherItems") + } + + private fun initializeMaxScreenId() { + getMaxId(db, "workspaceScreens") + } + + private fun getMaxId( + db: BlissLauncherDatabase, + tableName: String + ) { + Observable.fromCallable { + db.launcherDao().getMaxIdInTable(SimpleSQLiteQuery("SELECT MAX(_id) FROM $tableName")) + }.subscribeOn(Schedulers.io()) + .subscribe { + if (it == -1L) { + throw RuntimeException("Error: could not query max id in $tableName") + } + if (tableName == "launcherItems") + maxItemId = it + else if (tableName == "workspaceScreens") + maxScreenId = it + else throw IllegalArgumentException("Error: specified table doesn't match") + } + } + + fun createEmptyDatabase() { + db.launcherDao().createEmptyDb() + } + + fun insertAndCheck(item: WorkspaceItem): Long { + checkItemId(item) + return db.launcherDao().insert(item) + } + + fun checkItemId(item: WorkspaceItem) { + val id = item._id + maxItemId = Math.max(id, maxItemId) + } + + fun generateNewItemId(): Long { + if (maxItemId < 0) { + throw RuntimeException("Error: max item id was not initialized") + } + maxItemId += 1 + return maxItemId + } + + fun generateNewScreenId(): Long { + if (maxScreenId < 0) { + throw RuntimeException("Error: max screen id was not initialized") + } + + maxScreenId += 1 + return maxScreenId + } + + fun deleteEmptyFolders() {} + + fun getAllWorkspaceItems(): List = db.launcherDao().getAllWorkspaceItems() + + fun loadWorkspaceScreensInOrder(): List = + db.launcherDao().getAllWorkspaceScreens().map { it._id } + + fun markDeleted(id: Long) {} + + fun markDeleted(item: WorkspaceItem) { + markDeleted(item._id) + } + + fun saveAll(items: ArrayList) { + db.launcherDao().insertAll(items) + } + + // Helper function to initialise the database + fun createDbIfNotExist() { + db = Room.databaseBuilder( + context, + BlissLauncherDatabase::class.java, + BlissLauncherFiles.LAUNCHER_DB + ).addCallback( + object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + Timber.d("Room database created") + maxItemId = 0 + maxScreenId = 0 + sharedPrefs.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit() + } + + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + Timber.d("Room database opened") + initIds() + } + } + ).build() + db.openHelper.readableDatabase + } + + fun updateWorkspaceScreenOrder(screenIds: ArrayList) { + val list = ArrayList() + for(i in screenIds.indices) { + val id = screenIds[i] + if(id >= 0) { + list.add(WorkspaceScreen(id, i)) + } + } + db.launcherDao().insertAllWorkspaceScreens(list) + } + + companion object { + const val EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED" + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..4678f85d675c7070f1e9a6741f7defd2f974ffa4 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt @@ -0,0 +1,43 @@ +package foundation.e.blisslauncher.data + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +class LauncherProvider : ContentProvider() { + override fun insert(uri: Uri, values: ContentValues?): Uri? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCreate(): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getType(uri: Uri): String? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3da819487cb6545bad18535c2bdd0bdcf5d86a1 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt @@ -0,0 +1,59 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.os.Process +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.data.notification.NotificationListener +import foundation.e.blisslauncher.data.receiver.ConfigChangedReceiver +import foundation.e.blisslauncher.data.receiver.ProfileReceiver +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LauncherStateManagerImpl @Inject constructor( + private val context: Context, + private val profileReceiver: ProfileReceiver, + private val userManagerRepository: UserManagerRepository, + private val configReceiver: ConfigChangedReceiver, + private val onAppsChangedCallbackCompat: LauncherAppsCompat.OnAppsChangedCallbackCompat, + private val appExecutors: AppExecutors, + private val launcherAppsCompat: LauncherAppsCompat +) : LauncherStateManager { + private lateinit var notificationBadgingObserver: SettingsObserver + + override fun init() { + Timber.d("Initialising Launcher components") + + launcherAppsCompat.addOnAppsChangedCallback(onAppsChangedCallbackCompat) + + profileReceiver.register() + userManagerRepository.enableAndResetCache() + configReceiver.register() + notificationBadgingObserver = object : SettingsObserver.Secure(context.contentResolver) { + override fun onSettingChanged(isNotificationBadgingEnabled: Boolean) { + if (isNotificationBadgingEnabled) { + NotificationListener.requestRebind(context) + } + } + } + notificationBadgingObserver.register(NotificationListener.NOTIFICATION_BADGING) + Timber.d("Initialisation completed") + } + + override fun terminate() { + launcherAppsCompat.removeOnAppsChangedCallback(onAppsChangedCallbackCompat) + profileReceiver.unregister() + configReceiver.unregister() + notificationBadgingObserver.unregister() + } + + fun changeThreadPriority(priority: Int) { + appExecutors.io.execute { + Process.setThreadPriority(priority) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..556b004781df395b6826958bf6dca20d32779f87 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt @@ -0,0 +1,105 @@ +package foundation.e.blisslauncher.data + +import android.app.AppOpsManager +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.Build +import android.os.UserHandle +import android.text.TextUtils +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import javax.inject.Inject + +class PackageManagerHelper @Inject constructor( + context: Context, + private val launcherApps: LauncherAppsCompat +) { + private val pm = context.packageManager + + val isSafeMode + get() = pm.isSafeMode + + /** + * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't + * guarantee that the app is on SD card. + */ + fun isAppOnSdcard( + packageName: String, + user: UserHandle + ): Boolean { + val info = launcherApps.getApplicationInfo( + packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, user + ) + return info != null && info.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0 + } + + /** + * Returns whether the target app is suspended for a given user as per + * [android.app.admin.DevicePolicyManager.isPackageSuspended]. + */ + fun isAppSuspended( + packageName: String, + user: UserHandle + ): Boolean { + val info = launcherApps.getApplicationInfo(packageName, 0, user) + return info != null && info.flags and ApplicationInfo.FLAG_SUSPENDED != 0 + } + + fun isAppSuspended(info: ApplicationInfo): Boolean = + info.flags and ApplicationInfo.FLAG_SUSPENDED != 0 + + fun getAppLaunchIntent(pkg: String, user: UserHandle): Intent? { + val activities = launcherApps.getActivityList(pkg, user) + return if (activities.isEmpty()) null else + ApplicationItem.makeLaunchIntent(activities[0]) + } + + /** + * Returns true if {@param srcPackage} has the permission required to start the activity from + * {@param intent}. If {@param srcPackage} is null, then the activity should not need + * any permissions + */ + fun hasPermissionForActivity( + intent: Intent?, + srcPackage: String? + ): Boolean { + val target: ResolveInfo = pm.resolveActivity(intent, 0) + ?: // Not a valid target + return false + if (TextUtils.isEmpty(target.activityInfo.permission)) { + // No permission is needed + return true + } + if (TextUtils.isEmpty(srcPackage)) { + // The activity requires some permission but there is no source. + return false + } + + // Source does not have sufficient permissions. + if (pm.checkPermission(target.activityInfo.permission, srcPackage) != + PackageManager.PERMISSION_GRANTED + ) { + return false + } + + // On M and above also check AppOpsManager for compatibility mode permissions. + if (TextUtils.isEmpty(AppOpsManager.permissionToOp(target.activityInfo.permission))) { + // There is no app-op for this permission, which could have been disabled. + return true + } + + // There is no direct way to check if the app-op is allowed for a particular app. Since + // app-op is only enabled for apps running in compatibility mode, simply block such apps. + try { + return pm.getApplicationInfo( + srcPackage, + 0 + ).targetSdkVersion >= Build.VERSION_CODES.M + } catch (e: PackageManager.NameNotFoundException) { + } + return false + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java b/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java new file mode 100644 index 0000000000000000000000000000000000000000..4f36ac0a90c08bef8c316b239a73dad22f3f98a5 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.data; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; + +public interface SettingsObserver { + + /** + * Registers the content observer to call {@link #onSettingChanged(boolean)} when any of the + * passed settings change. The value passed to onSettingChanged() is based on the key setting. + */ + void register(String keySetting, String... dependentSettings); + void unregister(); + void onSettingChanged(boolean keySettingEnabled); + + + abstract class Secure extends ContentObserver implements SettingsObserver { + private ContentResolver mResolver; + private String mKeySetting; + + public Secure(ContentResolver resolver) { + super(new Handler()); + mResolver = resolver; + } + + @Override + public void register(String keySetting, String ... dependentSettings) { + mKeySetting = keySetting; + mResolver.registerContentObserver( + Settings.Secure.getUriFor(mKeySetting), false, this); + for (String setting : dependentSettings) { + mResolver.registerContentObserver( + Settings.Secure.getUriFor(setting), false, this); + } + onChange(true); + } + + @Override + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + onSettingChanged(Settings.Secure.getInt(mResolver, mKeySetting, 1) == 1); + } + } + + abstract class System extends ContentObserver implements SettingsObserver { + private ContentResolver mResolver; + private String mKeySetting; + + public System(ContentResolver resolver) { + super(new Handler()); + mResolver = resolver; + } + + @Override + public void register(String keySetting, String ... dependentSettings) { + mKeySetting = keySetting; + mResolver.registerContentObserver( + Settings.System.getUriFor(mKeySetting), false, this); + for (String setting : dependentSettings) { + mResolver.registerContentObserver( + Settings.System.getUriFor(setting), false, this); + } + onChange(true); + } + + @Override + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + onSettingChanged(Settings.System.getInt(mResolver, mKeySetting, 1) == 1); + } + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..f522029ebe44f46686417949b2535f03c8803a30 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt @@ -0,0 +1,687 @@ +package foundation.e.blisslauncher.data + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.LauncherActivityInfo +import android.os.Build +import android.os.Process +import android.os.UserHandle +import android.util.LongSparseArray +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.common.util.LabelComparator +import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.icon.LauncherIcons +import foundation.e.blisslauncher.data.parser.DefaultHotseatParser +import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager +import foundation.e.blisslauncher.data.util.LauncherItemComparator +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.ShortcutItem +import foundation.e.blisslauncher.domain.keys.ShortcutKey +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository +import timber.log.Timber +import java.util.Collections +import javax.inject.Inject + +class WorkspaceRepositoryImpl +@Inject constructor( + private val context: Context, + private val launcherApps: LauncherAppsCompat, + private val launcherDatabase: LauncherDatabaseGateway, + private val userManager: UserManagerRepository, + private val packageManagerHelper: PackageManagerHelper, + private val shortcutManager: PinnedShortcutManager, + private val sharedPrefs: SharedPreferences, + private val idp: InvariantDeviceProfile, + private val launcherItemComparator: LauncherItemComparator, + private val launcherIcons: LauncherIcons +) : WorkspaceRepository { + + override fun loadWorkspace(): WorkspaceModel { + val workspaceModel = WorkspaceModel() + val pmHelper = PackageManagerHelper(context, launcherApps) + val isSafeMode = pmHelper.isSafeMode + val isSdCardReady = Utilities.isBootCompleted() + val pendingPackages = MultiHashMap() + val shortcutKeyToPinnedShortcuts = HashMap() + val allUsers: LongSparseArray = LongSparseArray() + val quietMode: LongSparseArray = LongSparseArray() + val unlockedUsers: LongSparseArray = LongSparseArray() + + var clearDb = false + + //TODO: GridSize Migration Task + /*if (!clearDb && GridSizeMigrationTask.ENABLED && + !GridSizeMigrationTask.migrateGridIfNeeded(context) + ) { + // Migration failed. Clear workspace. + clearDb = true + }*/ + + if (clearDb) { + Timber.d("loadLauncher: resetting launcher database") + clearAllDbs() + } + + val allAppsMap = hashMapOf() + + userManager.userProfiles.forEach { user -> + val serialNo = userManager.getSerialNumberForUser(user) + allUsers.put(serialNo, user) + quietMode.put(serialNo, userManager.isQuietModeEnabled(user)) + + var userUnlocked = userManager.isUserUnlocked(user) + + // Query for pinned shortcuts only when user is unlocked. + if (userUnlocked) { + val pinnedShortcuts = + shortcutManager.queryForPinnedShortcuts(null, user) + if (shortcutManager.wasLastCallSuccess()) { + pinnedShortcuts.map { shortcut -> ShortcutKey.fromShortcutInfoCompat(shortcut) to shortcut } + .toMap(shortcutKeyToPinnedShortcuts) + } else { + // Shortcut Manager can fail due to various reasons. + // Consider this condition as user locked. + userUnlocked = false + } + } + unlockedUsers.put(serialNo, userUnlocked) + launcherApps.getActivityList(null, user).forEach { + allAppsMap[it.componentName] = it + } + } + + loadDefaultWorkspaceIfNecessary(allAppsMap) + + workspaceModel.workspaceScreens.addAll(launcherDatabase.loadWorkspaceScreensInOrder()) + + //Populate item from database and fill necessary details based on users. + val launcherDatabaseItems = launcherDatabase.getAllWorkspaceItems() + Timber.d("LauncherDatabase size is ${launcherDatabaseItems.size}") + Timber.d("Workspace screen size is ${workspaceModel.workspaceScreens.size}") + launcherDatabaseItems + .forEach { + it.apply { + user = allUsers[profileId] + validTarget = + targetPackage.isNullOrEmpty() or launcherApps.isPackageEnabledForProfile( + targetPackage, + user + ) + } + + if (!checkAndValidate( + it, + unlockedUsers, + shortcutKeyToPinnedShortcuts, + isSdCardReady + ) + ) + return@forEach + + val launcherItem = convertToLauncherItem( + it, + quietMode, + isSdCardReady, + isSafeMode, + unlockedUsers, + pendingPackages, + shortcutKeyToPinnedShortcuts + ) + if (launcherItem != null && checkAndAddItem(launcherItem, workspaceModel)) { + if (launcherItem.itemType == LauncherConstants.ItemType.APPLICATION) { + launcherItem.getTargetComponent()?.let { componentName -> + allAppsMap.remove(componentName) + } + } + } + + } + + Timber.d( + "Size of launcherItems before processing remaining app items: " + + "${workspaceModel.workspaceItems.size}" + ) + + sortWorkspaceItems(workspaceModel.workspaceItems) + + // Processing newly added apps. + // These apps are added when launcher was not running + val apps = ArrayList() + allAppsMap.values.map { + val intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(it.componentName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + + WorkspaceItem( + launcherDatabase.generateNewItemId(), + it.label.toString(), + intent.toUri(0), + CONTAINER_DESKTOP, + -1, + -1, + -1, + LauncherConstants.ItemType.APPLICATION, + -1, + userManager.getSerialNumberForUser(it.user) + ) + } + + var (cellX, cellY, rank) = lastItemPositionAndRank(workspaceModel.workspaceItems) + + val labelComparator = LabelComparator() + apps.sortWith(Comparator { item1, item2 -> + labelComparator.compare(item1.title, item2.title) + }) + + var currentScreenId = + workspaceModel.workspaceScreens[workspaceModel.workspaceScreens.size - 1] + + apps.forEach { + if (rank == idp.numRows * idp.numColumns) { + currentScreenId = launcherDatabase.generateNewScreenId() + workspaceModel.workspaceScreens.add(currentScreenId) + cellX = 0 + cellY = 0 + rank = calculateRank(cellX, cellY, idp.numRows) + } + it.screen = currentScreenId + it.cellX = cellX + it.cellY = cellY + it.rank = rank + val (cX, cY) = generateNewCell(cellX, cellY, idp.numColumns) + cellX = cX + cellY = cY + } + launcherDatabase.saveAll(apps) + launcherDatabase.updateWorkspaceScreenOrder(workspaceModel.workspaceScreens) + + apps.forEach { + val item = convertToLauncherItem( + it, + quietMode, + isSdCardReady, + isSafeMode, + unlockedUsers, + pendingPackages, + shortcutKeyToPinnedShortcuts + ) + if (item != null) { + checkAndAddItem(item, workspaceModel) + } + } + + // Remove any empty screens + val unusedScreens: ArrayList = + ArrayList(workspaceModel.workspaceScreens) + for (item in workspaceModel.itemsIdMap) { + val screenId: Long = item.screenId + if (item.container == CONTAINER_DESKTOP && unusedScreens.contains(screenId)) { + unusedScreens.remove(screenId) + } + } + + // If there are any empty screens remove them, and update. + if (unusedScreens.size != 0) { + workspaceModel.workspaceScreens.removeAll(unusedScreens) + launcherDatabase.updateWorkspaceScreenOrder(workspaceModel.workspaceScreens) + } + return workspaceModel + } + + private fun lastItemPositionAndRank(workspaceItems: ArrayList): Triple { + val size = workspaceItems.size + val lastItem = workspaceItems[size - 1] + var screenId = lastItem.screenId + val x = lastItem.cellX + val y = lastItem.cellY + val rank = calculateRank(x, y, idp.numColumns) + Timber.d("${rank + 1} items on last screens having id $screenId") + return Triple(x, y, rank) + } + + private fun checkAndAddItem( + item: LauncherItem, + workspaceModel: WorkspaceModel + ): Boolean { + if (checkItemPlacement(item, workspaceModel)) { + workspaceModel.addItem(context, item, false) + return true + } else { + launcherDatabase.markDeleted(item.id) + return false + } + } + + private fun checkItemPlacement( + item: LauncherItem, + workspaceModel: WorkspaceModel + ): Boolean { + if (item.container == CONTAINER_DESKTOP) { + if (!workspaceModel.workspaceScreens.contains(item.screenId)) { + launcherDatabase.markDeleted(item.id) + return false + } + } + return true + } + + private fun loadDefaultWorkspaceIfNecessary(allAppsMap: HashMap) { + launcherDatabase.createDbIfNotExist() + if (sharedPrefs.getBoolean(LauncherDatabaseGateway.EMPTY_DATABASE_CREATED, false)) { + val hotseatParser = + DefaultHotseatParser(launcherDatabase, idp.defaultLayoutId, context, userManager) + val count = hotseatParser.loadDefaultLayout() + Timber.d("Hotseat count is $count") + val existingWorkspaceItems = launcherDatabase.getAllWorkspaceItems() + val apps = ArrayList() + val screenIds = ArrayList() + allAppsMap.values.forEach { + if (!isApplicationAlreadyAdded(existingWorkspaceItems, it.componentName)) { + val intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(it.componentName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + + apps.add( + WorkspaceItem( + launcherDatabase.generateNewItemId(), + it.label.toString(), + intent.toUri(0), + CONTAINER_DESKTOP, + -1, + -1, + -1, + LauncherConstants.ItemType.APPLICATION, + -1, + userManager.getSerialNumberForUser(it.user) + ) + ) + } + } + + val labelComparator = LabelComparator() + apps.sortWith(Comparator { item1, item2 -> + labelComparator.compare(item1.title, item2.title) + }) + + populateScreensAndCells(apps, screenIds) + + launcherDatabase.saveAll(apps) + launcherDatabase.updateWorkspaceScreenOrder(screenIds) + clearFlagEmptyDbCreated() + } + } + + private fun populateScreensAndCells( + apps: ArrayList, + screenIds: ArrayList + ) { + var currentScreenId = launcherDatabase.generateNewScreenId() + screenIds.add(currentScreenId) + var cellX = 0 + var cellY = 0 + val cols = idp.numColumns + val rows = idp.numRows + apps.forEach { + var rank = calculateRank(cellX, cellY, cols) + Timber.d("Cellx, celly, rank of ${it.title} is $cellX, $cellY, $rank") + if (rank >= rows * cols) { + currentScreenId = launcherDatabase.generateNewScreenId() + screenIds.add(currentScreenId) + cellX = 0 + cellY = 0 + rank = calculateRank(cellX, cellY, cols) + Timber.d("Cellx, celly, rank of ${it.title} is $cellX, $cellY, $rank") + } + it.screen = currentScreenId + it.cellX = cellX + it.cellY = cellY + it.rank = rank + val (cX, cY) = generateNewCell(cellX, cellY, cols) + cellX = cX + cellY = cY + } + } + + private fun generateNewCell(cellX: Int, cellY: Int, cols: Int): Pair { + var tempX = cellX + 1 + var tempY = cellY + if (tempX >= cols) { + tempX = 0 + tempY += 1 + } + return Pair(tempX, tempY) + } + + private fun calculateRank(cellX: Int, cellY: Int, cols: Int): Int = cellY * cols + cellX + + private fun clearFlagEmptyDbCreated() { + sharedPrefs.edit().putBoolean(LauncherDatabaseGateway.EMPTY_DATABASE_CREATED, false) + .commit() + } + + private fun isApplicationAlreadyAdded( + existingWorkspaceItems: List, + componentName: ComponentName + ): Boolean { + for (i in existingWorkspaceItems.indices) { + val item = existingWorkspaceItems[i] + if (item.componentName != null && item.componentName == componentName) { + return true + } + } + return false + } + + private fun checkAndValidate( + item: WorkspaceItem, + unlockedUsers: LongSparseArray, + shortcutKeyToPinnedShortcuts: HashMap, + isSdcardReady: Boolean + ): Boolean { + + //val restoreFlag = getInt(restoredIndex) + + if (item.user == null) { + launcherDatabase.markDeleted(item) + return false + } + + when (item.itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT, + LauncherConstants.ItemType.DEEP_SHORTCUT -> { + if (item.intent == null) { + launcherDatabase.markDeleted(item) + return false + } + + if (Process.myUserHandle() != item.user) { + if (item.itemType == LauncherConstants.ItemType.SHORTCUT) { + launcherDatabase.markDeleted(item) + return false + } + } + + if (item.targetPackage.isNullOrEmpty() and (item.itemType != LauncherConstants.ItemType.SHORTCUT)) { + launcherDatabase.markDeleted(item) + return false + } + + // If there is no target package, its an implicit intent + // (legacy shortcut) which is always valid + val validTarget = item.targetPackage.isNullOrEmpty() || + launcherApps.isPackageEnabledForProfile(item.targetPackage, item.user) + if (item.componentName != null && validTarget) { + if (!launcherApps.isActivityEnabledForProfile(item.componentName, item.user)) { + launcherDatabase.markDeleted(item) + return false + } + } + + // else if componentName == null => can't infer much, leave it + // else if !validPackage => could be restored icon or missing sd-card + + if (item.targetPackage?.isNotEmpty() == true && !validTarget) { + // Points to a valid app (superset of componentName != null) but the apk + // is not available. + if (!packageManagerHelper.isAppOnSdcard( + item.targetPackage, + item.user!! + ) and isSdcardReady + ) { + launcherDatabase.markDeleted(item) + return false + } + } + + if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + val shortcutKey = ShortcutKey.fromIntent(item.intent, item.user!!) + if (unlockedUsers.get(item.profileId)) { + val pinnedShortcut: ShortcutInfoCompat? = + shortcutKeyToPinnedShortcuts.get(shortcutKey) + if (pinnedShortcut == null) { + // The shortcut is no longer valid. + launcherDatabase.markDeleted(item) + return false + } + } + } + } + } + return true + } + + private fun sortWorkspaceItems(launcherItems: List) { + val screenCols = idp.numColumns + val screenRows = idp.numRows + val screenCellCount = screenCols * screenRows + Collections.sort(launcherItems) { lhs, rhs -> + if (lhs.container == rhs.container) { + when (lhs.container) { + CONTAINER_DESKTOP -> { + val lr = lhs.screenId * screenCellCount + lhs.cellY * screenCols + lhs.cellX + val rr = rhs.screenId * screenCellCount + rhs.cellY * screenCols + rhs.cellX + val result = lr.compareTo(rr) + if (result != 0) result + else launcherItemComparator.compare(lhs, rhs) + } + LauncherConstants.ContainerType.CONTAINER_HOTSEAT -> { + lhs.screenId.compareTo(rhs.screenId) + } + else -> throw RuntimeException("Unexpected container type when sorting: ${lhs}") + } + } else { + lhs.container.compareTo(rhs.container) + } + } + } + + private fun convertToLauncherItem( + item: WorkspaceItem, + quietMode: LongSparseArray, + isSdcardReady: Boolean, + isSafeMode: Boolean, + unlockedUsers: LongSparseArray, + pendingPackages: MultiHashMap, + shortcutKeyToPinnedShortcuts: HashMap + ): LauncherItem? { + var allowMissingTarget = false + // Load necessary properties. + val itemType = item.itemType + var disabledState = + if (quietMode[item.profileId]) LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER else 0 + //val restoreFlag = getInt(restoredIndex) + return when (itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT, + LauncherConstants.ItemType.DEEP_SHORTCUT -> { + if (item.targetPackage?.isNotEmpty() == true and !item.validTarget) { + // Points to a valid app but the apk is not available. + if (packageManagerHelper.isAppOnSdcard(item.targetPackage, item.user!!)) { + disabledState = + disabledState or LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE + allowMissingTarget = true + } else if (!isSdcardReady) { + Timber.d("Missing Package ${item.targetPackage}, will be checked later") + pendingPackages.addToList(item.user, item.targetPackage) + allowMissingTarget = true + } + } + + var launcherItem: ShortcutItem? = null + if (item.itemType == LauncherConstants.ItemType.APPLICATION) { + launcherItem = getApplicationItem( + item.user!!, + quietMode[item.profileId], + item.intent!!, + allowMissingTarget, + item.title + ) + } else if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + val shortcutKey = ShortcutKey.fromIntent(item.intent!!, item.user!!) + if (unlockedUsers.get(item.profileId)) { + val pinnedShortcut = shortcutKeyToPinnedShortcuts[shortcutKey] + if (pinnedShortcut != null) { + launcherItem = ShortcutItem() + .apply { + this.user = item.user!! + this.itemType = LauncherConstants.ItemType.DEEP_SHORTCUT + this.intent = pinnedShortcut.makeIntent() + this.title = pinnedShortcut.getShortLabel() + this.runtimeStatusFlags = if (pinnedShortcut.isEnabled()) { + this.runtimeStatusFlags and LauncherItemWithIcon.FLAG_DISABLED_BY_PUBLISHER.inv() + } else { + this.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_BY_PUBLISHER + } + this.disabledMessage = pinnedShortcut.getDisabledMessage() + } + //TODO: Set Icon here + if (packageManagerHelper.isAppSuspended( + pinnedShortcut.getPackage(), + launcherItem.user + ) + ) { + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED + } + } + } else { + launcherItem = ShortcutItem() + .apply { + this.user = item.user!! + this.itemType = item.itemType + this.title = if (item.title.isEmpty()) "" else item.title + // TODO: Set Icon here + } + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_LOCKED_USER + } + } else { + launcherItem = ShortcutItem() + .apply { + this.user = item.user!! + this.itemType = item.itemType + this.title = if (item.title.isEmpty()) "" else item.title + // TODO: Set Icon here + } + + val intent = item.intent!! + if (intent.action != null && + intent.categories != null && + intent.action == Intent.ACTION_MAIN && + intent.categories.contains(Intent.CATEGORY_LAUNCHER) + ) { + item.intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + } + } + launcherItem?.apply { + item.applyCommonProperties(this) + this.intent = item.intent + this.rank = item.rank + this.runtimeStatusFlags = + this.runtimeStatusFlags or disabledState + if (!isSafeMode && !Utilities.isSystemApp(context, item.intent)) { + this.runtimeStatusFlags = + this.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SAFEMODE + } + } + launcherItem + } + LauncherConstants.ItemType.FOLDER -> { + val folderItem = FolderItem() + .apply { + item.applyCommonProperties(this) + title = item.title + } + folderItem + } + else -> throw RuntimeException("Unexpected type of LauncherItem encountered") + } + } + + private fun getApplicationItem( + user: UserHandle, + quietMode: Boolean, + intent: Intent, + allowMissingTarget: Boolean, + title: String? + ): ApplicationItem? { + val componentName = intent.component + if (componentName == null) { + Timber.d("Missing component found in getApplicationItem") + return null + } + val newIntent = Intent(Intent.ACTION_MAIN, null) + .apply { + component = componentName + } + newIntent.addCategory(Intent.CATEGORY_LAUNCHER) + val lai = launcherApps.resolveActivity(newIntent, user) + if ((lai == null) && !allowMissingTarget) { + Timber.d("Missing activity found in getApplicationItem") + return null + } + + return if (lai != null) { + val applicationItem = ApplicationItem(lai, user, quietMode) + .apply { + itemType = LauncherConstants.ItemType.APPLICATION + iconBitmap = launcherIcons.createBadgedIconBitmap( + lai.getBadgedIcon(0), + user, + Build.VERSION.SDK_INT + ) + } + val isSuspended = packageManagerHelper.isAppSuspended(lai.applicationInfo) + Timber.d("$applicationItem is $isSuspended") + if (isSuspended) { + applicationItem.runtimeStatusFlags = + applicationItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED + } + applicationItem + } else { + val applicationItem = ApplicationItem() + .apply { + this.user = user + this.intent = newIntent + this.componentName = componentName + this.title = title + iconBitmap = launcherIcons.createBadgedIconBitmap( + context.packageManager.getPackageInfo( + this.componentName.packageName, + 0 + ).applicationInfo.loadIcon(context.packageManager), + user, + Build.VERSION.SDK_INT + ) + } + + if (applicationItem.title == null) { + applicationItem.title = componentName.className + } + applicationItem + } + } + + private fun clearAllDbs() { + launcherDatabase.createEmptyDatabase() + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..2954dabf6d001fe5936ea9dac73f7aa8edf56598 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt @@ -0,0 +1,50 @@ +package foundation.e.blisslauncher.data + +import foundation.e.blisslauncher.domain.entity.WorkspaceScreen +import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository +import javax.inject.Inject + +class WorkspaceScreenRepositoryImpl +@Inject constructor(private val launcherDatabase: LauncherDatabaseGateway) : + WorkspaceScreenRepository { + + override fun findAllOrderedByScreenRank(): List { + return launcherDatabase.loadWorkspaceScreensInOrder() + } + + override fun generateNewScreenId(): Long { + return launcherDatabase.generateNewScreenId() + } + + override fun save(entity: S): S { + TODO("Not yet implemented") + } + + override fun saveAll(entities: List): List { + TODO("Not yet implemented") + } + + override fun findById(id: Long): WorkspaceScreen? { + TODO("Not yet implemented") + } + + override fun findAll(): List { + TODO("Not yet implemented") + } + + override fun delete(entity: WorkspaceScreen) { + TODO("Not yet implemented") + } + + override fun deleteById(id: Long) { + TODO("Not yet implemented") + } + + override fun deleteAll() { + TODO("Not yet implemented") + } + + override fun deleteAll(entities: List) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt new file mode 100644 index 0000000000000000000000000000000000000000..953feaa7b77302e3e37185e8e5ba039a011d555b --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt @@ -0,0 +1,206 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.graphics.Rect +import android.os.Bundle +import android.os.Process +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import java.util.ArrayList + +open class LauncherAppsCompatVL internal constructor(protected val context: Context) : + LauncherAppsCompat { + + protected val launcherApps: LauncherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + private val callbacks = + ArrayMap() + + override fun getActivityList( + packageName: String?, + user: UserHandle? + ): List { + return launcherApps.getActivityList(packageName, user) + } + + override fun resolveActivity( + intent: Intent?, + user: UserHandle? + ): LauncherActivityInfo? { + return launcherApps.resolveActivity(intent, user) + } + + override fun startActivityForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) { + launcherApps.startMainActivity(component, user, sourceBounds, opts) + } + + override fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? { + val isPrimaryUser = Process.myUserHandle() == user + if (!isPrimaryUser && flags == 0) { + // We are looking for an installed app on a secondary profile. Prior to O, the only + // entry point for work profiles is through the LauncherActivity. + val activityList = + launcherApps.getActivityList(packageName, user) + return if (activityList.size > 0) activityList[0].applicationInfo else null + } + return try { + val info = + context.packageManager.getApplicationInfo(packageName, flags) + // There is no way to check if the app is installed for managed profile. But for + // primary profile, we can still have this check. + if (isPrimaryUser && info.flags and ApplicationInfo.FLAG_INSTALLED == 0 || + !info.enabled + ) { + null + } else info + } catch (e: PackageManager.NameNotFoundException) { // Package not found + null + } + } + + override fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) { + launcherApps.startAppDetailsActivity(component, user, sourceBounds, opts) + } + + override fun addOnAppsChangedCallback(listener: LauncherAppsCompat.OnAppsChangedCallbackCompat) { + val wrappedCallback = WrappedCallback(listener) + synchronized(callbacks) { callbacks.put(listener, wrappedCallback) } + launcherApps.registerCallback(wrappedCallback) + } + + override fun removeOnAppsChangedCallback(listener: LauncherAppsCompat.OnAppsChangedCallbackCompat) { + var wrappedCallback: WrappedCallback? + synchronized(callbacks) { wrappedCallback = callbacks.remove(listener) } + if (wrappedCallback != null) { + launcherApps.unregisterCallback(wrappedCallback) + } + } + + override fun isPackageEnabledForProfile( + packageName: String?, + user: UserHandle? + ): Boolean { + return launcherApps.isPackageEnabled(packageName, user) + } + + override fun isActivityEnabledForProfile( + component: ComponentName?, + user: UserHandle? + ): Boolean { + return launcherApps.isActivityEnabled(component, user) + } + + private class WrappedCallback(private val mCallback: LauncherAppsCompat.OnAppsChangedCallbackCompat) : + LauncherApps.Callback() { + override fun onPackageRemoved( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageRemoved(packageName, user) + } + + override fun onPackageAdded( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageAdded(packageName, user) + } + + override fun onPackageChanged( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageChanged(packageName, user) + } + + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + mCallback.onPackagesAvailable(packageNames, user, replacing) + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + mCallback.onPackagesUnavailable(packageNames, user, replacing) + } + + override fun onPackagesSuspended( + packageNames: Array, + user: UserHandle + ) { + mCallback.onPackagesSuspended(packageNames, user) + } + + override fun onPackagesUnsuspended( + packageNames: Array, + user: UserHandle + ) { + mCallback.onPackagesUnsuspended(packageNames, user) + } + + override fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) { + val shortcutInfoCompats: MutableList = + ArrayList( + shortcuts.size + ) + for (shortcutInfo in shortcuts) { + shortcutInfoCompats.add( + ShortcutInfoCompat( + shortcutInfo + ) + ) + } + mCallback.onShortcutsChanged(packageName, shortcutInfoCompats, user) + } + } + + /*@Override + public List getCustomShortcutActivityList( + @Nullable PackageUserKey packageUser) { + List result = new ArrayList<>(); + if (packageUser != null && !packageUser.mUser.equals(Process.myUserHandle())) { + return result; + } + PackageManager pm = mContext.getPackageManager(); + for (ResolveInfo info : + pm.queryIntentActivities(new Intent(Intent.ACTION_CREATE_SHORTCUT), 0)) { + if (packageUser == null || packageUser.mPackageName + .equals(info.activityInfo.packageName)) { + result.add(new ShortcutConfigActivityInfoVL(info.activityInfo, pm)); + } + } + return result; + }*/ +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe95d2c36e82071a90bf829f9739d23a9c9f1d7d --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.PinItemRequest +import android.content.pm.PackageManager +import android.os.Parcelable +import android.os.UserHandle +import androidx.annotation.Nullable + +@TargetApi(26) +class LauncherAppsCompatVO internal constructor(context: Context) : + LauncherAppsCompatVL(context) { + + override fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? { + return try { + val info = launcherApps.getApplicationInfo(packageName, flags, user) + if (info.flags and ApplicationInfo.FLAG_INSTALLED == 0 || !info.enabled) null else info + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + companion object { + //TODO + /*@Override + public List getCustomShortcutActivityList( + @Nullable PackageUserKey packageUser) { + List result = new ArrayList<>(); + UserHandle myUser = Process.myUserHandle(); + + final List users; + final String packageName; + if (packageUser == null) { + users = UserManagerCompat.getInstance(mContext).getUserProfiles(); + packageName = null; + } else { + users = new ArrayList<>(1); + users.add(packageUser.mUser); + packageName = packageUser.mPackageName; + } + for (UserHandle user : users) { + boolean ignoreTargetSdk = myUser.equals(user); + List activities = + mLauncherApps.getShortcutConfigActivityList(packageName, user); + for (LauncherActivityInfo activityInfo : activities) { + if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion >= + Build.VERSION_CODES.O) { + result.add(new ShortcutConfigActivityInfoVO(activityInfo)); + } + } + } + + return result; + }*/ + + /** + * request.accept() will initiate the following flow: + * -> go-to-system-process for actual processing (a) + * -> callback-to-launcher on UI thread (b) + * -> post callback on the worker thread (c) + * -> Update model and unpin (in system) any shortcut not in out model. (d) + * + * Note that (b) will take at-least one frame as it involves posting callback from binder + * thread to UI thread. + * If (d) happens before we add this shortcut to our model, we will end up unpinning + * the shortcut in the system. + * Here its the caller's responsibility to add the newly created ShortcutInfo immediately + * to the model (which may involves a single post-to-worker-thread). That will guarantee + * that (d) happens after model is updated. + */ + + //TODO for shortcuts + @Nullable + /*fun createShortcutInfoFromPinItemRequest( + context: Context?, request: PinItemRequest?, acceptDelay: Long + ): ShortcutInfo? { + return if (request != null && request.requestType == PinItemRequest.REQUEST_TYPE_SHORTCUT && + request.isValid + ) { + if (acceptDelay <= 0) { + if (!request.accept()) { + return null + } + } else { // Block the worker thread until the accept() is called. + LooperExecutor(LauncherModel.getWorkerLooper()).execute(Runnable { + try { + Thread.sleep(acceptDelay) + } catch (e: InterruptedException) { // Ignore + } + if (request.isValid) { + request.accept() + } + }) + } + val compat = + ShortcutInfoCompat(request.shortcutInfo) + val info = ShortcutInfo(compat, context) + // Apply the unbadged icon and fetch the actual icon asynchronously. + val li: LauncherIcons = LauncherIcons.obtain(context) + li.createShortcutIcon(compat, false *//* badged *//*).applyTo(info) + li.recycle() + LauncherAppState.getInstance(context).getModel() + .updateAndBindShortcutInfo(info, compat) + info + } else { + null + } + }*/ + + fun getPinItemRequest(intent: Intent): PinItemRequest? { + val extra = + intent.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST) + return if (extra is PinItemRequest) extra else null + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt new file mode 100644 index 0000000000000000000000000000000000000000..bda351d7d9e219f334fb803a1befdf5ffa2ecd90 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt @@ -0,0 +1,103 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionInfo +import android.os.Process +import android.util.SparseArray +import androidx.annotation.NonNull + +class PackageInstallerCompat(private val context: Context) { + + val activeSessions = SparseArray() + val installer = context.packageManager.packageInstaller + val appContext = context.applicationContext + val sessionVerifiedMap = HashMap() + + fun updateAndGetActiveSessionCache(): HashMap { + val activePackages = HashMap() + val user = Process.myUserHandle() + getAllVerifiedSessions().forEach { + if (it.appPackageName != null) { + //TODO: Cache to IconCache + activePackages[it.appPackageName] = it + activeSessions.run { put(it.sessionId, it.appPackageName) } + } + } + return activePackages + } + + fun onStop() { + } + + fun getAllVerifiedSessions(): List { + val list = ArrayList(installer.allSessions) + val iterator = list.iterator() + iterator.forEachRemaining { + if (verify(it) == null) { + iterator.remove() + } + } + + return list + } + + private fun verify(sessionInfo: SessionInfo?): SessionInfo? { + if (sessionInfo == null || sessionInfo.installerPackageName == null || + sessionInfo.appPackageName.isNullOrEmpty() + ) { + return null + } + val pkg = sessionInfo.installerPackageName + synchronized(sessionVerifiedMap) { + if (!sessionVerifiedMap.containsKey(pkg)) { + val launcherApps = LauncherAppsCompatVO(appContext) + val hasSystemFlag = launcherApps.getApplicationInfo( + pkg, + ApplicationInfo.FLAG_SYSTEM, Process.myUserHandle() + ) != null + sessionVerifiedMap[pkg] = hasSystemFlag + } + } + return if (sessionVerifiedMap[pkg] == true) sessionInfo else null + } + + companion object { + const val STATUS_INSTALLED = 0 + const val STATUS_INSTALLING = 1 + const val STATUS_FAILED = 2 + } + + class PackageInstallInfo { + val componentName: ComponentName + val packageName: String + val state: Int + val progress: Int + + private constructor(@NonNull info: SessionInfo) { + state = STATUS_INSTALLING + packageName = info.appPackageName + componentName = ComponentName(packageName, "") + progress = (info.progress * 100f).toInt() + } + + constructor(packageName: String, state: Int, progress: Int) { + this.state = state + this.packageName = packageName + componentName = ComponentName(packageName, "") + this.progress = progress + } + + companion object { + fun fromInstallingState(info: SessionInfo): PackageInstallInfo { + return PackageInstallInfo(info) + } + + fun fromState(state: Int, packageName: String): PackageInstallInfo { + return PackageInstallInfo(packageName, state, 0 /* progress */) + } + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6bd9536a55b39ff573fa2bb54403e297c4d796c --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt @@ -0,0 +1,91 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.util.ArrayMap +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import java.util.ArrayList + +open class UserManagerCompatVN(context: Context) : UserManagerRepository { + + protected val userManager: UserManager = + context.getSystemService(Context.USER_SERVICE) as UserManager + private val pm: PackageManager = context.packageManager + + private lateinit var users: LongArrayMap + + // Create a separate reverse map as LongArrayMap.indexOfValue checks if objects are same + // and not {@link Object#equals} + private lateinit var userToSerialMap: ArrayMap + + override fun enableAndResetCache() { + synchronized(this) { + users = LongArrayMap() + userToSerialMap = ArrayMap() + userManager.userProfiles.forEach { + val serial = userManager.getSerialNumberForUser(it) + users.put(serial, it) + userToSerialMap[it] = serial + } + } + } + + override val userProfiles: List + get() { + synchronized(this) { + if (::users.isInitialized) { + return ArrayList(userToSerialMap.keys) + } + } + return userManager.userProfiles + } + + override fun getSerialNumberForUser(user: UserHandle): Long { + synchronized(this) { + if (::userToSerialMap.isInitialized) { + return userToSerialMap[user] ?: 0 + } + } + return userManager.getSerialNumberForUser(user) + } + + override fun getUserForSerialNumber(serialNumber: Long): UserHandle? { + synchronized(this) { + if (::users.isInitialized) { + users[serialNumber] + } + } + return userManager.getUserForSerialNumber(serialNumber) + } + + override fun getBadgedLabelForUser(label: CharSequence, user: UserHandle?): CharSequence = + when (user) { + null -> label + else -> pm.getUserBadgedLabel(label, user) + } + + override fun isQuietModeEnabled(user: UserHandle): Boolean = + userManager.isQuietModeEnabled(user) + + override fun isUserUnlocked(user: UserHandle): Boolean = userManager.isUserUnlocked(user) + + override val isDemoUser: Boolean + get() = false + + override fun requestQuietModeEnabled(enableQuietMode: Boolean, user: UserHandle?): Boolean = + false + + override val isAnyProfileQuietModeEnabled: Boolean + get() { + for (userProfile in userProfiles) { + if (Process.myUserHandle().equals(userProfile)) continue + + if (isQuietModeEnabled(userProfile)) return true + } + return false + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt new file mode 100644 index 0000000000000000000000000000000000000000..899d0016402fcd1a3f2ea58c37d6cf0343e6375e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build + +@TargetApi(Build.VERSION_CODES.N_MR1) +open class UserManagerCompatVNMr1(context: Context) : UserManagerCompatVN(context) { + + override val isDemoUser: Boolean + get() = userManager.isDemoUser +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt new file mode 100644 index 0000000000000000000000000000000000000000..53f7aa21ff1cccffb341cdf6f945d2606e34423f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import android.os.UserHandle + +@TargetApi(Build.VERSION_CODES.P) +open class UserManagerCompatVP(context: Context) : UserManagerCompatVNMr1(context) { + + override fun requestQuietModeEnabled(enableQuietMode: Boolean, user: UserHandle?): Boolean = + userManager.requestQuietModeEnabled(enableQuietMode, user) +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..56d7c21e7a956f01016f31d5b45f19dfda774924 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import foundation.e.blisslauncher.data.database.dao.LauncherDao +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen + +@Database(entities = [WorkspaceItem::class, WorkspaceScreen::class], version = 1) +abstract class BlissLauncherDatabase : RoomDatabase() { + abstract fun launcherDao(): LauncherDao +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt new file mode 100644 index 0000000000000000000000000000000000000000..0256e9077950f8b31a9d7bf8a1acb488cbd63f08 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.data.database + +/** + * File names of all the file BlissLauncher uses to store data. + */ +class BlissLauncherFiles { + companion object { + private val XML = ".xml" + + const val LAUNCHER_DB = "launcher.db" + const val SHARED_PREFERENCES_KEY = "foundation.e.blisslauncher.prefs" + const val DEVICE_PREFERENCES_KEY = "foundation.e.blisslauncher.device.prefs" + + const val WIDGET_PREVIEWS_DB = "widgetpreviews.db" + const val APP_ICONS_DB = "app_icons.db" + + val ALL_FILES = listOf( + LAUNCHER_DB, + SHARED_PREFERENCES_KEY + XML, + WIDGET_PREVIEWS_DB, + DEVICE_PREFERENCES_KEY + XML, + APP_ICONS_DB + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..931166761ba7fe044b46de58ab2e82908b7d23fa --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt @@ -0,0 +1,11 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import foundation.e.blisslauncher.data.database.dao.IconDao +import foundation.e.blisslauncher.data.database.roomentity.IconEntity + +@Database(entities = [IconEntity::class], version = 1) +abstract class IconDatabase : RoomDatabase() { + abstract fun iconDao(): IconDao +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f2135f1bcf223b2fe7a71dc8b3f1766de43ee5f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import foundation.e.blisslauncher.data.database.roomentity.IconEntity + +@Dao +interface IconDao { + @Query("DELETE FROM icons WHERE componentName LIKE :componentName AND profileId = :userSerial") + fun delete(componentName: String, userSerial: Int) + + @Query("DELETE FROM icons WHERE componentName in (:components)") + fun delete(components: List) + + @Query("DELETE FROM icons") + fun clear() + + @Query("SELECT * FROM icons WHERE profileId = :userSerial") + fun query(userSerial: Long): List + + @Query("SELECT * FROM icons WHERE componentName = :userSerial AND profileId = :userSerial") + fun query(componentName: String, userSerial: Long): IconEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(iconEntity: IconEntity) +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..095f4f76d6f23faa61b88d7260dc491549d11d2f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt @@ -0,0 +1,47 @@ +package foundation.e.blisslauncher.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen + +@Dao +abstract class LauncherDao { + + @RawQuery + abstract fun getMaxIdInTable(query: SupportSQLiteQuery): Long + + @Insert + abstract fun insert(workspaceItem: WorkspaceItem): Long + + @Insert + abstract fun insertAll(workspaceItems: List) + + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertAllWorkspaceScreens(screens: List) + + @Transaction + open fun createEmptyDb() { + dropWorkspaceItemTable() + dropWorkspaceScreenTable() + } + + @Query("SELECT * FROM launcherItems") + abstract fun getAllWorkspaceItems(): List + + @Query("DELETE FROM launcherItems") + abstract fun dropWorkspaceItemTable() + + @Query("DELETE FROM workspaceScreens") + abstract fun dropWorkspaceScreenTable() + + @Query("SELECT * FROM workspaceScreens") + abstract fun getAllWorkspaceScreens(): List +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..3225d7613bce1bad05f6352535a7664eff244b61 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt @@ -0,0 +1,49 @@ +package foundation.e.blisslauncher.data.database.roomentity + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity(tableName = "icons", primaryKeys = ["componentName", "profileId"]) +data class IconEntity( + @ColumnInfo(name = "componentName", typeAffinity = ColumnInfo.TEXT) + val componentName: String, + @ColumnInfo(name = "profileId") + val profileId: Long, + val lastUpdated: Long = 0, + @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) + val version: Int = 0, + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val icon: ByteArray, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val label: String, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val systemState: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IconEntity + + if (componentName != other.componentName) return false + if (profileId != other.profileId) return false + if (lastUpdated != other.lastUpdated) return false + if (version != other.version) return false + if (!icon.contentEquals(other.icon)) return false + if (label != other.label) return false + if (systemState != other.systemState) return false + + return true + } + + override fun hashCode(): Int { + var result = componentName.hashCode() + result = 31 * result + profileId.hashCode() + result = 31 * result + lastUpdated.hashCode() + result = 31 * result + version + result = 31 * result + icon.contentHashCode() + result = 31 * result + label.hashCode() + result = 31 * result + systemState.hashCode() + return result + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..242fc2b1e29a12e8470127c9223ac76b721cc018 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt @@ -0,0 +1,75 @@ +package foundation.e.blisslauncher.data.database.roomentity + +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import androidx.annotation.NonNull +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import foundation.e.blisslauncher.domain.entity.LauncherItem +import timber.log.Timber +import java.net.URISyntaxException + +@Entity(tableName = "launcherItems") +data class WorkspaceItem( + @PrimaryKey + val _id: Long, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val title: String, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT, name = "intent") + val intentStr: String?, + var container: Long, + var screen: Long, + var cellX: Int, + var cellY: Int, + val itemType: Int, + @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) + @NonNull + var rank: Int, + @ColumnInfo + val profileId: Long +) { + // Properties to initialise for proper validation + + @Ignore + val targetPackage: String? + + @Ignore + val intent: Intent? + + @Ignore + val componentName: ComponentName? + + @Ignore + var user: UserHandle? = null + + @Ignore + var validTarget: Boolean = false + + init { + intent = getParsedIntent() + componentName = intent?.component + targetPackage = componentName?.packageName ?: intent?.`package` + } + + private fun getParsedIntent(): Intent? { + try { + return if (intentStr.isNullOrEmpty()) null else Intent.parseUri(intentStr, 0) + } catch (e: URISyntaxException) { + Timber.e(e) + return null + } + } + + fun applyCommonProperties( + destItem: LauncherItem + ) { + destItem.id = _id + destItem.container = container + destItem.screenId = screen + destItem.cellX = cellX + destItem.cellY = cellY + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..86d1b84bb810c3184a68266fcc82408588d0c08a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt @@ -0,0 +1,14 @@ +package foundation.e.blisslauncher.data.database.roomentity + +import androidx.annotation.NonNull +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "workspaceScreens") +data class WorkspaceScreen( + @PrimaryKey + val _id: Long, + @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) + val screenRank: Int +) \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt new file mode 100644 index 0000000000000000000000000000000000000000..1b175cc03170e1b3c55e4296fab81cb720367663 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -0,0 +1,590 @@ +package foundation.e.blisslauncher.data.icon + +import android.R +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Process +import android.os.UserHandle +import android.text.TextUtils +import android.util.Log +import foundation.e.blisslauncher.common.BitmapRenderer +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.data.database.dao.IconDao +import foundation.e.blisslauncher.data.database.roomentity.IconEntity +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.PackageItem +import foundation.e.blisslauncher.domain.keys.ComponentKey +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import java.util.HashSet +import java.util.Stack +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IconCache @Inject constructor( + private val context: Context, + private val inv: InvariantDeviceProfile, + private val iconProvider: IconProvider, + private val launcherApps: LauncherAppsCompat, + private val userManager: UserManagerRepository, + private val iconDao: IconDao, + private val launcherIcons: LauncherIcons +) { + + data class CacheEntry( + var bitmap: Bitmap? = null, + var title: CharSequence? = "", + var contentDescription: CharSequence = "" + ) + + private val mDefaultIcons: HashMap = HashMap() + private val packageManager: PackageManager = context.packageManager + private val cache = HashMap() + private val iconDpi: Int = inv.fillResIconDpi + private val lowResOptions = + BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 } + + @SuppressLint("NewApi") + private val highResOptions = if (BitmapRenderer.USE_HARDWARE_BITMAP) { + BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.HARDWARE } + } else { + null + } + + private fun getFullResDefaultActivityIcon(): Drawable { + return getFullResIcon( + Resources.getSystem(), + if (Utilities.ATLEAST_OREO) R.drawable.sym_def_app_icon else R.mipmap.sym_def_app_icon + ) + } + + private fun getFullResIcon( + resources: Resources, + iconId: Int + ): Drawable { + val d: Drawable? = try { + resources.getDrawableForDensity(iconId, iconDpi) + } catch (e: Resources.NotFoundException) { + null + } + return d ?: getFullResDefaultActivityIcon() + } + + fun getFullResIcon(packageName: String, iconId: Int): Drawable { + return try { + packageManager.getResourcesForApplication(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + }.let { + if (it != null && iconId != 0) + getFullResIcon(it, iconId) + else getFullResDefaultActivityIcon() + } + } + + fun getFullResIcon(info: ActivityInfo): Drawable { + return try { + packageManager.getResourcesForApplication( + info.applicationInfo + ) + } catch (e: PackageManager.NameNotFoundException) { + null + }.let { + if (it != null && info.iconResource != 0) + getFullResIcon(it, info.iconResource) + else getFullResDefaultActivityIcon() + } + } + + fun getFullResIcon(info: LauncherActivityInfo): Drawable { + return getFullResIcon(info, true) + } + + fun getFullResIcon( + info: LauncherActivityInfo, + flattenDrawable: Boolean + ): Drawable { + return iconProvider.getIcon(info, iconDpi, flattenDrawable) + } + + fun makeDefaultIcon(user: UserHandle?): Bitmap { + return launcherIcons.createBadgedIconBitmap( + getFullResDefaultActivityIcon(), user, Build.VERSION.SDK_INT + ) + } + + /** + * Remove any records for the supplied ComponentName. + */ + @Synchronized + fun remove(componentName: ComponentName, user: UserHandle) { + cache.remove(ComponentKey(componentName, user)) + } + + /** + * Remove any records for the supplied package name from memory. + */ + private fun removeFromMemCacheLocked( + packageName: String, + user: UserHandle + ) { + val forDeletion = HashSet() + for (key in cache.keys) { + if (key.componentName.packageName == packageName && + key.user == user + ) { + forDeletion.add(key) + } + } + for (condemned in forDeletion) { + cache.remove(condemned) + } + } + + /** + * Updates the entries related to the given package in memory and persistent DB. + */ + @Synchronized + fun updateIconsForPkg( + packageName: String, + user: UserHandle + ) { + removeIconsForPkg(packageName, user) + try { + val info: PackageInfo = packageManager.getPackageInfo( + packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES + ) + val userSerial: Long = userManager.getSerialNumberForUser(user) + for (app in launcherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, info, userSerial, false /*replace existing*/) + } + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "Package not found", e) + } + } + + private fun addIconToDBAndMemCache( + app: LauncherActivityInfo, + info: PackageInfo, + userSerial: Long, + replaceExisting: Boolean + ) { + val componentKey = ComponentKey(app.componentName, app.user) + var entry: CacheEntry? = null + if (!replaceExisting) { + entry = cache[componentKey] + if (entry?.bitmap == null) { + entry == null + } + } + + if (entry == null) { + entry = launcherIcons.createBadgedIconBitmap( + getFullResIcon(app), + app.user, + app.applicationInfo.targetSdkVersion + ).let { + CacheEntry(it, app.label, userManager.getBadgedLabelForUser(app.label, app.user)) + } + } + cache.put(componentKey, entry) + addIconToDB(entry, app.componentName, app.applicationInfo.packageName, info, userSerial) + } + + private fun addIconToDB( + entry: CacheEntry, + componentName: ComponentName, + packageName: String, + info: PackageInfo, + userSerial: Long + ) { + val iconEntity = IconEntity( + componentName.flattenToString(), + userSerial, + info.lastUpdateTime, + info.versionCode, + Utilities.flattenBitmap(entry.bitmap), + entry.title.toString(), + iconProvider.getIconSystemState(packageName) + ) + + iconDao.insertOrReplace(iconEntity) + } + + /** + * Removes the entries related to the given package in memory and persistent DB. + */ + @Synchronized + fun removeIconsForPkg( + packageName: String, + user: UserHandle + ) { + removeFromMemCacheLocked(packageName, user) + val userSerial: Long = userManager.getSerialNumberForUser(user) + iconDao.delete("$packageName/%", userSerial.toInt()) + } + + fun updateDbIcons(ignorePackagesForMainUser: Set) { + //TODO: Dispose all current running tasks + // Remove all active icon update tasks. + //mWorkerHandler.removeCallbacksAndMessages(IconCache.ICON_UPDATE_TOKEN) + iconProvider.updateSystemStateString(context) + for (user in userManager.userProfiles) { + // Query for the set of apps + val apps: List = launcherApps.getActivityList(null, user) + // Fail if we don't have any apps + // TODO: Fix this. Only fail for the current user. + if (apps.isEmpty()) { + return + } + // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} + // is called by the icon cache when the job is complete. + updateDbIcons( + user, + apps, + if (Process.myUserHandle() == user) ignorePackagesForMainUser else emptySet() + ) + } + } + + private fun updateDbIcons( + user: UserHandle, + apps: List, + ignorePackages: Set + ) { + val userSerial = userManager.getSerialNumberForUser(user) + val pm = context.packageManager + val pkgInfoMap = HashMap() + pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES).forEach { + pkgInfoMap[it.packageName] = it + } + + val componentMap = HashMap() + for (app in apps) { + componentMap[app.componentName] = app + } + + val itemsToRemove = HashSet() + val appsToUpdate = Stack() + + iconDao.query(userSerial).forEach { + val cn = it.componentName + val component = ComponentName.unflattenFromString(cn) + val info = pkgInfoMap[component.packageName] + if (info == null) { + if (!ignorePackages.contains(component.packageName)) { + remove(component, user) + itemsToRemove.add(cn) + } + return@forEach + } + + if (info.applicationInfo.flags and ApplicationInfo.FLAG_IS_DATA_ONLY != 0) { + return@forEach + } + + val app = componentMap.remove(component) + if (it.version == info.versionCode && it.lastUpdated == info.lastUpdateTime + && it.systemState == iconProvider.getIconSystemState(info.packageName) + ) { + return@forEach + } + + if (app == null) { + remove(component, user) + itemsToRemove.add(cn) + } else { + appsToUpdate.add(app) + } + } + + if (itemsToRemove.isNotEmpty()) { + iconDao.delete(itemsToRemove.toList()) + } + + if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) { + val appsToAdd = + Stack() + appsToAdd.addAll(componentMap.values) + /*SerializedIconUpdateTask( + userSerial, pkgInfoMap, + appsToAdd, appsToUpdate + ).scheduleNext()*/ + } + } + + /** + * Updates {@param application} only if a valid entry is found. + */ + @Synchronized + fun updateTitleAndIcon(application: ApplicationItem) { + val entry: CacheEntry = cacheLocked( + application.componentName, + null, + application.user, false + ) + if (entry.bitmap != null && !isDefaultIcon(entry.bitmap!!, application.user)) { + applyCacheEntry(entry, application) + } + } + + /** + * Fill in {@param info} with the icon and label for {@param activityInfo} + */ + @Synchronized + fun getTitleAndIcon( + info: LauncherItemWithIcon, + activityInfo: LauncherActivityInfo? + ) { + // If we already have activity info, no need to use package icon + getTitleAndIcon(info, activityInfo, false) + } + + /** + * Fill in {@param info} with the icon and label. If the + * corresponding activity is not found, it reverts to the package icon. + */ + @Synchronized + fun getTitleAndIcon(info: LauncherItemWithIcon) { + // null info means not installed, but if we have a component from the intent then + // we should still look in the cache for restored app icons. + if (info.getTargetComponent() == null) { + getDefaultIcon(info.user).let { info.iconBitmap = it } + info.title = "" + info.contentDescription = "" + info.usingLowResIcon = false + } else { + getTitleAndIcon( + info, + launcherApps.resolveActivity(info.getIntent(), info.user), + true + ) + } + } + + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + @Synchronized + private fun getTitleAndIcon( + infoInOut: LauncherItemWithIcon, + activityInfoProvider: LauncherActivityInfo?, + usePkgIcon: Boolean + ) { + val entry: CacheEntry = cacheLocked( + infoInOut.getTargetComponent()!!, activityInfoProvider, + infoInOut.user, usePkgIcon + ) + applyCacheEntry(entry, infoInOut) + } + + /** + * Fill in {@param infoInOut} with the corresponding icon and label. + */ + @Synchronized + fun getTitleAndIconForApp(infoInOut: PackageItem) { + val entry: CacheEntry = getEntryForPackageLocked( + infoInOut.packageName!!, infoInOut.user + ) + applyCacheEntry(entry, infoInOut) + } + + private fun applyCacheEntry(entry: CacheEntry, info: LauncherItemWithIcon) { + info.title = Utilities.trim(entry.title) + info.contentDescription = entry.contentDescription + (if (entry.bitmap == null) getDefaultIcon(info.user) else entry.bitmap).let { + info.iconBitmap = it + } + } + + /** + * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. + * This method is not thread safe, it must be called from a synchronized method. + */ + protected fun cacheLocked( + componentName: ComponentName, + info: LauncherActivityInfo?, + user: UserHandle, usePackageIcon: Boolean + ): CacheEntry { + //Preconditions.assertWorkerThread() + val cacheKey = ComponentKey(componentName, user) + var entry: CacheEntry? = cache[cacheKey] + if (entry == null) { + entry = CacheEntry() + cache[cacheKey] = entry + + if (!getEntryFromDB(cacheKey, entry)) { + if (info != null) { + launcherIcons.createBadgedIconBitmap( + getFullResIcon(info), info.user, + info.applicationInfo.targetSdkVersion + ).let { + entry.bitmap = it + } + } else { + if (usePackageIcon) { + val packageEntry: CacheEntry = + getEntryForPackageLocked( + componentName.packageName, user + ) + entry.bitmap = packageEntry.bitmap + entry.title = packageEntry.title + entry.contentDescription = packageEntry.contentDescription + } + if (entry.bitmap == null) { + getDefaultIcon(user).let { + entry.bitmap = it + } + } + } + } + if (TextUtils.isEmpty(entry.title)) { + if (info != null) { + entry.title = info.label + entry.contentDescription = + userManager.getBadgedLabelForUser(entry.title.toString(), user) + } + } + } + return entry + } + + @Synchronized + fun getDefaultIcon(user: UserHandle): Bitmap? { + if (!mDefaultIcons.containsKey(user)) { + mDefaultIcons[user] = makeDefaultIcon(user) + } + return mDefaultIcons[user] + } + + fun isDefaultIcon( + icon: Bitmap, + user: UserHandle + ): Boolean { + return getDefaultIcon(user) === icon + } + + private fun getEntryFromDB( + cacheKey: ComponentKey, + entry: CacheEntry + ): Boolean { + val iconEntity = iconDao.query( + cacheKey.componentName.flattenToString(), + userManager.getSerialNumberForUser(cacheKey.user) + ) + if (iconEntity != null) { + entry.bitmap = loadIcon(iconEntity.icon, highResOptions) + entry.title = iconEntity.label + if (entry.title == null) { + entry.title = "" + entry.contentDescription = "" + } else { + entry.contentDescription = userManager.getBadgedLabelForUser( + entry.title.toString(), cacheKey.user + ) + } + return true + } else return false + } + + /** + * Gets an entry for the package, which can be used as a fallback entry for various components. + * This method is not thread safe, it must be called from a synchronized method. + */ + private fun getEntryForPackageLocked( + packageName: String, user: UserHandle + ): CacheEntry { + val cacheKey: ComponentKey = getPackageKey(packageName, user) + var entry: CacheEntry? = cache.get(cacheKey) + if (entry == null) { + entry = CacheEntry() + var entryUpdated = true + + // Check the DB first. + if (!getEntryFromDB(cacheKey, entry)) { + try { + val flags = + if (Process.myUserHandle() == user) 0 else PackageManager.MATCH_UNINSTALLED_PACKAGES + val info: PackageInfo = + packageManager.getPackageInfo(packageName, flags) + val appInfo = info.applicationInfo + ?: throw PackageManager.NameNotFoundException("ApplicationInfo is null") + // Load the full res icon for the application, but if useLowResIcon is set, then + // only keep the low resolution icon instead of the larger full-sized icon + val icon: Bitmap = launcherIcons.createBadgedIconBitmap( + appInfo.loadIcon(packageManager), user, appInfo.targetSdkVersion + ) + entry.title = appInfo.loadLabel(packageManager) + entry.contentDescription = + userManager.getBadgedLabelForUser(entry.title.toString(), user) + entry.bitmap = icon + + // Add the icon in the DB here, since these do not get written during + // package updates. + addIconToDB( + entry, + cacheKey.componentName, + packageName, + info, + userManager.getSerialNumberForUser(user) + ) + } catch (e: PackageManager.NameNotFoundException) { + Timber.d("Application not installed $packageName") + entryUpdated = false + } + } + // Only add a filled-out entry to the cache + if (entryUpdated) { + cache[cacheKey] = entry + } + } + return entry + } + + private fun getPackageKey(packageName: String, user: UserHandle): ComponentKey { + val cn = ComponentName(packageName, packageName + EMPTY_CLASS_NAME) + return ComponentKey(cn, user) + } + + private fun loadIcon( + blob: ByteArray, + highResOptions: BitmapFactory.Options? + ): Bitmap? { + return try { + BitmapFactory.decodeByteArray(blob, 0, blob.size, highResOptions) + } catch (e: Exception) { + null + } + } + + @Synchronized + fun clear() { + iconDao.clear() + } + + companion object { + private const val TAG = "IconCache" + + private const val INITIAL_ICON_CAPACITY = 50 + + private const val EMPTY_CLASS_NAME = "." + + private const val LOW_RES_SCALE_FACTOR = 5 + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6b355bd6b4c0a6d662bbe142318ac9792a763a7 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.data.icon + +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.graphics.drawable.Drawable +import android.os.Build +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IconProvider @Inject constructor(context: Context) { + private var mSystemState: String = "" + + init { + updateSystemStateString(context) + } + + fun updateSystemStateString(context: Context) { + val locale: String = context.resources.configuration.locales.toLanguageTags() + mSystemState = locale + "," + Build.VERSION.SDK_INT + } + + fun getIconSystemState(packageName: String): String { + return mSystemState + } + + /** + * @param flattenDrawable true if the caller does not care about the specification of the + * original icon as long as the flattened version looks the same. + */ + fun getIcon( + info: LauncherActivityInfo, + iconDpi: Int, + flattenDrawable: Boolean + ): Drawable = info.getIcon(iconDpi) +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt new file mode 100644 index 0000000000000000000000000000000000000000..3975866075cca5c08d56de447d46b676f91f8c07 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data.icon + +import android.content.Context +import android.content.Intent +import android.content.Intent.ShortcutIconResource +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PaintFlagsDrawFilter +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.PaintDrawable +import android.os.Process +import android.os.UserHandle +import foundation.e.blisslauncher.common.BitmapRenderer +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.data.R +import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.PackageItem +import javax.inject.Inject + +/** + * Helper methods for generating various launcher icons + */ +class LauncherIcons @Inject constructor( + context: Context, + idp: InvariantDeviceProfile, + private val pinnedShortcutManager: PinnedShortcutManager +) : AutoCloseable { + + override fun close() { + } + + private val mOldBounds = Rect() + private val mContext: Context = context.applicationContext + private val mCanvas: Canvas + private val mPm: PackageManager + private val mFillResIconDpi: Int + private val mIconBitmapSize: Int + private var mWrapperIcon: Drawable? = null + private var mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND + + // sometimes we store linked lists of these things + private var next: LauncherIcons? = null + + /** + * Returns a bitmap suitable for the all apps view. If the package or the resource do not + * exist, it returns null. + */ + fun createIconBitmap(iconRes: ShortcutIconResource): Bitmap? { + try { + val resources = + mPm.getResourcesForApplication(iconRes.packageName) + if (resources != null) { + val id = resources.getIdentifier(iconRes.resourceName, null, null) + // do not stamp old legacy shortcuts as the app may have already forgotten about it + return createBadgedIconBitmap( + resources.getDrawableForDensity(id, mFillResIconDpi), + Process.myUserHandle() /* only available on primary user */, + 0 /* do not apply legacy treatment */ + ) + } + } catch (e: Exception) { + // Icon not found. + } + return null + } + + /** + * Returns a bitmap which is of the appropriate size to be displayed as an icon + */ + fun createIconBitmap(icon: Bitmap): Bitmap { + return if (mIconBitmapSize == icon.width && mIconBitmapSize == icon.height) { + icon + } else createIconBitmap(BitmapDrawable(mContext.resources, icon)) + } + + /** + * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps + * view or workspace. The icon is badged for {@param user}. + * The bitmap is also visually normalized with other icons. + */ + @JvmOverloads + fun createBadgedIconBitmap( + icon: Drawable?, user: UserHandle?, iconAppTargetSdk: Int + ): Bitmap { + var icon = icon + icon = normalizeAndWrapToAdaptiveIcon(icon!!, iconAppTargetSdk, null) + val bitmap = createIconBitmap(icon!!) + if (Utilities.ATLEAST_OREO && icon is AdaptiveIconDrawable) { + mCanvas.setBitmap(bitmap) + mCanvas.setBitmap(null) + } + val result: Bitmap + result = if (user != null && Process.myUserHandle() != user) { + val drawable: BitmapDrawable = FixedSizeBitmapDrawable(bitmap) + val badged = mPm.getUserBadgedIcon(drawable, user) + if (badged is BitmapDrawable) { + badged.bitmap + } else { + createIconBitmap(badged) + } + } else { + bitmap + } + return result + } + + /** + * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually + * normalized with other icons and has enough spacing to add shadow. + */ + fun createBitmapWithoutShadow( + icon: Drawable?, + iconAppTargetSdk: Int + ): Bitmap { + var icon = icon + val iconBounds = RectF() + icon = normalizeAndWrapToAdaptiveIcon(icon!!, iconAppTargetSdk, iconBounds) + return createIconBitmap(icon) + } + + /** + * Sets the background color used for wrapped adaptive icon + */ + fun setWrapperBackgroundColor(color: Int) { + mWrapperBackgroundColor = + if (Color.alpha(color) < 255) DEFAULT_WRAPPER_BACKGROUND else color + } + + private fun normalizeAndWrapToAdaptiveIcon( + icon: Drawable, iconAppTargetSdk: Int, + outIconBounds: RectF? + ): Drawable { + // Ignore icon processing as of now. + return icon + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + fun badgeWithDrawable(target: Bitmap?, badge: Drawable) { + mCanvas.setBitmap(target) + badgeWithDrawable(mCanvas, badge) + mCanvas.setBitmap(null) + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + private fun badgeWithDrawable(target: Canvas, badge: Drawable) { + val badgeSize = mContext.resources + .getDimensionPixelSize(R.dimen.profile_badge_size) + badge.setBounds( + mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, + mIconBitmapSize, mIconBitmapSize + ) + badge.draw(target) + } + + private fun createIconBitmap(icon: Drawable): Bitmap { + var width = mIconBitmapSize + var height = mIconBitmapSize + if (icon is PaintDrawable) { + val painter = icon + painter.intrinsicWidth = width + painter.intrinsicHeight = height + } else if (icon is BitmapDrawable) { + // Ensure the bitmap has a density. + val bitmapDrawable = icon + val bitmap = bitmapDrawable.bitmap + if (bitmap != null && bitmap.density == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(mContext.resources.displayMetrics) + } + } + val sourceWidth = icon!!.intrinsicWidth + val sourceHeight = icon.intrinsicHeight + if (sourceWidth > 0 && sourceHeight > 0) { + // Scale the icon proportionally to the icon dimensions + val ratio = sourceWidth.toFloat() / sourceHeight + if (sourceWidth > sourceHeight) { + height = (width / ratio).toInt() + } else if (sourceHeight > sourceWidth) { + width = (height * ratio).toInt() + } + } + // no intrinsic size --> use default size + val textureWidth = mIconBitmapSize + val textureHeight = mIconBitmapSize + val bitmap = Bitmap.createBitmap( + textureWidth, textureHeight, + Bitmap.Config.ARGB_8888 + ) + mCanvas.setBitmap(bitmap) + val left = (textureWidth - width) / 2 + val top = (textureHeight - height) / 2 + mOldBounds.set(icon.bounds) + icon.setBounds(left, top, left + width, top + height) + mCanvas.save() + icon.draw(mCanvas) + mCanvas.restore() + icon.bounds = mOldBounds + mCanvas.setBitmap(null) + return bitmap + } + + @JvmOverloads + fun createShortcutIcon( + shortcutInfo: ShortcutInfoCompat, + badged: Boolean = true + ): Bitmap { + val unbadgedDrawable: Drawable? = + pinnedShortcutManager.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi) + val unbadgedBitmap: Bitmap + unbadgedBitmap = createBitmapWithoutShadow(unbadgedDrawable, 0) + if (!badged) { + return unbadgedBitmap + } + val badge = getShortcutInfoBadge(shortcutInfo) + return BitmapRenderer.createHardwareBitmap( + mIconBitmapSize, + mIconBitmapSize, + object : BitmapRenderer.Renderer { + override fun draw(out: Canvas) { + badgeWithDrawable(out, BitmapDrawable(badge.iconBitmap)) + } + }) + } + + private fun getShortcutInfoBadge( + shortcutInfo: ShortcutInfoCompat + ): LauncherItemWithIcon { + val cn = shortcutInfo.getActivity() + val badgePkg = shortcutInfo.getBadgePackage(mContext) + val hasBadgePkgSet = badgePkg != shortcutInfo.getPackage() + return if (cn != null && !hasBadgePkgSet) { + // Get the app info for the source activity. + val appItem = ApplicationItem() + appItem.user = shortcutInfo.getUserHandle() + appItem.componentName = cn + appItem.intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + + appItem + } else { + val pkgInfo = PackageItem(badgePkg) + pkgInfo + } + } + + /** + * An extension of [BitmapDrawable] which returns the bitmap pixel size as intrinsic size. + * This allows the badging to be done based on the action bitmap size rather than + * the scaled bitmap size. + */ + private class FixedSizeBitmapDrawable(bitmap: Bitmap) : + BitmapDrawable(null, bitmap) { + override fun getIntrinsicHeight(): Int { + return bitmap.width + } + + override fun getIntrinsicWidth(): Int { + return bitmap.width + } + } + + companion object { + private const val DEFAULT_WRAPPER_BACKGROUND = Color.WHITE + } + + init { + mPm = mContext.packageManager + mFillResIconDpi = idp.fillResIconDpi + mIconBitmapSize = idp.iconBitmapSize + mCanvas = Canvas() + mCanvas.drawFilter = PaintFlagsDrawFilter( + Paint.DITHER_FLAG, + Paint.FILTER_BITMAP_FLAG + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ec34b369f827022f9fb4e4c4eedcba84c013abd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt @@ -0,0 +1,60 @@ +package foundation.e.blisslauncher.data.inject + +import android.content.Context +import android.os.Process +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.common.executors.MainThreadExecutor +import foundation.e.blisslauncher.data.launcher.LauncherAppsChangedCallbackCompat +import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVL +import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVO +import foundation.e.blisslauncher.data.compat.UserManagerCompatVN +import foundation.e.blisslauncher.data.compat.UserManagerCompatVNMr1 +import foundation.e.blisslauncher.data.compat.UserManagerCompatVP +import java.util.concurrent.Executors +import javax.inject.Singleton + +@Module +class CompatModule { + + @Provides + @Singleton + fun provideLauncherAppsCompat(context: Context): LauncherAppsCompat = + if (Utilities.ATLEAST_OREO) { + LauncherAppsCompatVO(context) + } else LauncherAppsCompatVL(context) + + @Provides + @Singleton + fun provideUserManagerCompat(context: Context): UserManagerRepository = when { + Utilities.ATLEAST_P -> { + UserManagerCompatVP(context) + } + Utilities.ATLEAST_NOUGAT_MR1 -> { + UserManagerCompatVNMr1(context) + } + else -> UserManagerCompatVN(context) + } + + @Provides + @Singleton + fun provideExecutors() = + AppExecutors( + io = Executors.newSingleThreadExecutor().apply { + execute { + Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT) + } + }, + computation = Executors.newSingleThreadExecutor(), + main = MainThreadExecutor() + ) + + @Provides + @Singleton + fun provideOnAppsChangedCallback(launcherAppsChangedCallbackCompat: LauncherAppsChangedCallbackCompat): LauncherAppsCompat.OnAppsChangedCallbackCompat = + launcherAppsChangedCallbackCompat +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..3565750fe05a1c2d46fb445e1816672bb5f658fd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt @@ -0,0 +1,22 @@ +package foundation.e.blisslauncher.data.inject + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import foundation.e.blisslauncher.domain.inject.DomainComponent +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + CompatModule::class, + DataRepoBindingModule::class + ] +) +interface DataComponent : DomainComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance applicationContext: Context): DataComponent + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..4c2c15c4f572eedfef32775faebdbf04890c516e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -0,0 +1,37 @@ +package foundation.e.blisslauncher.data.inject + +import android.content.Context +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.data.WorkspaceRepositoryImpl +import foundation.e.blisslauncher.data.LauncherStateManagerImpl +import foundation.e.blisslauncher.data.WorkspaceScreenRepositoryImpl +import foundation.e.blisslauncher.data.database.BlissLauncherFiles +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository + +@Module +class DataRepoBindingModule { + + @Provides + fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = + launcherStateManagerImpl + + @Provides + fun bindLauncherRepository(workspaceRepositoryImpl: WorkspaceRepositoryImpl): WorkspaceRepository = + workspaceRepositoryImpl + + @Provides + fun bindWorkspaceScreenRepository(workspaceScreenRepositoryImpl: WorkspaceScreenRepositoryImpl): WorkspaceScreenRepository = + workspaceScreenRepositoryImpl + + @Provides + fun provideSharedPreferences(context: Context): SharedPreferences = + context.getSharedPreferences( + BlissLauncherFiles.SHARED_PREFERENCES_KEY, + Context.MODE_PRIVATE + ) +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt b/data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt new file mode 100644 index 0000000000000000000000000000000000000000..1055d7304d394524c047d900a48cab2e6993359f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt @@ -0,0 +1,83 @@ +package foundation.e.blisslauncher.data.launcher + +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.interactor.AddPackages +import foundation.e.blisslauncher.domain.interactor.MakePackageUnavailable +import foundation.e.blisslauncher.domain.interactor.RemovePackages +import foundation.e.blisslauncher.domain.interactor.SuspendPackages +import foundation.e.blisslauncher.domain.interactor.UnsuspendPackages +import foundation.e.blisslauncher.domain.interactor.UpdatePackages +import javax.inject.Inject + +class LauncherAppsChangedCallbackCompat +@Inject constructor( + private val updatePackages: UpdatePackages, + private val addPackages: AddPackages, + private val removePackages: RemovePackages, + private val makePackageUnavailable: MakePackageUnavailable, + private val suspendPackages: SuspendPackages, + private val unsuspendPackages: UnsuspendPackages +) : + LauncherAppsCompat.OnAppsChangedCallbackCompat { + override fun onPackageRemoved(packageName: String, user: UserHandle) { + removePackages( + RemovePackages.Params(user, packageName) + ) + } + + override fun onPackageAdded(packageName: String, user: UserHandle) { + addPackages( + AddPackages.Params(user, packageName) + ) + } + + override fun onPackageChanged(packageName: String, user: UserHandle) { + updatePackages( + UpdatePackages.Params(user, packageName) + ) + } + + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + updatePackages( + UpdatePackages.Params(user, *packageNames) + ) + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + if (!replacing) { + makePackageUnavailable( + MakePackageUnavailable.Params(user, *packageNames) + ) + } + } + + override fun onPackagesSuspended(packageNames: Array, user: UserHandle) { + suspendPackages( + SuspendPackages.Params(user, *packageNames) + ) + } + + override fun onPackagesUnsuspended(packageNames: Array, user: UserHandle) { + unsuspendPackages( + UnsuspendPackages.Params(user, *packageNames) + ) + } + + override fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt b/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt new file mode 100755 index 0000000000000000000000000000000000000000..a9a5aaed7f4b51f7e7a802dfce819cf3896a8f14 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt @@ -0,0 +1,139 @@ +package foundation.e.blisslauncher.data.notification + +import android.app.Notification +import android.app.NotificationChannel +import android.content.ComponentName +import android.content.Context +import android.os.Build +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import foundation.e.blisslauncher.data.SettingsObserver +import io.reactivex.subjects.Subject + +/** + * Created by falcon on 14/3/18. + */ +class NotificationListener : NotificationListenerService() { + + private lateinit var notificationBadgingObserver: SettingsObserver + + val tempRanking = Ranking() + + init { + instance = this + } + + override fun onCreate() { + super.onCreate() + isCreated = true + } + + override fun onDestroy() { + super.onDestroy() + isCreated = false + } + + override fun onListenerConnected() { + isConnected = true + + notificationBadgingObserver = object : SettingsObserver.Secure(contentResolver) { + override fun onSettingChanged(isNotificationBadgingEnabled: Boolean) { + if (!isNotificationBadgingEnabled) { + requestUnbind() + } + } + } + notificationBadgingObserver.register(NOTIFICATION_BADGING) + updateNotifications() + } + + override fun onListenerDisconnected() { + super.onListenerDisconnected() + isConnected = false + notificationBadgingObserver.unregister() + } + + override fun onNotificationPosted(sbn: StatusBarNotification) { + updateNotifications() + } + + override fun onNotificationRemoved(sbn: StatusBarNotification) { + updateNotifications() + } + + fun getListenerIfConnected(): NotificationListener? { + return if (isConnected) instance else null + } + + fun observeNotifications(behaviourSubject: Subject>) { + /*subject = behaviourSubject + val notificationListener = getListenerIfConnected() + if (notificationListener != null) { + updateNotifications() + } else if (!isCreated) { + subject.onNext(emptyList()) + }*/ + } + + private fun updateNotifications() { + if (isSubjectInitialised()) { + if (isConnected) { + try { + subject.onNext(filterNotifications(activeNotifications).map { it.packageName }) + } catch (e: SecurityException) { + Log.e( + TAG, + "SecurityException: failed to fetch notifications" + ) + subject.onNext(emptyList()) + } + } + subject.onNext(emptyList()) + } + } + + private fun filterNotifications(notifications: Array?): List { + if (notifications == null) return emptyList() + return notifications.filter { shouldBeFilteredOut(it) } + } + + private fun shouldBeFilteredOut(sbn: StatusBarNotification): Boolean { + val notification = sbn.notification + currentRanking.getRanking(sbn.key, tempRanking) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!tempRanking.canShowBadge()) + return true + + if (tempRanking.channel.id == NotificationChannel.DEFAULT_CHANNEL_ID) { + if (notification.flags and Notification.FLAG_ONGOING_EVENT != 0) { + return true + } + } + } + + val title = notification.extras.getCharSequence(Notification.EXTRA_TITLE) + val text = notification.extras.getCharSequence(Notification.EXTRA_TEXT) + val missingTitleAndText = title.isNullOrEmpty() and text.isNullOrEmpty() + val isGroupHeader = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 + return isGroupHeader or missingTitleAndText + } + + companion object { + private var isCreated: Boolean = false + private var isConnected: Boolean = false + private var instance: NotificationListener? = null + + private lateinit var subject: Subject> + + const val NOTIFICATION_BADGING = "notification_badging" + + private const val TAG = "NotificationListener" + + private fun isSubjectInitialised() = ::subject.isInitialized + + fun requestRebind(context: Context) { + requestRebind(ComponentName(context, NotificationListener::class.java)) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae82c45700f2781cf01fb0d3108e470babe2367d --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt @@ -0,0 +1,71 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.res.XmlResourceParser +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import timber.log.Timber + +open class AppShortcutParser(context: Context) : TagParser { + + val packageManager = context.packageManager + + override fun parseAndAdd(parser: XmlResourceParser): ParseResult { + val packageName: String? = DefaultHotseatParser.getAttributeValue( + parser, + DefaultHotseatParser.ATTR_PACKAGE_NAME + ) + val className: String? = DefaultHotseatParser.getAttributeValue( + parser, + DefaultHotseatParser.ATTR_CLASS_NAME + ) + + return if (!packageName.isNullOrEmpty() && !className.isNullOrEmpty()) { + var info: ActivityInfo + try { + var cn: ComponentName? + try { + cn = ComponentName(packageName, className) + info = packageManager.getActivityInfo(cn, 0) + } catch (nnfe: PackageManager.NameNotFoundException) { + val packages: Array = + packageManager.currentToCanonicalPackageNames( + arrayOf(packageName) + ) + cn = ComponentName(packages[0], className) + info = packageManager.getActivityInfo(cn, 0) + } + val intent = + Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + .setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + return ParseResult( + 1, Triple( + info.loadLabel(packageManager).toString(), + intent, LauncherConstants.ItemType.APPLICATION + ) + ) + } catch (e: PackageManager.NameNotFoundException) { + Timber.e("Favorite not found: $packageName/$className") + return ParseResult(-1) + } + } else { + return invalidPackageOrClass(parser) + } + } + + /** + * Helper method to allow extending the parser capabilities + */ + protected open fun invalidPackageOrClass(parser: XmlResourceParser): ParseResult { + Timber.w("Skipping invalid with no component") + return ParseResult(-1) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..62ccdb27eaee43196a817846e9d2a9dfde373a4e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt @@ -0,0 +1,102 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.res.XmlResourceParser +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import timber.log.Timber +import java.net.URISyntaxException + +class AppShortcutWithUriParser(context: Context) : AppShortcutParser(context) { + + override fun invalidPackageOrClass(parser: XmlResourceParser): ParseResult { + val uri: String? = + DefaultHotseatParser.getAttributeValue(parser, DefaultHotseatParser.ATTR_URI) + if (uri.isNullOrEmpty()) { + Timber.e("Skipping invalid with no component or uri") + return ParseResult(-1) + } + val metaIntent: Intent + metaIntent = try { + Intent.parseUri(uri, 0) + } catch (e: URISyntaxException) { + Timber.e("Unable to add meta-favorite: $uri") + return ParseResult(-1) + } + var resolved: ResolveInfo = packageManager.resolveActivity( + metaIntent, + PackageManager.MATCH_DEFAULT_ONLY + ) + val appList: List = packageManager.queryIntentActivities( + metaIntent, PackageManager.MATCH_DEFAULT_ONLY + ) + + // Verify that the result is an app and not just the resolver dialog asking which + // app to use. + if (wouldLaunchResolverActivity(resolved, appList)) { + // If only one of the results is a system app then choose that as the default. + val systemApp = getSingleSystemActivity(appList) + if (systemApp == null) { + // There is no logical choice for this meta-favorite, so rather than making + // a bad choice just add nothing. + Timber.w( + "No preference or single system activity found for " + + metaIntent.toString() + ) + return ParseResult(-1) + } + resolved = systemApp + } + val info = resolved.activityInfo + val intent: Intent = + packageManager.getLaunchIntentForPackage(info.packageName) + ?: return ParseResult(-1) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + return ParseResult( + 1, Triple( + info.loadLabel(packageManager).toString(), intent, + LauncherConstants.ItemType.APPLICATION + ) + ) + } + + private fun getSingleSystemActivity(appList: List): ResolveInfo? { + var systemResolve: ResolveInfo? = null + val N = appList.size + for (i in 0 until N) { + try { + val info: ApplicationInfo = packageManager.getApplicationInfo( + appList[i].activityInfo.packageName, 0 + ) + Timber.d("$info") + if (info.flags and ApplicationInfo.FLAG_SYSTEM != 0) { + Timber.d("True for $info") + return appList[i] + } + } catch (e: PackageManager.NameNotFoundException) { + Timber.w(e, "Unable to get info about resolve results") + return null + } + } + return systemResolve + } + + private fun wouldLaunchResolverActivity( + resolved: ResolveInfo, + appList: List + ): Boolean { + // If the list contains the above resolved activity, then it can't be + // ResolverActivity itself. + for (i in appList.indices) { + val tmp = appList[i] + if (tmp.activityInfo.name == resolved.activityInfo.name && tmp.activityInfo.packageName == resolved.activityInfo.packageName) { + return false + } + } + return true + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b9efd24c2beb5c7c4d8957d667b42c9661d8020 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt @@ -0,0 +1,165 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.os.Process +import android.util.ArrayMap +import foundation.e.blisslauncher.data.LauncherDatabaseGateway +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import timber.log.Timber +import java.io.IOException + +class DefaultHotseatParser ( + private val dbGateway: LauncherDatabaseGateway, + private val defaultHotseatId: Int, + private val context: Context, + private val userManagerRepository: UserManagerRepository +) { + + private val resources: Resources = context.resources + + /** + * Loads the default hotseat layout and returns number of entries added to the desktop + */ + fun loadDefaultLayout(): Int = try { + parseLayout(defaultHotseatId) + } catch (e: Exception) { + Timber.e(e, "Error parsing layout") + -1 + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parseLayout(defaultHotseatId: Int): Int { + val parser = resources.getXml(defaultHotseatId) + beginDocument(parser, ROOT_TAG) + val depth = parser.depth + Timber.d("Depth of parser is: $depth") + var type: Int + val tagParserMap = getLayoutElementsMap() + var count = 0 + while ((parser.next().also { type = it } != XmlPullParser.END_TAG || parser.depth > depth) + && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue + } + count += parseAndAddNode(parser, tagParserMap) + } + return count + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parseAndAddNode( + parser: XmlResourceParser, + tagParserMap: ArrayMap + ): Int { + Timber.d("Parser is ${parser.name}") + val container = getAttributeValue(parser, ATTR_CONTAINER)?.toLong() + val screenId = getAttributeValue(parser, ATTR_SCREEN)?.toLong() + val rank = screenId?.toInt() + val x = getAttributeValue(parser, ATTR_X)?.toInt() + val y = getAttributeValue(parser, ATTR_Y)?.toInt() + val tagParser = tagParserMap.get(parser.name) + if (tagParser == null) { + Timber.d("Ignoring unknown element tag: ${parser.name}" ) + return 0 + } + + val parsedResult = tagParser.parseAndAdd(parser) + return if (parsedResult.result >= 0 && parsedResult.dataTriplet != null) { + val triple = parsedResult.dataTriplet + WorkspaceItem( + _id = dbGateway.generateNewItemId(), title = triple.first, + intentStr = triple.second.toUri(0), container = container!!, screen = screenId!!, + cellX = x!!, cellY = y!!, itemType = triple.third, rank = rank!!, + profileId = userManagerRepository.getSerialNumberForUser(Process.myUserHandle()) + ).let { + Timber.d("Parsed Item: $it") + dbGateway.insertAndCheck(it) + 1 + } + } else 0 + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun beginDocument( + parser: XmlPullParser, + firstElementName: String + ) { + var type: Int + while (parser.next().also { type = it } != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT + ); + if (type != XmlPullParser.START_TAG) { + throw XmlPullParserException("No start tag found") + } + if (parser.name != firstElementName) { + throw XmlPullParserException( + "Unexpected start tag: found " + parser.name + + ", expected " + firstElementName + ) + } + } + + private fun getLayoutElementsMap(): ArrayMap { + val parsers = ArrayMap() + parsers[TAG_RESOLVE] = ResolveParser(context) + return parsers + } + + companion object { + const val TAG_RESOLVE = "resolve" + const val TAG_FAVORITE = "favorite" + const val ATTR_URI = "uri" + const val ATTR_CONTAINER = "container" + const val ATTR_SCREEN = "screen" + const val ATTR_PACKAGE_NAME = "packageName" + const val ATTR_CLASS_NAME = "className" + + const val ROOT_TAG = "hotseat" + + const val ATTR_X = "x" + const val ATTR_Y = "y" + + /** + * Return attribute value, attempting launcher-specific namespace first + * before falling back to anonymous attribute. + */ + fun getAttributeValue( + parser: XmlResourceParser, + attribute: String + ): String? { + Timber.d("Attribute is $attribute") + var value = parser.getAttributeValue( + "http://schemas.android.com/apk/res-auto", attribute + ) + if (value == null) { + value = parser.getAttributeValue(null, attribute) + } + Timber.d("Value is $value") + + return value + } + + /** + * Return attribute resource value, attempting launcher-specific namespace + * first before falling back to anonymous attribute. + */ + fun getAttributeResourceValue( + parser: XmlResourceParser, attribute: String?, + defaultValue: Int + ): Int { + var value = parser.getAttributeResourceValue( + "http://schemas.android.com/apk/res-auto/foundation.e.blisslauncher", attribute, + defaultValue + ) + if (value == defaultValue) { + value = parser.getAttributeResourceValue(null, attribute, defaultValue) + } + return value + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..72a3257b036463db6ce2319e57ded84c8810478b --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Intent + +/** + * Result return after parsing the node. + * If [result] == 1, the xml node is parsed successfully and it contains additional data in + * [dataTriplet] + * + * If their is any error or no package/shortcut is found [result] is set to -1. + */ +data class ParseResult(val result: Int, val dataTriplet: Triple? = null) \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..e7924c2279594e15f6fcaf097dd6d3059a26e10b --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt @@ -0,0 +1,40 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.res.XmlResourceParser +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import timber.log.Timber +import java.io.IOException + +/** + * Contains a list of nodes, and accepts the first successfully parsed node. + */ +class ResolveParser(context: Context) : TagParser { + private val mChildParser: AppShortcutWithUriParser = AppShortcutWithUriParser(context) + + @Throws(XmlPullParserException::class, IOException::class) + override fun parseAndAdd(parser: XmlResourceParser): ParseResult { + val groupDepth = parser.depth + var type: Int + var parseResult = ParseResult(-1) + while (parser.next().also { type = it } != XmlPullParser.END_TAG || + parser.depth > groupDepth + ) { + Timber.d("Parsing Type is $type") + if (type != XmlPullParser.START_TAG || parseResult.result > -1) { + continue + } + val fallbackItemName = parser.name + Timber.d("FallbackItem $fallbackItemName") + if (DefaultHotseatParser.TAG_FAVORITE == fallbackItemName) { + parseResult = mChildParser.parseAndAdd(parser) + return parseResult + } else { + Timber.e("Fallback groups can contain only favorites, found " + + "$fallbackItemName") + } + } + return parseResult + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..caf3ef381a577897110b32fe5a026acd39436908 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt @@ -0,0 +1,15 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Intent +import android.content.res.XmlResourceParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +interface TagParser { + /** + * Parses the tag and adds to the db + * @return the id of the row added or -1; + */ + @Throws(XmlPullParserException::class, IOException::class) + fun parseAndAdd(parser: XmlResourceParser): ParseResult +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8d7b21cca86afdc8b0fa96d9f3975342a85fdcd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..79dfd154b2bbbe1bd694d71d251de03fec2f165e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Process +import timber.log.Timber +import javax.inject.Inject + +class ConfigChangedReceiver @Inject constructor(private val context: Context) : + BroadcastReceiver() { + + private val fontScale = context.resources.configuration.fontScale + private val density = context.resources.configuration.densityDpi + + override fun onReceive(context: Context, intent: Intent) { + val config = context.resources.configuration + if (fontScale != config.fontScale || density != config.densityDpi) { + Timber.d("Configuration changed, restarting launcher") + unregister() + Process.killProcess(Process.myPid()) + } + } + + fun register() { + context.registerReceiver(this, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)) + } + + fun unregister() { + context.unregisterReceiver(this) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8d7b21cca86afdc8b0fa96d9f3975342a85fdcd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..9464d2ae2b6908ee5a227e612e9f70639fe7f18c --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt @@ -0,0 +1,77 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import foundation.e.blisslauncher.domain.interactor.ChangeUserAvailability +import foundation.e.blisslauncher.domain.interactor.ChangeUserLockState +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ProfileReceiver @Inject constructor( + private val context: Context, + private val changeUserAvailability: ChangeUserAvailability, + private val changeUserLockState: ChangeUserLockState, + private val userManager: UserManagerRepository +) : + BroadcastReceiver() { + + fun register() { + // Register intent receivers + val filter = IntentFilter() + filter.addAction(Intent.ACTION_LOCALE_CHANGED) + // For handling managed profiles + // For handling managed profiles + filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) + + context.registerReceiver(this, filter) + } + + fun unregister() { + context.unregisterReceiver(this) + } + + override fun onReceive(context: Context, intent: Intent) { + + if (DEBUG_RECEIVER) Timber.d("onReceive intent=$intent") + + when (val action = intent.action) { + Intent.ACTION_LOCALE_CHANGED -> TODO("Force reload here") // If locale has been changed, clear out all the labels in workspace + Intent.ACTION_MANAGED_PROFILE_ADDED, Intent.ACTION_MANAGED_PROFILE_REMOVED -> { + userManager.enableAndResetCache() + TODO("Force reload here") + } + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> { + val user = intent.getParcelableExtra(Intent.EXTRA_USER) + if (user != null) { + if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE == action || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE == action) { + changeUserAvailability(user) + // TODO + } + + // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so + // we need to run the state change task again. + // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so + // we need to run the state change task again. + if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE == action || Intent.ACTION_MANAGED_PROFILE_UNLOCKED == action) { + changeUserLockState(user) + } + } + } + } + } + + companion object { + private const val TAG = "ProfileReceiver" + private const val DEBUG_RECEIVER = true + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8d7b21cca86afdc8b0fa96d9f3975342a85fdcd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd59306e0ed8ea35462c00db44f109f473d1387e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt @@ -0,0 +1,11 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.Context +import android.os.UserHandle + +class SessionCommitReceiver { + companion object { + fun queueAppIconAddition(context: Context, it: String, user: UserHandle) { + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8d7b21cca86afdc8b0fa96d9f3975342a85fdcd --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..64bf9a8eaac8e59304ee49581e7e8784e79cd7c2 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt @@ -0,0 +1,158 @@ +package foundation.e.blisslauncher.data.shortcuts + +import android.annotation.TargetApi +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.ShortcutQuery +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Drawable +import android.os.UserHandle +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.keys.ShortcutKey +import timber.log.Timber +import javax.inject.Inject + +/** + * Manages shortcuts, such as querying for them, pinning them, etc. + */ +class PinnedShortcutManager @Inject constructor( + private val context: Context +) { + private var wasLastCallSuccess = false + private val launcherApps: LauncherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + + fun wasLastCallSuccess() = wasLastCallSuccess + + /** + * Removes the given shortcut from the current list of pinned shortcuts. + * (Runs on background thread) + */ + @TargetApi(25) + fun unpinShortcut(key: ShortcutKey) { + if (Utilities.ATLEAST_NOUGAT_MR1) { + val packageName = key.componentName.getPackageName() + val id = key.getId() + val user = key.user + val pinnedIds = + extractIds(queryForPinnedShortcuts(packageName, user)) + pinnedIds.remove(id) + try { + launcherApps.pinShortcuts(packageName, pinnedIds, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to unpin shortcut") + wasLastCallSuccess = false + } catch (e: IllegalStateException) { + Timber.e(e, "Failed to unpin shortcut") + wasLastCallSuccess = false + } + } + } + + /** + * Adds the given shortcut to the current list of pinned shortcuts. + * (Runs on background thread) + */ + @TargetApi(25) + fun pinShortcut(key: ShortcutKey) { + if (Utilities.ATLEAST_NOUGAT_MR1) { + val packageName = key.componentName.getPackageName() + val id = key.getId() + val user = key.user + val pinnedIds = + extractIds(queryForPinnedShortcuts(packageName, user)) + pinnedIds.add(id) + try { + launcherApps.pinShortcuts(packageName, pinnedIds, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to pin shortcut") + wasLastCallSuccess = false + } catch (e: IllegalStateException) { + Timber.e(e, "Failed to pin shortcut") + wasLastCallSuccess = false + } + } + } + + @TargetApi(25) + fun getShortcutIconDrawable(shortcutInfo: ShortcutInfoCompat, density: Int): Drawable? { + if (Utilities.ATLEAST_NOUGAT_MR1) { + try { + val icon: Drawable = launcherApps.getShortcutIconDrawable( + shortcutInfo.getShortcutInfo(), density + ) + wasLastCallSuccess = true + return icon + } catch (e: SecurityException) { + Timber.e(e, "Failed to get shortcut icon") + wasLastCallSuccess = false + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to get shortcut icon") + wasLastCallSuccess = false + } + } + return null + } + + fun queryForPinnedShortcuts( + packageName: String?, + user: UserHandle? + ): List { + return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, null, user) + } + + @TargetApi(25) + private fun query( + flags: Int, + packageName: String?, + activity: ComponentName?, + shortcutIds: List?, + user: UserHandle? + ): List { + return if (Utilities.ATLEAST_NOUGAT_MR1) { + val q = ShortcutQuery() + q.setQueryFlags(flags) + if (packageName != null) { + q.setPackage(packageName) + q.setActivity(activity) + q.setShortcutIds(shortcutIds) + } + var shortcutInfos: List? = null + try { + shortcutInfos = launcherApps.getShortcuts(q, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to query for shortcuts") + wasLastCallSuccess = false + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to query for shortcuts") + wasLastCallSuccess = false + } + shortcutInfos?.map { ShortcutInfoCompat(it) } ?: emptyList() + } else { + emptyList() + } + } + + @TargetApi(25) + fun hasHostPermission(): Boolean { + if (Utilities.ATLEAST_NOUGAT_MR1) { + try { + return launcherApps.hasShortcutHostPermission() + } catch (e: SecurityException) { + Timber.e(e, "Failed to make shortcut manager call") + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to make shortcut manager call") + } + } + return false + } + + private fun extractIds(shortcuts: List): MutableList { + return shortcuts.map { it.getId() }.toMutableList() + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt b/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt new file mode 100644 index 0000000000000000000000000000000000000000..949e34b8fc8bc17cb2c8c717a764f190c04f68b9 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.data.util + +import android.os.Process +import foundation.e.blisslauncher.common.util.LabelComparator +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import javax.inject.Inject + +class LauncherItemComparator +@Inject constructor( + private val userManager: UserManagerRepository +) : + Comparator { + + private val myUser = Process.myUserHandle() + private val labelComparator = LabelComparator() + + override fun compare(itemA: LauncherItem, itemB: LauncherItem): Int { + // Order by the title in the current locale + var result: Int = labelComparator.compare(itemA.title.toString(), itemB.title.toString()) + if (result != 0) { + return result + } + + return if (myUser == itemA.user) { + -1 + } else { + val aUserSerial: Long = userManager.getSerialNumberForUser(itemA.user) + val bUserSerial: Long = userManager.getSerialNumberForUser(itemB.user) + aUserSerial.compareTo(bUserSerial) + } + } +} \ No newline at end of file diff --git a/data/src/main/res/values/config.xml b/data/src/main/res/values/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c972ebdd5fad943943660d14e472eaf1f5f36ea --- /dev/null +++ b/data/src/main/res/values/config.xml @@ -0,0 +1,4 @@ + + + 90 + \ No newline at end of file diff --git a/data/src/main/res/values/dimens.xml b/data/src/main/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..deab6f11f27657e0d575fa416d17a9692f71c197 --- /dev/null +++ b/data/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + + 8dp + 1dp + 8dp + 8dp + 8dp + + 8dp + + 5.5dp + 0dp + 8dp + + 8dp + 2dp + 80dp + 0dp + + 9dp + 6dp + 13sp + 4dp + 12dp + 14sp + 48dp + 24dp + 24dp + + \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle index dcca293e86e96225fcd3f89989e9c55b37b87f1c..812cffbad8d184d98eb563d0247ada1646297a60 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -1,30 +1,14 @@ import foundation.e.blisslauncher.buildsrc.Libs -import foundation.e.blisslauncher.buildsrc.Versions -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdkVersion Versions.compile_sdk - - defaultConfig { - minSdkVersion Versions.min_sdk - targetSdkVersion Versions.target_sdk - } - - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } -} +apply from: '../common.gradle' dependencies { - implementation Libs.Kotlin.stdlib - - kapt Libs.Dagger.compiler - + implementation project(path: ":common") + implementation Libs.RxJava.rxJava implementation Libs.RxJava.rxKotlin + implementation "androidx.room:room-rxjava2:2.2.3" + implementation Libs.timber + testImplementation Libs.junit } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3014686991ecbcf094fa70d09a78e27f45d2db9 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt @@ -0,0 +1,56 @@ +package foundation.e.blisslauncher.domain + +import android.content.ComponentName +import android.os.UserHandle +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.LauncherItem + +typealias ItemInfoMatcher = (item: LauncherItem, cn: ComponentName) -> Boolean + +fun ItemInfoMatcher.or(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + itemInfoMatcher(launcherItem, componentName) || this(launcherItem, componentName) + } +} + +fun ItemInfoMatcher.and(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + itemInfoMatcher(launcherItem, componentName) && this(launcherItem, componentName) + } +} + +fun ItemInfoMatcher.not(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + !itemInfoMatcher(launcherItem, componentName) + } +} + +class Matcher { + companion object { + fun ofPackages(packageNames: HashSet, user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + packageNames.contains(componentName.packageName) && launcherItem.user == user + } + + fun ofComponents(components: HashSet, user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + components.contains(componentName) && launcherItem.user == user + } + + fun ofItemIds(ids: LongArrayMap, matchDefault: Boolean): ItemInfoMatcher = + { launcherItem: LauncherItem, _: ComponentName -> + ids.get(launcherItem.id, matchDefault) + } + + fun ofUser(user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + launcherItem.user == user + } + } +} + +typealias ApplyFlag = (flag: Int) -> (oldFlags: Int) -> Int + +val addFlag: ApplyFlag = { flag -> { oldFlags -> oldFlags or flag } } + +val removeFlag: ApplyFlag = { flag -> { oldFlags -> oldFlags and flag.inv() } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..36580641745906e261307cfc34c012855c95fd95 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt @@ -0,0 +1,103 @@ +package foundation.e.blisslauncher.domain.dto + +import android.content.Context +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_HOTSEAT +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.ShortcutItem + +data class WorkspaceModel( + + /** + * Map of all the Items (shortcuts, folders, and widgets) created by + * LoadLauncher Interactor to their ids + */ + val itemsIdMap: LongArrayMap = LongArrayMap(), + + /** + * List of all the folders and shortcuts directly on the home screen (no widgets + * or shortcuts within folders). + */ + val workspaceItems: ArrayList = ArrayList(), + + /** + * Map of id to FolderItems of all the folders created by LauncherModel + */ + val folders: LongArrayMap = LongArrayMap(), + + /** + * Ordered list of workspace screens ids. + */ + val workspaceScreens: ArrayList = ArrayList() +) { + + /** + * Clear all data that this model holds + */ + @Synchronized + fun clear() { + workspaceItems.clear() + workspaceScreens.clear() + itemsIdMap.clear() + folders.clear() + } + + @Synchronized + fun removeItem(context: Context, items: List) { + items.forEach { + when (it.itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + workspaceItems.remove(it) + } + LauncherConstants.ItemType.FOLDER -> { + folders.remove(it.id) + //TODO: Add debug log if folder contains some items + } + } + itemsIdMap.remove(it.id) + } + } + + @Synchronized + fun addItem(context: Context, item: LauncherItem, newItem: Boolean) { + itemsIdMap.put(item.id, item) + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + workspaceItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + if (item.container == CONTAINER_DESKTOP || item.container == CONTAINER_HOTSEAT) { + workspaceItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + // Adding an item to a folder that doesn't exist. + val msg = + "adding item: " + item + " to a folder that " + + " doesn't exist" + } + } else { + findOrMakeFolder(item.container).add(item as ShortcutItem, false) + } + } + } + } + } + + private fun findOrMakeFolder(id: Long): FolderItem { + // See if a placeholder was created for us already + var folderInfo: FolderItem? = folders[id] + if (folderInfo == null) { + // No placeholder -- create a new instance + folderInfo = FolderItem() + folders.put(id, folderInfo) + } + return folderInfo + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9bc1b88618f9b06c8d0d0438e1343c179f0febe --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -0,0 +1,74 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.os.Build +import android.os.Process +import android.os.UserHandle +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.domain.keys.ComponentKey + +/** + * Represents an app in Launcher Workspace + */ +open class ApplicationItem : ShortcutItem { + + /** + * The intent used to start the application. + */ + lateinit var componentName: ComponentName + + constructor() : super() { + itemType = LauncherConstants.ItemType.APPLICATION + } + + constructor(info: LauncherActivityInfo, user: UserHandle, quietModeEnabled: Boolean) { + this.componentName = info.componentName + this.container = NO_ID.toLong() + this.user = user + this.title = info.label + intent = makeLaunchIntent(componentName) + + if (quietModeEnabled) { + runtimeStatusFlags = runtimeStatusFlags or FLAG_DISABLED_QUIET_USER + } + + updateRuntimeFlagsForActivityTarget(this, info) + } + + override fun getIntent(): Intent? = intent + + fun toComponentKey(): ComponentKey = + ComponentKey( + componentName, + user + ) + + companion object { + fun makeLaunchIntent(info: LauncherActivityInfo) = makeLaunchIntent(info.componentName) + + fun makeLaunchIntent(cn: ComponentName): Intent { + return Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + } + + fun updateRuntimeFlagsForActivityTarget( + info: LauncherItemWithIcon, + lai: LauncherActivityInfo + ) { + val appInfo = lai.applicationInfo + + info.runtimeStatusFlags = + info.runtimeStatusFlags or if (appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) FLAG_SYSTEM_NO else FLAG_SYSTEM_YES + if (Utilities.ATLEAST_OREO && + appInfo.targetSdkVersion >= Build.VERSION_CODES.O && Process.myUserHandle() == lai.user + ) { // The icon for a non-primary user is badged, hence it's not exactly an adaptive icon. + info.runtimeStatusFlags = info.runtimeStatusFlags or FLAG_ADAPTIVE_ICON + } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt new file mode 100644 index 0000000000000000000000000000000000000000..baee2c0e1c2a2bf52a7785f1073142307c05be03 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt @@ -0,0 +1,8 @@ +package foundation.e.blisslauncher.domain.entity + +/** + * The entity with only one value: the `Empty` object. Can be used in interactors those are intended to return void. + */ +object Empty : Entity { + override fun toString(): String = "BlissLauncher.Empty" +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7c4422b47ecb123627787f1b0b6f17843132a8f --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.domain.entity + +/** + * Application-wide critical business rule which is high level, changes very infrequently. + * It defines the data representation and the logic on how to manipulate this data + * and exposes both to the interactors. + * + * It should not load the data. + */ +interface Entity \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..721184b08ae441a655aec11115a312c44074b6dd --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt @@ -0,0 +1,111 @@ +package foundation.e.blisslauncher.domain.entity + +import android.os.Process +import foundation.e.blisslauncher.common.Utilities + +class FolderItem : LauncherItem() { + + var options: Int = 0 + + val contents = mutableListOf() + + val listeners = ArrayList() + + init { + itemType = LauncherConstants.ItemType.FOLDER + user = Process.myUserHandle() + } + + /** + * Add an app or shortcut + * + * @param item + */ + fun add(item: ShortcutItem, animate: Boolean) { + add(item, contents.size, animate) + } + + /** + * Add an app or shortcut for a specified rank. + */ + fun add(item: ShortcutItem, rank: Int, animate: Boolean) { + var rank = rank + rank = Utilities.boundToRange(rank, 0, contents.size) + contents.add(rank, item) + /*for (i in listeners.indices) { + listeners.get(i).onAdd(item, rank) + }*/ + itemsChanged(animate) + } + + /** + * Remove an app or shortcut. Does not change the DB. + * + * @param item + */ + fun remove(item: ShortcutItem, animate: Boolean) { + contents.remove(item) + /*for (i in listeners.indices) { + listeners.get(i).onRemove(item) + }*/ + itemsChanged(animate) + } + + fun addListener(listener: FolderListener) { + listeners.add(listener) + } + + fun removeListener(listener: FolderListener) { + listeners.remove(listener) + } + + fun itemsChanged(animate: Boolean) { + for (i in listeners.indices) { + listeners.get(i).onItemsChanged(animate) + } + } + + fun prepareAutoUpdate() { + for (i in listeners.indices) { + listeners.get(i).prepareAutoUpdate() + } + } + + interface FolderListener { + fun onAdd(item: ShortcutItem, rank: Int) + fun onRemove(item: ShortcutItem) + fun onTitleChanged(title: CharSequence) + fun onItemsChanged(animate: Boolean) + fun prepareAutoUpdate() + } + + fun hasOption(optionFlag: Int): Boolean { + return options and optionFlag != 0 + } + + /** + * @param option flag to set or clear + * @param isEnabled whether to set or clear the flag + * @param writer if not null, save changes to the db. + */ + fun setOption(option: Int, isEnabled: Boolean) { + val oldOptions = options + options = if (isEnabled) { + options or option + } else { + options and option.inv() + } + /*if (writer != null && oldOptions != options) { + writer.updateItemInDatabase(this) + }*/ + } + + companion object { + const val NO_FLAGS = 0x00000000 + + /** + * Represent a work folder + */ + const val FLAG_WORK_FOLDER = 0x00000002 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3589fafd9e6c907fa14d0cdf0bbf69ab51ea2ac --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.domain.entity + +class LauncherConstants { + + object ItemType { + + const val APPLICATION = 0 + const val SHORTCUT = 1 + const val FOLDER = 2 + const val APPWIDGET = 4 + const val CUSTOM_APPWIDGET = 5 + const val DEEP_SHORTCUT = 6 + + fun itemTypeToString(type: Int): String = when (type) { + APPLICATION -> "APP" + SHORTCUT -> "SHORTCUT" + FOLDER -> "FOLDER" + APPWIDGET -> "WIDGET" + CUSTOM_APPWIDGET -> "CUSTOMWIDGET" + DEEP_SHORTCUT -> "DEEPSHORTCUT" + else -> type.toString() + } + } + + object ContainerType { + + const val CONTAINER_DESKTOP: Long = -100 + const val CONTAINER_HOTSEAT: Long = -101 + + fun containerToString(container: Long): String = when (container) { + CONTAINER_DESKTOP -> "desktop" + CONTAINER_HOTSEAT -> "hotseat" + else -> container.toString() + } + } +} diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd5676e82d1c18b8d97775585bec9d641eb6aace --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt @@ -0,0 +1,148 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import android.os.UserHandle + +/** + * Represents an item in BlissLauncher. + */ +open class LauncherItem : Entity { + + /** + * The id in the settings database for this item + */ + var id: Long = NO_ID.toLong() + + /** + * One of [LauncherConstants.ItemType.APPLICATION], + * [LauncherConstants.ItemType.SHORTCUT], + * [LauncherConstants.ItemType.FOLDER] + * [LauncherConstants.ItemType.APPWIDGET], + * [LauncherConstants.ItemType.CUSTOM_APPWIDGET] or + * [LauncherConstants.ItemType.DEEP_SHORTCUT]. + */ + var itemType = 0 + + /** + * The id of the container that holds this item. For the desktop, this will be + * [LauncherConstants.ContainerType.CONTAINER_DESKTOP]. For the all applications folder it + * will be [.NO_ID] (since it is not stored in the settings DB). For user folders + * it will be the id of the folder. + */ + var container: Long = NO_ID.toLong() + + /** + * Indicates the screen in which the shortcut appears if the container types is + * [LauncherConstants.ContainerType.CONTAINER_DESKTOP]. (i.e., ignore if the container type is + * [LauncherConstants.ContainerType.CONTAINER_HOTSEAT]) + */ + var screenId: Long = -1 + + /** + * Indicates the X position of the associated cell. + */ + var cellX = -1 + + /** + * Indicates the Y position of the associated cell. + */ + var cellY = -1 + + /** + * Indicates the X cell span. + */ + var spanX = 1 + + /** + * Indicates the Y cell span. + */ + var spanY = 1 + + /** + * Indicates the minimum X cell span. + */ + var minSpanX = 1 + + /** + * Indicates the minimum Y cell span. + */ + var minSpanY = 1 + + /** + * Indicates the position in an ordered list. + */ + var rank = 0 + + /** + * Title of the item + */ + var title: CharSequence? = null + + /** + * Content description of the item. + */ + var contentDescription: CharSequence? = null + + lateinit var user: UserHandle + + constructor() { + user = Process.myUserHandle() + } + + constructor(item: LauncherItem) { + copyFrom(item) + } + + private fun copyFrom(item: LauncherItem) { + id = item.id + cellX = item.cellX + cellY = item.cellY + spanX = item.spanX + spanY = item.spanY + rank = item.rank + screenId = item.screenId + itemType = item.itemType + container = item.container + user = item.user + contentDescription = item.contentDescription + } + + open fun getIntent(): Intent? { + return null + } + + open fun getTargetComponent(): ComponentName? { + val intent = getIntent() + return intent?.component + } + + override fun toString(): String { + return javaClass.simpleName + "(" + dumpProperties() + ")" + } + + protected open fun dumpProperties(): String? { + return ("id=" + id + + " type=" + LauncherConstants.ItemType.itemTypeToString(itemType) + + " container=" + LauncherConstants.ContainerType.containerToString(container) + + " screen=" + screenId + + " cell(" + cellX + "," + cellY + ")" + + " span(" + spanX + "," + spanY + ")" + + " minSpan(" + minSpanX + "," + minSpanY + ")" + + " rank=" + rank + + " user=" + user + + " title=" + title) + } + + /** + * Whether this item is disabled. + */ + open fun isDisabled(): Boolean { + return false + } + + companion object { + const val NO_ID = -1 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt new file mode 100644 index 0000000000000000000000000000000000000000..b62c30910c7227cec18bd89892588ccdfb329100 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt @@ -0,0 +1,100 @@ +package foundation.e.blisslauncher.domain.entity + +import android.graphics.Bitmap + +/** + * Represents a LauncherItem which also have an icon. + */ +abstract class LauncherItemWithIcon : LauncherItem { + + /** + * A bitmap version of the application icon. + */ + var iconBitmap: Bitmap? = null + + /** + * Dominant color in the [.iconBitmap]. + */ + var iconColor = 0 + + /** + * Indicates whether we're using a low res icon + */ + var usingLowResIcon = false + + /** + * Status associated with the system state of the underlying item. This is calculated every + * time a new info is created and not persisted on the disk. + */ + var runtimeStatusFlags = 0 + + constructor() + + constructor(item: LauncherItemWithIcon) : super(item) { + iconBitmap = item.iconBitmap + iconColor = item.iconColor + usingLowResIcon = item.usingLowResIcon + runtimeStatusFlags = item.runtimeStatusFlags + } + + override fun isDisabled(): Boolean = (runtimeStatusFlags and FLAG_DISABLED_MASK) != 0 + + companion object { + /** + * Indicates that the icon is disabled due to safe mode restrictions. + */ + const val FLAG_DISABLED_SAFEMODE = 1 shl 0 + + /** + * Indicates that the icon is disabled as the app is not available. + */ + const val FLAG_DISABLED_NOT_AVAILABLE = 1 shl 1 + + /** + * Indicates that the icon is disabled as the app is suspended + */ + const val FLAG_DISABLED_SUSPENDED = 1 shl 2 + + /** + * Indicates that the icon is disabled as the user is in quiet mode. + */ + const val FLAG_DISABLED_QUIET_USER = 1 shl 3 + + /** + * Indicates that the icon is disabled as the publisher has disabled the actual shortcut. + */ + const val FLAG_DISABLED_BY_PUBLISHER = 1 shl 4 + + /** + * Indicates that the icon is disabled as the user partition is currently locked. + */ + const val FLAG_DISABLED_LOCKED_USER = 1 shl 5 + + const val FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE or + FLAG_DISABLED_NOT_AVAILABLE or FLAG_DISABLED_SUSPENDED or + FLAG_DISABLED_QUIET_USER or FLAG_DISABLED_BY_PUBLISHER or FLAG_DISABLED_LOCKED_USER + + /** + * The item points to a system app. + */ + const val FLAG_SYSTEM_YES = 1 shl 6 + + /** + * The item points to a non system app. + */ + const val FLAG_SYSTEM_NO = 1 shl 7 + + const val FLAG_SYSTEM_MASK = FLAG_SYSTEM_YES or FLAG_SYSTEM_NO + + /** + * Flag indicating that the icon is an [android.graphics.drawable.AdaptiveIconDrawable] + * that can be optimized in various way. + */ + const val FLAG_ADAPTIVE_ICON = 1 shl 8 + + /** + * Flag indicating that the icon is badged. + */ + const val FLAG_ICON_BADGED = 1 shl 9 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..ad79ccca4a6cfefad1c51a0a0b5d4938abbb5272 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt @@ -0,0 +1,3 @@ +package foundation.e.blisslauncher.domain.entity + +data class PackageItem(var packageName: String?): LauncherItemWithIcon() \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4969a90dc9b1da03fb6cc8df5adedcc54d491c0 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt @@ -0,0 +1,111 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ShortcutIconResource + +/** + * Represents an item in workspace and inside folder. + * It can be an Application, Shortcut or Deep Shortcut. + * Also used for pinned and dynamic shortcuts of the apps. + */ +open class ShortcutItem : LauncherItemWithIcon { + + /** + * The intent used to start the application. + */ + @get:JvmName("_getIntent") + var intent: Intent? = null + + /** + * If isShortcut=true and customIcon=false, this contains a reference to the + * shortcut icon as an application's resource. + */ + var iconResource: ShortcutIconResource? = null + + /** + * A message to display when the user tries to start a disabled shortcut. + * This is currently only used for deep shortcuts. + */ + var disabledMessage: CharSequence? = null + + var status = 0 + + /** + * The installation progress [0-100] of the package that this shortcut represents. + */ + private var installProgress = 0 + set(value) { + status = status or FLAG_INSTALL_SESSION_ACTIVE + field = value + } + + constructor() { + itemType = LauncherConstants.ItemType.SHORTCUT + } + + constructor(item: ShortcutItem) : super(item) { + title = item.title + intent = item.getIntent() + iconResource = item.iconResource + status = item.status + installProgress = item.installProgress + } + + override fun getIntent(): Intent? { + return intent + } + + fun hasStatusFlag(flag: Int) = status and flag != 0 + + fun isPromise() = hasStatusFlag(FLAG_RESTORED_ICON or FLAG_AUTOINSTALL_ICON) + + fun hasPromiseIconUi(): Boolean = isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI) + + override fun getTargetComponent(): ComponentName? { + val cn = super.getTargetComponent() + if (cn == null && (itemType == LauncherConstants.ItemType.SHORTCUT || + hasStatusFlag(FLAG_SUPPORTS_WEB_UI)) + ) { + // Legacy shortcuts and promise icons with web UI may not have a componentName but just + // a packageName. In that case create a dummy componentName instead of adding additional + // check everywhere. + val pkg: String? = intent?.getPackage() + return if (pkg == null) null else ComponentName(pkg, ".") + } + return cn + } + + companion object { + val DEFAULT = 0 + + /** + * The shortcut was restored from a backup and it not ready to be used. This is automatically + * set during backup/restore + */ + const val FLAG_RESTORED_ICON = 1 + + /** + * The icon was added as an auto-install app, and is not ready to be used. This flag can't + * be present along with [.FLAG_RESTORED_ICON], and is set during default layout + * parsing. + */ + const val FLAG_AUTOINSTALL_ICON = 2 //0B10; + + /** + * The icon is being installed. If [.FLAG_RESTORED_ICON] or [.FLAG_AUTOINSTALL_ICON] + * is set, then the icon is either being installed or is in a broken state. + */ + const val FLAG_INSTALL_SESSION_ACTIVE = 4 // 0B100; + + /** + * Indicates that the widget restore has started. + */ + const val FLAG_RESTORE_STARTED = 8 //0B1000; + + /** + * Web UI supported. + */ + const val FLAG_SUPPORTS_WEB_UI = 16 //0B10000; + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4fe0bdd08ac3660cea6ba886742a196ea631235 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.domain.entity + +data class WorkspaceScreen( + val id: Long, + val screenRank: Int +) \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt deleted file mode 100644 index ba8ed2ca00f0cd30c86908b522b3adf42eedacdd..0000000000000000000000000000000000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt +++ /dev/null @@ -1,7 +0,0 @@ -package foundation.e.blisslauncher.domain.executors - -import io.reactivex.Scheduler - -interface PostExecutionThread { - val scheduler: Scheduler -} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt deleted file mode 100644 index f2b27ad31beacca6d30360477de737e9baa5f98a..0000000000000000000000000000000000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt +++ /dev/null @@ -1,5 +0,0 @@ -package foundation.e.blisslauncher.domain.executors - -import java.util.concurrent.Executor - -interface ThreadExecutor : Executor \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..03ff9539143fa0058cfbfe0efe6f3ca66851e91e --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.domain.inject + +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository + +/** + * Interface that lists all public repositories and data access layer components which are needed + * to be exposed to the `domain` layer + */ +interface DomainComponent { + + fun appExecutors(): AppExecutors + + fun launcherAppsCompat(): LauncherAppsCompat + + fun workspaceRepository(): WorkspaceRepository + + + companion object { + @Volatile + @JvmStatic + lateinit var INSTANCE: DomainComponent + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bbe2d4a6fdf735eadae875ef3ded8660867c2d0 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt @@ -0,0 +1,31 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class AddPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val userManager: UserManagerRepository, + private val observeAddedApps: ObserveAddedApps +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + params.packages.forEach { + //TODO: Update icons cache + //launcherItemRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) + //TODO: Add SessionCommitReceiver for below O devices + } + } /*.doOnComplete { + observeAddedApps(Unit) + }*/ +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt new file mode 100644 index 0000000000000000000000000000000000000000..fd8894104748cd303b83eea10f1a1850651e0503 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class ChangeUserAvailability @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val userManager: UserManagerRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: UserHandle): Completable = Completable.fromAction { + /*observeUpdatedLauncherItems( + launcherItemRepository.updateUserAvailability( + params, + userManager.isQuietModeEnabled(params) + ) + )*/ + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a1140977aeed88932a7509f3334eda3c145e580 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import io.reactivex.Flowable +import javax.inject.Inject + +class ChangeUserLockState @Inject constructor(appExecutors: AppExecutors) : + PublishSubjectInteractor() { + override val subscribeExecutor = appExecutors.io + override val observeExecutor = appExecutors.main + + override fun createObservable(params: UserHandle): Flowable { + return Flowable.just("") + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt new file mode 100644 index 0000000000000000000000000000000000000000..dcda2e83b1389fce7ab124dd39f21f66754df18d --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt @@ -0,0 +1,14 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import io.reactivex.Completable +import java.util.concurrent.Executor + +class DeleteComponents(appExecutors: AppExecutors) : CompletableInteractor() { + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: ItemInfoMatcher): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt new file mode 100644 index 0000000000000000000000000000000000000000..a922d0d8281a6f7cf9e809f716392ce6eb71d04b --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt @@ -0,0 +1,112 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.Looper +import io.reactivex.BackpressureStrategy +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import timber.log.Timber +import java.util.concurrent.Executor + +/** + * Interactor to execute tasks synchronously on main thread. + */ +abstract class SynchronousInteractor { + abstract fun doWork(params: P) + + operator fun invoke(params: P, block: () -> Unit = {}) { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw IllegalStateException("Can't be executed from thread other than main") + } + doWork(params) + block() // To do any additional work + } +} + +abstract class AsyncInteractor : Disposable { + abstract val subscribeExecutor: Executor + + protected val disposables: CompositeDisposable = CompositeDisposable() + + override fun dispose() = disposables.dispose() + + override fun isDisposed(): Boolean = disposables.isDisposed +} + +abstract class CompletableInteractor : AsyncInteractor

() { + + protected abstract fun doWork(params: P): Completable + + operator fun invoke(params: P, onComplete: () -> Unit = {}) { + this.disposables += doWork(params) + .subscribeOn(Schedulers.from(subscribeExecutor)) + .subscribe(onComplete, Timber::w) + } + + fun executeSync(params: P) { + doWork(params) + } +} + +abstract class ObservableInteractor : AsyncInteractor

() { + abstract val observeExecutor: Executor +} + +abstract class ResultInteractor : ObservableInteractor

() { + + abstract fun doWork(params: P? = null): Single + + operator fun invoke( + params: P? = null + ): Single { + return this.doWork(params) + } +} + +abstract class SubjectInteractor : ObservableInteractor

() { + abstract val subject: Subject

+ + protected abstract fun createObservable(params: P): Flowable + + operator fun invoke(params: P) = subject.onNext(params) + + fun observe(onNext: (result: T) -> Unit = {}) { + disposables += subject.toFlowable(BackpressureStrategy.BUFFER) + .flatMap { createObservable(it) } + .subscribeOn(Schedulers.from(subscribeExecutor)) + .observeOn(Schedulers.from(observeExecutor)) + .subscribe(onNext) + } +} + +abstract class PublishSubjectInteractor : SubjectInteractor() { + override val subject: Subject

= PublishSubject.create() +} + +abstract class BehaviourSubjectInteractor : SubjectInteractor() { + override val subject: Subject

= BehaviorSubject.create() +} + +abstract class FlowableInteractor : ObservableInteractor

() { + + protected abstract fun buildObservable(params: P? = null): Flowable + + operator fun invoke( + params: P, + onNext: (next: T) -> Unit = {}, + onError: (e: Throwable) -> Unit = {}, + onComplete: () -> Unit = {} + ) { + disposables += this.buildObservable(params) + .subscribeOn(Schedulers.from(subscribeExecutor)) + .observeOn(Schedulers.from(observeExecutor)) + .subscribe(onNext, onError, onComplete) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..2596ee0b3499d3ff0c0fa930fc025a5e524b1c13 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt @@ -0,0 +1,19 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LauncherStateInteractor @Inject constructor(private val launcherStateManager: LauncherStateManager) : + SynchronousInteractor() { + + override fun doWork(command: Command) { + if (command == Command.INIT) + launcherStateManager.init() + else if (command == Command.TERMINATE) + launcherStateManager.terminate() + } + + enum class Command { INIT, TERMINATE } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..17268e9285409e2f3242cbdac75c7b26917d6ed3 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LoadLauncher @Inject constructor( + private val workspaceRepository: WorkspaceRepository, + appExecutors: AppExecutors +) : ResultInteractor() { + + override val subscribeExecutor: Executor = appExecutors.io + override val observeExecutor: Executor = appExecutors.main + + override fun doWork(params: Unit?): Single = + Single.fromCallable { workspaceRepository.loadWorkspace() } + .subscribeOn(Schedulers.from(subscribeExecutor)) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt new file mode 100644 index 0000000000000000000000000000000000000000..27c462b677fd134e0666ba11ba30feb04d89b167 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class MakePackageUnavailable @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + /*observeUpdatedLauncherItems( + launcherItemRepository.makePackagesUnavailable( + params.packages, + params.user + ) + )*/ + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd00cd8d7152187b989e2ce653c4a9727131f6ff --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt @@ -0,0 +1,23 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Flowable +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ObserveAddedApps @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository +) : PublishSubjectInteractor, List>() { + + override val subscribeExecutor: Executor = appExecutors.io + + override val observeExecutor: Executor = appExecutors.main + + override fun createObservable(params: List): Flowable> = + Flowable.just(params) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt new file mode 100644 index 0000000000000000000000000000000000000000..00df62b05e6f67b215403111e5eef633531f8a35 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.domain.interactor diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt new file mode 100644 index 0000000000000000000000000000000000000000..00df62b05e6f67b215403111e5eef633531f8a35 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.domain.interactor diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt new file mode 100644 index 0000000000000000000000000000000000000000..4006bcb24106d6bb0c327756399251b6cec6c72b --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.entity.LauncherItem +import io.reactivex.Flowable +import java.util.concurrent.Executor +import javax.inject.Inject + +class ObserveUpdatedLauncherItems @Inject constructor(appExecutors: AppExecutors) : + PublishSubjectInteractor, List>() { + override val subscribeExecutor: Executor = appExecutors.io + override val observeExecutor: Executor = appExecutors.main + + override fun createObservable(params: List): Flowable> = + Flowable.just(params) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt new file mode 100644 index 0000000000000000000000000000000000000000..edc2554d10a21a1705ce2ff3bd85b58633579488 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class RemovePackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val observeAddedApps: ObserveAddedApps +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + + //launcherItemRepository.removePackages(params.packages, params.user) + }.doOnComplete { + //observeAddedApps(Unit) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ce960ccf22d80ef535fc32865f36c26f05c2cad --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class SuspendPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + /*observeUpdatedLauncherItems( + launcherItemRepository.suspendPackages( + params.packages, + params.user + ) + )*/ + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt new file mode 100644 index 0000000000000000000000000000000000000000..c09428bdac4bb8fdbc592b15703ff2ed092acfb1 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class UnsuspendPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + /*observeUpdatedLauncherItems( + launcherItemRepository.unsuspendPackages( + params.packages, + params.user + ) + )*/ + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..17aa60b3ce422ee899b43bed1ed25f88e1721120 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -0,0 +1,161 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.content.ComponentName +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import foundation.e.blisslauncher.domain.Matcher +import foundation.e.blisslauncher.domain.and +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.ShortcutItem +import foundation.e.blisslauncher.domain.or +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import io.reactivex.Completable +import java.util.concurrent.Executor + +class UpdateLauncher( + appExecutors: AppExecutors, + private val launcherItemRepository: LauncherItemRepository, + private val launcherAppsCompat: LauncherAppsCompat, + private val deleteComponents: DeleteComponents +) : CompletableInteractor() { + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + val addedOrUpdated = ArrayList() + /* addedOrUpdated.addAll(appsRepository.getModifiedApps()) + addedOrUpdated.addAll(appsRepository.getAddedApps()) + + val removedApps = ArrayList(appsRepository.getRemovedApps()) + appsRepository.clear()*/ + + val addedOrUpdatedApps = ArrayMap() + if (addedOrUpdated.isNotEmpty()) { + // TODO: Push updated apps here + addedOrUpdated.forEach { + addedOrUpdatedApps[it.componentName] = it + } + } + + // LauncherItems that are about to be removed + val removedItems = LongArrayMap() + + val isNewApkAvailable = params.command == Command.ADD || params.command == Command.UPDATE + val updatedItems = ArrayList() + //TODO: Uncomment it after successful presentation test. + //val map = launcherRepository.allItemsMap() + /*map.forEach { + if (it is WorkspaceItem && params.user == it.user) it.let { workspaceItem -> + var itemUpdated = false + var shortcutUpdated = false + if (workspaceItem.iconResource != null && params.packages.contains(workspaceItem.iconResource!!.packageName)) { + //TODO: Update shortcut icon here + itemUpdated = true + } + + val cn = workspaceItem.getTargetComponent() + if (cn != null && params.matcher(workspaceItem, cn)) { + val applicationItem = addedOrUpdatedApps[cn] + if (workspaceItem.hasStatusFlag(WorkspaceItem.FLAG_SUPPORTS_WEB_UI)) { + removedItems.put(it.id, false) + if (params.command == Command.REMOVE) { + return@forEach + } + } + + if (workspaceItem.isPromise() && isNewApkAvailable) { + if(workspaceItem.hasStatusFlag(WorkspaceItem.FLAG_AUTOINSTALL_ICON)) { + if(launcherAppsCompat.isActivityEnabledForProfile(cn, params.user)) { + + } + } + } + + if (isNewApkAvailable && + workspaceItem.itemType == LauncherConstants.ItemType.APPLICATION) { + // update icon cache from tile + itemUpdated = true + } + + val oldRuntimeFlags = workspaceItem.runtimeStatusFlags + workspaceItem.runtimeStatusFlags = + params.flagOp(workspaceItem.runtimeStatusFlags) + if (oldRuntimeFlags != workspaceItem.runtimeStatusFlags) { + shortcutUpdated = true + } + } + + if (itemUpdated || shortcutUpdated) { + updatedItems.add(workspaceItem) + } + + if (itemUpdated) { + //TODO: Updated item in database here + } + + } + //TODO: Update launcher widgets here + }*/ + + // TODO: Update Shortcut here + if (!removedItems.isEmpty) { + deleteComponents(Matcher.ofItemIds(removedItems, false)) + } + // TODO: Update or Add new apps here + // TODO: Update widgets here + + val removedPackages = HashSet() + val removedComponents = HashSet() + + if (params.command == Command.REMOVE) { + removedPackages.addAll(params.packages) + } else if (params.command == Command.UPDATE) { + params.packages.forEach { + if (!launcherAppsCompat.isPackageEnabledForProfile(it, params.user)) { + removedPackages.add(it) + } + } + //TODO + //Update removedComponents because some packages can get removed during package update + /*removedApps.forEach { + removedComponents.add(it.componentName) + }*/ + } + + if (removedPackages.isNotEmpty() || removedComponents.isNotEmpty()) { + val removeMatch = Matcher.ofPackages(removedPackages, params.user) + .or(Matcher.ofComponents(removedComponents, params.user)) + .and(Matcher.ofItemIds(removedItems, true)) + deleteComponents(removeMatch) + + //TODO: Remove packages from InstallQueue + } + + if (Utilities.ATLEAST_OREO && params.command == Command.ADD) { + // Load widgets for the new package. Changes due to app updates are handled through + // AppWidgetHost events, this is just to initialize the long-press options. + /*for (i in 0 until N) { + dataModel.widgetsModel.update(app, PackageUserKey(packages.get(i), mUser)) + } + bindUpdatedWidgets(dataModel)*/ + //TODO: Update widget model here + } + } + + data class Params( + val command: Command, + val packages: HashSet, + val user: UserHandle, + val matcher: ItemInfoMatcher, + val flagOp: (flag: Int) -> Int + ) + + enum class Command { + ADD, UPDATE, REMOVE, UNAVAILABLE, SUSPEND, UNSUSPEND, USER_AVAILABILITY_CHANGE + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt new file mode 100644 index 0000000000000000000000000000000000000000..2294ace20ea77ba99b65b4d3bf43bfb8f662c27c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt @@ -0,0 +1,31 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.content.Context +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class UpdatePackages @Inject constructor( + appExecutors: AppExecutors, + private val context: Context, + private val observeAddedApps: ObserveAddedApps, + private val launcherAppsCompat: LauncherAppsCompat +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + params.packages.forEach { + // TODO: update icon cache + //appsRepository.updateApp(context, it, params.user) + //TODO: Remove from widget cache + } + }.doOnComplete { + //observeAddedApps(Unit) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt deleted file mode 100644 index 144ad143f1e9ea9003559ac72ca3be03011eae04..0000000000000000000000000000000000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt +++ /dev/null @@ -1,52 +0,0 @@ -package foundation.e.blisslauncher.domain.interactors - -import foundation.e.blisslauncher.domain.executors.PostExecutionThread -import foundation.e.blisslauncher.domain.executors.ThreadExecutor -import io.reactivex.Completable -import io.reactivex.Flowable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.schedulers.Schedulers -import timber.log.Timber - -interface Interactor { - val threadExecutor: ThreadExecutor - val postExecutionThread: PostExecutionThread -} - -abstract class FlowableInteractor : Interactor

, Disposable { - - private val disposables: CompositeDisposable = CompositeDisposable() - - protected abstract fun buildObservable(params: P? = null): Flowable - - operator fun invoke(params: P, onNext: (next: T) -> Unit = {}, onComplete: () -> Unit = {}) { - disposables += this.buildObservable(params) - .subscribeOn(Schedulers.from(threadExecutor)) - .observeOn(postExecutionThread.scheduler) - .subscribe(onNext, Timber::w, onComplete) - } - - override fun dispose() = disposables.dispose() - - override fun isDisposed(): Boolean = disposables.isDisposed -} - -abstract class CompletableInteractor : Interactor

, Disposable { - - private val disposables: CompositeDisposable = CompositeDisposable() - - protected abstract fun buildObservable(params: P? = null): Completable - - operator fun invoke(params: P, onComplete: () -> Unit = {}) { - disposables += this.buildObservable(params) - .subscribeOn(Schedulers.from(threadExecutor)) - .observeOn(postExecutionThread.scheduler) - .subscribe(onComplete, Timber::w) - } - - override fun dispose() = disposables.dispose() - - override fun isDisposed(): Boolean = disposables.isDisposed -} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..967d7eb51c73257ff10840927d1b464eb082f7da --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt @@ -0,0 +1,19 @@ +package foundation.e.blisslauncher.domain.keys + +import android.content.ComponentName +import android.os.UserHandle + +open class ComponentKey(val componentName: ComponentName, val user: UserHandle) { + private val hashCode = arrayOf(componentName, user).contentHashCode() + + override fun hashCode(): Int = hashCode + + override fun equals(any: Any?): Boolean { + val other = any as ComponentKey + return (other.componentName == componentName) and (other.user == user) + } + + override fun toString(): String { + return "${componentName.flattenToString()}#$user" + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f640e23507d06967c7f8493aa63605959aeef08 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.domain.keys + +import android.service.notification.StatusBarNotification +import java.util.ArrayList + +/** + * The key data associated with the notification, used to determine what to include + * in badges and dummy popup views before they are populated. + * + * @see NotificationInfo for the full data used when populating the dummy views. + */ +class NotificationKey private constructor( + val notificationKey: String, + val shortcutId: String, + count: Int +) { + var count: Int + override fun equals(obj: Any?): Boolean { + return if (obj !is NotificationKey) { + false + } else obj.notificationKey == notificationKey + // Only compare the keys. + } + + companion object { + fun fromNotification(sbn: StatusBarNotification): NotificationKey { + val notif = sbn.notification + return NotificationKey(sbn.key, notif.shortcutId, notif.number) + } + + fun extractKeysOnly(notificationKeys: List): List { + val keysOnly: MutableList = + ArrayList(notificationKeys.size) + for (notificationKey in notificationKeys) { + keysOnly.add(notificationKey.notificationKey) + } + return keysOnly + } + } + + init { + this.count = Math.max(1, count) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..13d1042b1aad3c8143652d295c547c3a1311bf62 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt @@ -0,0 +1,43 @@ +package foundation.e.blisslauncher.domain.keys + +import android.os.UserHandle +import android.service.notification.StatusBarNotification +import foundation.e.blisslauncher.domain.entity.LauncherItem + +class PackageUserKey(var packageName: String?, var user: UserHandle?) { + private val hashCode: Int = arrayOf(packageName, user).contentHashCode() + + override fun hashCode(): Int = hashCode + + override fun equals(other: Any?): Boolean { + if (other !is PackageUserKey) return false + return other.packageName == packageName && other.user == user + } + + /** + * This should only be called to avoid new object creations in a loop. + * @return Whether this PackageUserKey was successfully updated - it shouldn't be used if not. + */ + fun updateFromItemInfo(info: LauncherItem): Boolean { + /*if (DeepShortcutManager.supportsShortcuts(info)) { + update(info.getTargetComponent().getPackageName(), info.user) + return true + } + return false*/ + TODO() + } + + companion object { + fun fromLauncherItem(item: LauncherItem): PackageUserKey = + PackageUserKey( + item.getTargetComponent()?.packageName, + item.user + ) + + fun fromNotification(sbn: StatusBarNotification): PackageUserKey = + PackageUserKey( + sbn.packageName, + sbn.user + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..604972a98886fa92b2c6a11ff476192abea79d21 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt @@ -0,0 +1,34 @@ +package foundation.e.blisslauncher.domain.keys + +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.entity.LauncherItem + +class ShortcutKey(componentName: ComponentName, user: UserHandle) : + ComponentKey(componentName, user) { + + constructor(packageName: String, user: UserHandle, id: String) : this( + ComponentName( + packageName, + id + ), user + ) + + fun getId() = componentName.className + + companion object { + fun fromShortcutInfoCompat(shortcutInfo: ShortcutInfoCompat): ShortcutKey = ShortcutKey( + shortcutInfo.getPackage(), shortcutInfo.getUserHandle(), shortcutInfo.getId() + ) + + fun fromIntent(intent: Intent, user: UserHandle) = ShortcutKey( + intent.`package`!!, + user, + intent.getStringExtra(ShortcutInfoCompat.EXTRA_SHORTCUT_ID) + ) + + fun fromLauncherItem(item: LauncherItem) = fromIntent(item.getIntent()!!, item.user) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..499f25bbb8e1b45b4fcf59ba76fcf7cb71e141ac --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.domain.manager + +interface LauncherStateManager { + fun init() + fun terminate() +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d6beaaa0d2a73b66cd802024402f4f2df2318f4 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.domain.repository + +import foundation.e.blisslauncher.domain.entity.LauncherItem + +/** + * Repository to manage [LauncherItem] + */ +interface LauncherItemRepository : Repository + +/*fun getAllActivities(user: UserHandle, quietMode: Boolean): List + + *//** + * Functions to fetch/add/update/remove AppsRepository + *//* + fun add(packageName: String, user: UserHandle, quietMode: Boolean): List + + fun remove(packageName: String, user: UserHandle) + + fun updatedPackages( + packages: Array, + user: UserHandle, + quietMode: Boolean + ): List + + fun suspendPackages(packages: Array, user: UserHandle): List + + fun unsuspendPackages(packages: Array, user: UserHandle): List + + fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List + + fun makePackagesUnavailable(packages: Array, user: UserHandle): List + + fun removePackages(packages: Array, user: UserHandle): List*/ \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt new file mode 100644 index 0000000000000000000000000000000000000000..269e3da42c359e153fd9d995053bf1305502fcea --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt @@ -0,0 +1,63 @@ +package foundation.e.blisslauncher.domain.repository + +/** + * Generic Repository interface which captures the domain type to manage with generic CRUD operations. + * + * @param the domain type which the repository manages. + * @param the type of the id of the entity which the repository manages. + * + * @author Amit Kumar + */ +interface Repository { + + /** + * Saves a given entity and return the instance for further operations. + * + * @param entity to be saved. + * @return [S] entity + */ + fun save(entity: S): S + + /** + * Saves all given entities. + * + * @param entities to be saved. + * @return [List] with the saved entities. + */ + fun saveAll(entities: List): List + + /** + * Retrieves an entity by its id. + * + * @param id of the entity. + * @return [T] the entity with the given id or null if none found. + */ + fun findById(id: ID): T? + + /** + * Return all entities of this type. + * + * @return [List] with all entities. + */ + fun findAll(): List + + /** + * Deletes a given entity. + */ + fun delete(entity: T) + + /** + * Deletes the entity with the given id. + */ + fun deleteById(id: ID) + + /** + * Deletes all entities managed by this repository. + */ + fun deleteAll() + + /** + * Deletes the given entities. + */ + fun deleteAll(entities: List) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..715a3628f859e4a0bdc1a820dbcff63507548fc2 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt @@ -0,0 +1,29 @@ +package foundation.e.blisslauncher.domain.repository + +import android.os.UserHandle + +interface UserManagerRepository { + + val userProfiles: List + val isDemoUser: Boolean + val isAnyProfileQuietModeEnabled: Boolean + + /** + * Creates a cache for users. + */ + fun enableAndResetCache() + + fun getSerialNumberForUser(user: UserHandle): Long + fun getUserForSerialNumber(serialNumber: Long): UserHandle? + fun getBadgedLabelForUser( + label: CharSequence, + user: UserHandle? + ): CharSequence + + fun isQuietModeEnabled(user: UserHandle): Boolean + fun isUserUnlocked(user: UserHandle): Boolean + fun requestQuietModeEnabled( + enableQuietMode: Boolean, + user: UserHandle? + ): Boolean +} diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..ade788383e02393b743c73d1e0177df128e040bf --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt @@ -0,0 +1,7 @@ +package foundation.e.blisslauncher.domain.repository + +import foundation.e.blisslauncher.domain.dto.WorkspaceModel + +interface WorkspaceRepository { + fun loadWorkspace(): WorkspaceModel +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..34390048921fc0f97f5e2cbadd03208cd783589b --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt @@ -0,0 +1,9 @@ +package foundation.e.blisslauncher.domain.repository + +import foundation.e.blisslauncher.domain.entity.WorkspaceScreen + +interface WorkspaceScreenRepository : Repository { + fun findAllOrderedByScreenRank(): List + + fun generateNewScreenId(): Long +} \ No newline at end of file diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt deleted file mode 100644 index 7c5a7b55e68f9443f2d8e80a4d187df12feeb94d..0000000000000000000000000000000000000000 --- a/domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package foundation.e.blisslauncher.domain - -import org.junit.Test - -import org.junit.Assert.assertEquals - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/mvicore/.gitignore b/mvicore/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/mvicore/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mvicore/build.gradle b/mvicore/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..fc229d6e4737f5c2d36b490a59811a4ef0ed3085 --- /dev/null +++ b/mvicore/build.gradle @@ -0,0 +1,14 @@ +import foundation.e.blisslauncher.buildsrc.Libs + +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation Libs.Kotlin.stdlib + + implementation Libs.RxJava.rxKotlin +} + +sourceCompatibility = "8" +targetCompatibility = "8" diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..0be5054088e4510fcb6de20a1332bccdf32f0a76 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt @@ -0,0 +1,68 @@ +package foundation.e.blisslauncher.mvicore.component + +import foundation.e.blisslauncher.mvicore.util.SameThreadVerifier +import io.reactivex.ObservableSource +import io.reactivex.Observer +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject + +open class BaseStore( + initialState: State, + private val intentToAction: IntentToAction, + private val actor: Actor, + private val reducer: Reducer, + private val newsPublisher: NewsPublisher? = null +) : Store, Disposable { + + private val threadVerifier = SameThreadVerifier() + private val actionSubject = PublishSubject.create() + private val stateSubject = BehaviorSubject.createDefault(initialState) + private val newsSubject = PublishSubject.create() + + private val disposable = CompositeDisposable() + + private val news: ObservableSource + get() = newsSubject + + override val state: State + get() = stateSubject.value!! + + init { + disposable += actionSubject.subscribe { invokeActor(state, it) } + } + + override fun accept(intent: Intent) { + val action = intentToAction(intent) + actionSubject.onNext(action) + } + + override fun subscribe(observer: Observer) { + stateSubject.subscribe(observer) + } + + override fun isDisposed(): Boolean = disposable.isDisposed + + override fun dispose() = disposable.dispose() + + private fun invokeActor(state: State, action: Action) { + if (isDisposed) return + + disposable += actor(state, action) + .subscribe { invokeReducer(stateSubject.value!!, action, it) } + } + + private fun invokeReducer(state: State, action: Action, effect: Effect) { + if (isDisposed) return + + threadVerifier.verify() + + val newState = reducer.invoke(state, effect) + stateSubject.onNext(newState) + newsPublisher?.invoke(action, effect, state)?.let { + newsSubject.onNext(it) + } + } +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt new file mode 100644 index 0000000000000000000000000000000000000000..05501fdc7330e9c9e6b7931e32276eb6a828e1ad --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.Observable + +interface MviView { + + val events: Observable + + fun render(state: State) +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e5ab5595d0b5535962795890503aae290374d89 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.ObservableSource +import io.reactivex.functions.Consumer + +/** + * Store manages the state of the application, similar to the Model + * and are bound to a particular domain. + */ +interface Store : Consumer, + ObservableSource { + val state: State +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt new file mode 100644 index 0000000000000000000000000000000000000000..5975227fd0aa7eca5408ca3dd0fa786a6070cef9 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt @@ -0,0 +1,24 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.Observable + +/** + * Function which maps Intent to Action. + */ +typealias IntentToAction = (intent: Intent) -> Action + +/** + * Actor function which takes current state, action and returns a stream of effects. + */ +typealias Actor = (State, Action) -> Observable + +/** + * Reducer function which takes current state, applies an effect to it and returns a new state. + */ +typealias Reducer = (state: State, effect: Effect) -> State + +/** + * Publisher used to publish Single Events (aka News) + */ +typealias NewsPublisher = + (action: Action, effect: Effect, state: State) -> News? diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..1e18521c2b03014214cd36d911ca20fe0cc676f6 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.mvicore.util + +class SameThreadVerifier { + + companion object { + var isEnabled: Boolean = true + } + + private val originalThread = Thread.currentThread().id + + fun verify() { + if (isEnabled && (Thread.currentThread().id != originalThread)) { + throw AssertionError("Not on same thread as previous verification") + } + } +} \ No newline at end of file diff --git a/quickstep/.gitignore b/quickstep/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/quickstep/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quickstep/build.gradle b/quickstep/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..f241c0b84024b61ac5d6fcf65ad5c359cfaf1864 --- /dev/null +++ b/quickstep/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.0" + + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +final String SUPPORT_LIBS_VERSION = '28.0.0' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}" + +} diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar new file mode 100644 index 0000000000000000000000000000000000000000..4ed224193b74f7cf4c785e95930b8ca44ad0e0e7 Binary files /dev/null and b/quickstep/libs/sysui_shared.jar differ diff --git a/quickstep/proguard-rules.pro b/quickstep/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..2f9dc5a47edc8241c18c1374b3f1fcf6396a829d --- /dev/null +++ b/quickstep/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java b/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..637b472d81b39ff6581ac5095341c9a75e14d36e --- /dev/null +++ b/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("foundation.e.blisslauncher.quickstep.test", appContext.getPackageName()); + } +} diff --git a/quickstep/src/main/AndroidManifest.xml b/quickstep/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff0a74d66e098bba8a013a22f61730fa1fd12e43 --- /dev/null +++ b/quickstep/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..e0ca4935fb07319f44abb39312b5b2d95e981a8d --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.BinderThread; +import android.support.annotation.UiThread; + +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; + +@TargetApi(Build.VERSION_CODES.P) +public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { + + private static final int SINGLE_FRAME_MS = 16; + + private final Handler mHandler; + private final boolean mStartAtFrontOfQueue; + private AnimationResult mAnimationResult; + + /** + * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the + * queue to minimize latency. + */ + public LauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue) { + mHandler = handler; + mStartAtFrontOfQueue = startAtFrontOfQueue; + } + + @BinderThread + @Override + public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) { + Runnable r = () -> { + finishExistingAnimation(); + mAnimationResult = new AnimationResult(runnable); + onCreateAnimation(targetCompats, mAnimationResult); + }; + if (mStartAtFrontOfQueue) { + postAtFrontOfQueueAsynchronously(mHandler, r); + } else { + postAsyncCallback(mHandler, r); + } + } + + /** + * Called on the UI thread when the animation targets are received. The implementation must + * call {@link AnimationResult#setAnimation(AnimatorSet)} with the target animation to be run. + */ + @UiThread + public abstract void onCreateAnimation( + RemoteAnimationTargetCompat[] targetCompats, AnimationResult result); + + @UiThread + private void finishExistingAnimation() { + if (mAnimationResult != null) { + mAnimationResult.finish(); + mAnimationResult = null; + } + } + + /** + * Called by the system + */ + @BinderThread + @Override + public void onAnimationCancelled() { + postAsyncCallback(mHandler, this::finishExistingAnimation); + } + + public static final class AnimationResult { + + private final Runnable mFinishRunnable; + + private AnimatorSet mAnimator; + private boolean mFinished = false; + private boolean mInitialized = false; + + private AnimationResult(Runnable finishRunnable) { + mFinishRunnable = finishRunnable; + } + + @UiThread + private void finish() { + if (!mFinished) { + mFinishRunnable.run(); + mFinished = true; + } + } + + @UiThread + public void setAnimation(AnimatorSet animation) { + if (mInitialized) { + throw new IllegalStateException("Animation already initialized"); + } + mInitialized = true; + mAnimator = animation; + if (mAnimator == null) { + finish(); + } else if (mFinished) { + // Animation callback was already finished, skip the animation. + mAnimator.start(); + mAnimator.end(); + } else { + // Start the animation + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finish(); + } + }); + mAnimator.start(); + + // Because t=0 has the app icon in its original spot, we can skip the + // first frame and have the same movement one frame earlier. + mAnimator.setCurrentPlayTime(SINGLE_FRAME_MS); + } + } + } + + /** + * Utility method to post a runnable on the handler, skipping the synchronization barriers. + */ + private void postAsyncCallback(Handler handler, Runnable callback) { + Message msg = Message.obtain(handler, callback); + msg.setAsynchronous(true); + handler.sendMessage(msg); + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..714c2966aba3265760d3e92927acfd8b35b7d08d --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java @@ -0,0 +1,816 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.shortcuts.DeepShortcutView; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS; +import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; +import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS; +import static com.android.quickstep.TaskUtils.findTaskViewToLaunch; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Manages the opening and closing app transitions from Launcher. + */ +@TargetApi(Build.VERSION_CODES.O) +@SuppressWarnings("unused") +public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManager + implements OnDeviceProfileChangeListener { + + private static final String TAG = "LauncherTransition"; + + /** Duration of status bar animations. */ + public static final int STATUS_BAR_TRANSITION_DURATION = 120; + + /** + * Since our animations decelerate heavily when finishing, we want to start status bar animations + * x ms before the ending. + */ + public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96; + + private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = + "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; + + private static final int APP_LAUNCH_DURATION = 500; + // Use a shorter duration for x or y translation to create a curve effect + private static final int APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2; + // We scale the durations for the downward app launch animations (minus the scale animation). + private static final float APP_LAUNCH_DOWN_DUR_SCALE_FACTOR = 0.8f; + private static final int APP_LAUNCH_ALPHA_START_DELAY = 32; + private static final int APP_LAUNCH_ALPHA_DURATION = 50; + + public static final int RECENTS_LAUNCH_DURATION = 336; + public static final int RECENTS_QUICKSCRUB_LAUNCH_DURATION = 300; + private static final int LAUNCHER_RESUME_START_DELAY = 100; + private static final int CLOSING_TRANSITION_DURATION_MS = 250; + + // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down. + public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f; + + private final Launcher mLauncher; + private final DragLayer mDragLayer; + private final AlphaProperty mDragLayerAlpha; + + private final Handler mHandler; + private final boolean mIsRtl; + + private final float mContentTransY; + private final float mWorkspaceTransY; + private final float mClosingWindowTransY; + + private DeviceProfile mDeviceProfile; + private View mFloatingView; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mLauncher.addForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.clearForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + }; + + public LauncherAppTransitionManagerImpl(Context context) { + mLauncher = Launcher.getLauncher(context); + mDragLayer = mLauncher.getDragLayer(); + mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS); + mHandler = new Handler(Looper.getMainLooper()); + mIsRtl = Utilities.isRtl(mLauncher.getResources()); + mDeviceProfile = mLauncher.getDeviceProfile(); + + Resources res = mLauncher.getResources(); + mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y); + mWorkspaceTransY = res.getDimensionPixelSize(R.dimen.workspace_trans_y); + mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); + + mLauncher.addOnDeviceProfileChangeListener(this); + registerRemoteAnimations(); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + mDeviceProfile = dp; + } + + /** + * @return ActivityOptions with remote animations that controls how the window of the opening + * targets are displayed. + */ + @Override + public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) { + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + AnimatorSet anim = new AnimatorSet(); + + boolean launcherClosing = + launcherIsATargetWithMode(targetCompats, MODE_CLOSING); + + if (!composeRecentsLaunchAnimator(v, targetCompats, anim)) { + // Set the state animation first so that any state listeners are called + // before our internal listeners. + mLauncher.getStateManager().setCurrentAnimation(anim); + + Rect windowTargetBounds = getWindowTargetBounds(targetCompats); + playIconAnimators(anim, v, windowTargetBounds); + if (launcherClosing) { + Pair launcherContentAnimator = + getLauncherContentAnimator(true /* isAppOpening */); + anim.play(launcherContentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + launcherContentAnimator.second.run(); + } + }); + } + anim.play(getOpeningWindowAnimators(v, targetCompats, windowTargetBounds)); + } + + if (launcherClosing) { + anim.addListener(mForceInvisibleListener); + } + + result.setAnimation(anim); + } + }; + + boolean fromRecents = mLauncher.getStateManager().getState().overviewUi + && findTaskViewToLaunch(launcher, v, null) != null; + int duration = fromRecents + ? RECENTS_LAUNCH_DURATION + : APP_LAUNCH_DURATION; + + int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION + - STATUS_BAR_TRANSITION_PRE_DELAY; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, duration, statusBarTransitionDelay)); + } + return super.getActivityLaunchOptions(launcher, v); + } + + /** + * Return the window bounds of the opening target. + * In multiwindow mode, we need to get the final size of the opening app window target to help + * figure out where the floating view should animate to. + */ + private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] targets) { + Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + if (mLauncher.isInMultiWindowModeCompat()) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + bounds.set(target.sourceContainerBounds); + bounds.offsetTo(target.position.x, target.position.y); + return bounds; + } + } + } + return bounds; + } + + public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider, + CancellationSignal cancellationSignal) { + mRemoteAnimationProvider = animationProvider; + cancellationSignal.setOnCancelListener(() -> { + if (animationProvider == mRemoteAnimationProvider) { + mRemoteAnimationProvider = null; + } + }); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private boolean composeRecentsLaunchAnimator(View v, + RemoteAnimationTargetCompat[] targets, AnimatorSet target) { + // Ensure recents is actually visible + if (!mLauncher.getStateManager().getState().overviewUi) { + return false; + } + + RecentsView recentsView = mLauncher.getOverviewPanel(); + boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING); + boolean skipLauncherChanges = !launcherClosing; + boolean isLaunchingFromQuickscrub = + recentsView.getQuickScrubController().isWaitingForTaskLaunch(); + + TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets); + if (taskView == null) { + return false; + } + + int duration = isLaunchingFromQuickscrub + ? RECENTS_QUICKSCRUB_LAUNCH_DURATION + : RECENTS_LAUNCH_DURATION; + + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper) + .setDuration(duration)); + + Animator childStateAnimation = null; + // Found a visible recents task that matches the opening app, lets launch the app from there + Animator launcherAnim; + final AnimatorListenerAdapter windowAnimEndListener; + if (launcherClosing) { + launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper); + launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + launcherAnim.setDuration(duration); + + // Make sure recents gets fixed up by resetting task alphas and scales, etc. + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().moveToRestState(); + mLauncher.getStateManager().reapplyState(); + } + }; + } else { + AnimatorPlaybackController controller = + mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL, duration); + controller.dispatchOnStart(); + childStateAnimation = controller.getTarget(); + launcherAnim = controller.getAnimationPlayer().setDuration(duration); + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().goToState(NORMAL, false); + } + }; + } + target.play(launcherAnim); + + // Set the current animation first, before adding windowAnimEndListener. Setting current + // animation adds some listeners which need to be called before windowAnimEndListener + // (the ordering of listeners matter in this case). + mLauncher.getStateManager().setCurrentAnimation(target, childStateAnimation); + target.addListener(windowAnimEndListener); + return true; + } + + /** + * Content is everything on screen except the background and the floating view (if any). + * + * @param isAppOpening True when this is called when an app is opening. + * False when this is called when an app is closing. + */ + private Pair getLauncherContentAnimator(boolean isAppOpening) { + AnimatorSet launcherAnimator = new AnimatorSet(); + Runnable endListener; + + float[] alphas = isAppOpening + ? new float[] {1, 0} + : new float[] {0, 1}; + float[] trans = isAppOpening + ? new float[] {0, mContentTransY} + : new float[] {-mContentTransY, 0}; + + if (mLauncher.isInState(ALL_APPS)) { + // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView. + final View appsView = mLauncher.getAppsView(); + final float startAlpha = appsView.getAlpha(); + final float startY = appsView.getTranslationY(); + appsView.setAlpha(alphas[0]); + appsView.setTranslationY(trans[0]); + + ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + appsView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + alpha.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + + launcherAnimator.play(alpha); + launcherAnimator.play(transY); + + endListener = () -> { + appsView.setAlpha(startAlpha); + appsView.setTranslationY(startY); + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + }; + } else if (mLauncher.isInState(OVERVIEW)) { + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + launcherAnimator.play(ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, + allAppsController.getProgress(), ALL_APPS_PROGRESS_OFF_SCREEN)); + + RecentsView overview = mLauncher.getOverviewPanel(); + ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, + RecentsView.CONTENT_ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + endListener = mLauncher.getStateManager()::reapplyState; + } else { + mDragLayerAlpha.setValue(alphas[0]); + ObjectAnimator alpha = + ObjectAnimator.ofFloat(mDragLayerAlpha, MultiValueAlpha.VALUE, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + mDragLayer.setTranslationY(trans[0]); + ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + mDragLayer.getScrim().hideSysUiScrim(true); + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + endListener = this::resetContentView; + } + return new Pair<>(launcherAnimator, endListener); + } + + /** + * Animators for the "floating view" of the view used to launch the target. + */ + private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds) { + final boolean isBubbleTextView = v instanceof BubbleTextView; + mFloatingView = new View(mLauncher); + if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) { + // Create a copy of the app icon + mFloatingView.setBackground( + DrawableFactory.get(mLauncher).newIcon((ItemInfoWithIcon) v.getTag())); + } + + // Position the floating view exactly on top of the original + Rect rect = new Rect(); + final boolean fromDeepShortcutView = v.getParent() instanceof DeepShortcutView; + if (fromDeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, rect); + } + int viewLocationLeft = rect.left; + int viewLocationTop = rect.top; + + float startScale = 1f; + if (isBubbleTextView && !fromDeepShortcutView) { + BubbleTextView btv = (BubbleTextView) v; + btv.getIconBounds(rect); + Drawable dr = btv.getIcon(); + if (dr instanceof FastBitmapDrawable) { + startScale = ((FastBitmapDrawable) dr).getAnimatedScale(); + } + } else { + rect.set(0, 0, rect.width(), rect.height()); + } + viewLocationLeft += rect.left; + viewLocationTop += rect.top; + int viewLocationStart = mIsRtl + ? windowTargetBounds.width() - rect.right + : viewLocationLeft; + LayoutParams lp = new LayoutParams(rect.width(), rect.height()); + lp.ignoreInsets = true; + lp.setMarginStart(viewLocationStart); + lp.topMargin = viewLocationTop; + mFloatingView.setLayoutParams(lp); + + // Set the properties here already to make sure they'are available when running the first + // animation frame. + mFloatingView.setLeft(viewLocationLeft); + mFloatingView.setTop(viewLocationTop); + mFloatingView.setRight(viewLocationLeft + rect.width()); + mFloatingView.setBottom(viewLocationTop + rect.height()); + + // Swap the two views in place. + ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView); + v.setVisibility(View.INVISIBLE); + + int[] dragLayerBounds = new int[2]; + mDragLayer.getLocationOnScreen(dragLayerBounds); + + // Animate the app icon to the center of the window bounds in screen coordinates. + float centerX = windowTargetBounds.centerX() - dragLayerBounds[0]; + float centerY = windowTargetBounds.centerY() - dragLayerBounds[1]; + + float xPosition = mIsRtl + ? windowTargetBounds.width() - lp.getMarginStart() - rect.width() + : lp.getMarginStart(); + float dX = centerX - xPosition - (lp.width / 2); + float dY = centerY - lp.topMargin - (lp.height / 2); + + ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX); + ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY); + + // Use upward animation for apps that are either on the bottom half of the screen, or are + // relatively close to the center. + boolean useUpwardAnimation = lp.topMargin > centerY + || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx; + if (useUpwardAnimation) { + x.setDuration(APP_LAUNCH_CURVED_DURATION); + y.setDuration(APP_LAUNCH_DURATION); + } else { + x.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_DURATION)); + y.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_CURVED_DURATION)); + } + x.setInterpolator(AGGRESSIVE_EASE); + y.setInterpolator(AGGRESSIVE_EASE); + appOpenAnimator.play(x); + appOpenAnimator.play(y); + + // Scale the app icon to take up the entire screen. This simplifies the math when + // animating the app window position / scale. + float maxScaleX = windowTargetBounds.width() / (float) rect.width(); + float maxScaleY = windowTargetBounds.height() / (float) rect.height(); + float scale = Math.max(maxScaleX, maxScaleY); + ObjectAnimator scaleAnim = ObjectAnimator + .ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale); + scaleAnim.setDuration(APP_LAUNCH_DURATION) + .setInterpolator(Interpolators.EXAGGERATED_EASE); + appOpenAnimator.play(scaleAnim); + + // Fade out the app icon. + ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f); + if (useUpwardAnimation) { + alpha.setStartDelay(APP_LAUNCH_ALPHA_START_DELAY); + alpha.setDuration(APP_LAUNCH_ALPHA_DURATION); + } else { + alpha.setStartDelay((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR + * APP_LAUNCH_ALPHA_START_DELAY)); + alpha.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_ALPHA_DURATION)); + } + alpha.setInterpolator(LINEAR); + appOpenAnimator.play(alpha); + + appOpenAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Reset launcher to normal state + v.setVisibility(View.VISIBLE); + ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView); + } + }); + } + + /** + * @return Animator that controls the window of the opening targets. + */ + private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets, + Rect windowTargetBounds) { + Rect bounds = new Rect(); + if (v.getParent() instanceof DeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), bounds); + } else if (v instanceof BubbleTextView) { + ((BubbleTextView) v).getIconBounds(bounds); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, bounds); + } + int[] floatingViewBounds = new int[2]; + + Rect crop = new Rect(); + Matrix matrix = new Matrix(); + + RemoteAnimationTargetSet openingTargets = new RemoteAnimationTargetSet(targets, + MODE_OPENING); + RemoteAnimationTargetSet closingTargets = new RemoteAnimationTargetSet(targets, + MODE_CLOSING); + SyncRtSurfaceTransactionApplier surfaceApplier = new SyncRtSurfaceTransactionApplier( + mFloatingView); + + ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setDuration(APP_LAUNCH_DURATION); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + // Fade alpha for the app window. + FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR); + + @Override + public void onUpdate(float percent) { + final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent); + + // Calculate app icon size. + float iconWidth = bounds.width() * mFloatingView.getScaleX(); + float iconHeight = bounds.height() * mFloatingView.getScaleY(); + + // Scale the app window to match the icon size. + float scaleX = iconWidth / windowTargetBounds.width(); + float scaleY = iconHeight / windowTargetBounds.height(); + float scale = Math.min(1f, Math.min(scaleX, scaleY)); + + // Position the scaled window on top of the icon + int windowWidth = windowTargetBounds.width(); + int windowHeight = windowTargetBounds.height(); + float scaledWindowWidth = windowWidth * scale; + float scaledWindowHeight = windowHeight * scale; + + float offsetX = (scaledWindowWidth - iconWidth) / 2; + float offsetY = (scaledWindowHeight - iconHeight) / 2; + mFloatingView.getLocationOnScreen(floatingViewBounds); + + float transX0 = floatingViewBounds[0] - offsetX; + float transY0 = floatingViewBounds[1] - offsetY; + + // Animate the window crop so that it starts off as a square, and then reveals + // horizontally. + float cropHeight = windowHeight * easePercent + windowWidth * (1 - easePercent); + float initialTop = (windowHeight - windowWidth) / 2f; + crop.left = 0; + crop.top = (int) (initialTop * (1 - easePercent)); + crop.right = windowWidth; + crop.bottom = (int) (crop.top + cropHeight); + + SurfaceParams[] params = new SurfaceParams[targets.length]; + for (int i = targets.length - 1; i >= 0; i--) { + RemoteAnimationTargetCompat target = targets[i]; + + Rect targetCrop; + float alpha; + if (target.mode == MODE_OPENING) { + matrix.setScale(scale, scale); + matrix.postTranslate(transX0, transY0); + targetCrop = crop; + alpha = mAlpha.value; + } else { + matrix.setTranslate(target.position.x, target.position.y); + alpha = 1f; + targetCrop = target.sourceContainerBounds; + } + + params[i] = new SurfaceParams(target.leash, alpha, matrix, targetCrop, + RemoteAnimationProvider.getLayer(target, MODE_OPENING)); + } + surfaceApplier.scheduleApply(params); + } + }); + return appAnimator; + } + + /** + * Registers remote animations used when closing apps to home screen. + */ + private void registerRemoteAnimations() { + // Unregister this + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); + definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, + WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, + new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(), + CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); + + // TODO: Transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER + new ActivityCompat(mLauncher).registerRemoteAnimations(definition); + } + } + + private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) { + return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode); + } + + /** + * @return Runner that plays when user goes to Launcher + * ie. pressing home, swiping up from nav bar. + */ + private RemoteAnimationRunnerCompat getWallpaperOpenRunner() { + return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) { + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + if (!mLauncher.hasBeenResumed()) { + // If launcher is not resumed, wait until new async-frame after resume + mLauncher.setOnResumeCallback(() -> + postAsyncCallback(mHandler, () -> + onCreateAnimation(targetCompats, result))); + return; + } + + if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) { + mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS); + mLauncher.getStateManager().moveToRestState(); + } + + AnimatorSet anim = null; + RemoteAnimationProvider provider = mRemoteAnimationProvider; + if (provider != null) { + anim = provider.createWindowAnimation(targetCompats); + } + + if (anim == null) { + anim = new AnimatorSet(); + anim.play(getClosingWindowAnimators(targetCompats)); + + // Normally, we run the launcher content animation when we are transitioning + // home, but if home is already visible, then we don't want to animate the + // contents of launcher unless we know that we are animating home as a result + // of the home button press with quickstep, which will result in launcher being + // started on touch down, prior to the animation home (and won't be in the + // targets list because it is already visible). In that case, we force + // invisibility on touch down, and only reset it after the animation to home + // is initialized. + if (launcherIsATargetWithMode(targetCompats, MODE_OPENING) + || mLauncher.isForceInvisible()) { + // Only register the content animation for cancellation when state changes + mLauncher.getStateManager().setCurrentAnimation(anim); + createLauncherResumeAnimation(anim); + } + } + + mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); + result.setAnimation(anim); + } + }; + } + + /** + * Animator that controls the transformations of the windows the targets that are closing. + */ + private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) { + SyncRtSurfaceTransactionApplier surfaceApplier = + new SyncRtSurfaceTransactionApplier(mDragLayer); + Matrix matrix = new Matrix(); + ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); + int duration = CLOSING_TRANSITION_DURATION_MS; + closingAnimator.setDuration(duration); + closingAnimator.addUpdateListener(new MultiValueUpdateListener() { + FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DEACCEL_1_7); + FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DEACCEL_1_7); + FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR); + + @Override + public void onUpdate(float percent) { + SurfaceParams[] params = new SurfaceParams[targets.length]; + for (int i = targets.length - 1; i >= 0; i--) { + RemoteAnimationTargetCompat target = targets[i]; + float alpha; + if (target.mode == MODE_CLOSING) { + matrix.setScale(mScale.value, mScale.value, + target.sourceContainerBounds.centerX(), + target.sourceContainerBounds.centerY()); + matrix.postTranslate(0, mDy.value); + matrix.postTranslate(target.position.x, target.position.y); + alpha = mAlpha.value; + } else { + matrix.setTranslate(target.position.x, target.position.y); + alpha = 1f; + } + params[i] = new SurfaceParams(target.leash, alpha, matrix, + target.sourceContainerBounds, + RemoteAnimationProvider.getLayer(target, MODE_CLOSING)); + } + surfaceApplier.scheduleApply(params); + } + }); + + return closingAnimator; + } + + /** + * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}. + */ + private void createLauncherResumeAnimation(AnimatorSet anim) { + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + Pair contentAnimator = + getLauncherContentAnimator(false /* isAppOpening */); + contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY); + anim.play(contentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + contentAnimator.second.run(); + } + }); + } else { + AnimatorSet workspaceAnimator = new AnimatorSet(); + + mDragLayer.setTranslationY(-mWorkspaceTransY);; + workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, + -mWorkspaceTransY, 0)); + + mDragLayerAlpha.setValue(0); + workspaceAnimator.play(ObjectAnimator.ofFloat( + mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f)); + + workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); + workspaceAnimator.setDuration(333); + workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7); + + mDragLayer.getScrim().hideSysUiScrim(true); + + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + workspaceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetContentView(); + } + }); + anim.play(workspaceAnimator); + } + } + + private void resetContentView() { + mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd(); + mDragLayerAlpha.setValue(1f); + mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null); + mDragLayer.setTranslationY(0f); + mDragLayer.getScrim().hideSysUiScrim(false); + } + + private boolean hasControlRemoteAppTransitionPermission() { + return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java new file mode 100644 index 0000000000000000000000000000000000000000..4bbc872a7292cd284d85d1963d7aa753bf1a6183 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; + +import com.android.launcher3.states.InternalStateHandler; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.util.function.BiPredicate; + +@TargetApi(Build.VERSION_CODES.P) +public class LauncherInitListener extends InternalStateHandler implements ActivityInitListener { + + private final BiPredicate mOnInitListener; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + public LauncherInitListener(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + protected boolean init(Launcher launcher, boolean alreadyOnHome) { + if (mRemoteAnimationProvider != null) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager(); + + // Set a one-time animation provider. After the first call, this will get cleared. + // TODO: Probably also check the intended target id. + CancellationSignal cancellationSignal = new CancellationSignal(); + appTransitionManager.setRemoteAnimationProvider((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + RemoteAnimationProvider provider = mRemoteAnimationProvider; + mRemoteAnimationProvider = null; + + if (provider != null && launcher.getStateManager().getState().overviewUi) { + return provider.createWindowAnimation(targets); + } + return null; + }, cancellationSignal); + } + OverviewCallbacks.get(launcher).onInitOverviewTransition(); + return mOnInitListener.test(launcher, alreadyOnHome); + } + + @Override + public void register() { + initWhenReady(); + } + + @Override + public void unregister() { + mRemoteAnimationProvider = null; + clearReference(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + mRemoteAnimationProvider = animProvider; + + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(addToIntent(new Intent((intent))), options); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..21eaf159f092030e71aed986f4880bbfadb44d6e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java @@ -0,0 +1,615 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherInitListener; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.uioverrides.FastOverviewState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.LauncherLayoutListener; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +import static android.view.View.TRANSLATION_Y; +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; +import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION; + +/** + * Utility class which abstracts out the logical differences between Launcher and RecentsActivity. + */ +@TargetApi(Build.VERSION_CODES.P) +public interface ActivityControlHelper { + + LayoutListener createLayoutListener(T activity); + + /** + * Updates the UI to indicate quick interaction. + */ + void onQuickInteractionStart(T activity, @Nullable ActivityManager.RunningTaskInfo taskInfo, + boolean activityVisible); + + float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context); + + void executeOnWindowAvailable(T activity, Runnable action); + + void onTransitionCancelled(T activity, boolean activityVisible); + + int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect); + + void onSwipeUpComplete(T activity); + + AnimationFactory prepareRecentsUI(T activity, boolean activityVisible, + Consumer callback); + + ActivityInitListener createActivityInitListener(BiPredicate onInitListener); + + @Nullable + T getCreatedActivity(); + + @UiThread + @Nullable + RecentsView getVisibleRecentsView(); + + @UiThread + boolean switchToRecentsIfVisible(boolean fromRecentsButton); + + Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target); + + boolean shouldMinimizeSplitScreen(); + + /** + * @return {@code true} if recents activity should be started immediately on touchDown, + * {@code false} if it should deferred until some threshold is crossed. + */ + boolean deferStartingActivity(int downHitTarget); + + boolean supportsLongSwipe(T activity); + + AlphaProperty getAlphaProperty(T activity); + + /** + * Must return a non-null controller is supportsLongSwipe was true. + */ + LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet); + + /** + * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher} + */ + int getContainerType(); + + class LauncherActivityControllerHelper implements ActivityControlHelper { + + @Override + public LayoutListener createLayoutListener(Launcher activity) { + return new LauncherLayoutListener(activity); + } + + @Override + public void onQuickInteractionStart(Launcher activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + LauncherState fromState = activity.getStateManager().getState(); + activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible); + + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + controller.onQuickScrubStart(activityVisible && !fromState.overviewUi, this); + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + // The padding calculations are exactly same as that of RecentsView.setInsets + int topMargin = context.getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + int paddingTop = targetRect.rect.top - topMargin - dp.getInsets().top; + int paddingBottom = dp.availableHeightPx + dp.getInsets().top - targetRect.rect.bottom; + + return FastOverviewState.OVERVIEW_TRANSLATION_FACTOR * (paddingBottom - paddingTop); + } + + @Override + public void executeOnWindowAvailable(Launcher activity, Runnable action) { + activity.getWorkspace().runOnOverlayHidden(action); + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateLauncherTaskSize(context, dp, outRect.rect); + if (interactionType == INTERACTION_QUICK_SCRUB) { + outRect.scale = FastOverviewState.getOverviewScale(dp, outRect.rect, context); + } + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + hotseatInset; + } else { + int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; + // Track slightly below the top of the shelf (between top and content). + return shelfHeight - dp.edgeMarginPx * 2; + } + } + + @Override + public void onTransitionCancelled(Launcher activity, boolean activityVisible) { + LauncherState startState = activity.getStateManager().getRestState(); + activity.getStateManager().goToState(startState, activityVisible); + } + + @Override + public void onSwipeUpComplete(Launcher activity) { + // Re apply state in case we did something funky during the transition. + activity.getStateManager().reapplyState(); + DiscoveryBounce.showForOverviewIfNeeded(activity); + } + + @Override + public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible, + Consumer callback) { + final LauncherState startState = activity.getStateManager().getState(); + + LauncherState resetState = startState; + if (startState.disableRestore) { + resetState = activity.getStateManager().getRestState(); + } + activity.getStateManager().setRestState(resetState); + + if (!activityVisible) { + // Since the launcher is not visible, we can safely reset the scroll position. + // This ensures then the next swipe up to all-apps starts from scroll 0. + activity.getAppsView().reset(false /* animate */); + activity.getStateManager().goToState(OVERVIEW, false); + + // Optimization, hide the all apps view to prevent layout while initializing + activity.getAppsView().getContentView().setVisibility(View.GONE); + } + + return new AnimationFactory() { + @Override + public void createActivityController(long transitionLength, + @InteractionType int interactionType) { + createActivityControllerInternal(activity, activityVisible, startState, + transitionLength, interactionType, callback); + } + + @Override + public void onTransitionCancelled() { + activity.getStateManager().goToState(startState, false /* animate */); + } + }; + } + + private void createActivityControllerInternal(Launcher activity, boolean wasVisible, + LauncherState startState, long transitionLength, + @InteractionType int interactionType, + Consumer callback) { + LauncherState endState = interactionType == INTERACTION_QUICK_SCRUB + ? FAST_OVERVIEW : OVERVIEW; + if (wasVisible) { + DeviceProfile dp = activity.getDeviceProfile(); + long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); + callback.accept(activity.getStateManager() + .createAnimationToNewWorkspace(startState, endState, accuracy)); + return; + } + + AnimatorSet anim = new AnimatorSet(); + + if (!activity.getDeviceProfile().isVerticalBarLayout()) { + AllAppsTransitionController controller = activity.getAllAppsController(); + float scrollRange = Math.max(controller.getShiftRange(), 1); + float progressDelta = (transitionLength / scrollRange); + + float endProgress = endState.getVerticalProgress(activity); + float startProgress = endProgress + progressDelta; + ObjectAnimator shiftAnim = ObjectAnimator.ofFloat( + controller, ALL_APPS_PROGRESS, startProgress, endProgress); + shiftAnim.setInterpolator(LINEAR); + anim.play(shiftAnim); + + // Since we are changing the start position of the UI, reapply the state, at the end + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + activity.getStateManager().reapplyState(); + } + }); + } + + if (interactionType == INTERACTION_NORMAL) { + playScaleDownAnim(anim, activity); + } + + anim.setDuration(transitionLength * 2); + activity.getStateManager().setCurrentAnimation(anim); + callback.accept(AnimatorPlaybackController.wrap(anim, transitionLength * 2)); + } + + /** + * Scale down recents from the center task being full screen to being in overview. + */ + private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage()); + if (v == null) { + return; + } + ClipAnimationHelper clipHelper = new ClipAnimationHelper(); + clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null); + if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) { + float fromScale = clipHelper.getSourceRect().width() + / clipHelper.getTargetRect().width(); + float fromTranslationY = clipHelper.getSourceRect().centerY() + - clipHelper.getTargetRect().centerY(); + Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, fromScale, 1); + Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, + fromTranslationY, 0); + scale.setInterpolator(LINEAR); + translateY.setInterpolator(LINEAR); + anim.playTogether(scale, translateY); + } + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new LauncherInitListener(onInitListener); + } + + @Nullable + @Override + public Launcher getCreatedActivity() { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + return null; + } + return (Launcher) app.getModel().getCallback(); + } + + @Nullable + @UiThread + private Launcher getVisibleLaucher() { + Launcher launcher = getCreatedActivity(); + return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ? + launcher : null; + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + Launcher launcher = getVisibleLaucher(); + return launcher != null && launcher.getStateManager().getState().overviewUi + ? launcher.getOverviewPanel() : null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + Launcher launcher = getVisibleLaucher(); + if (launcher != null) { + if (fromRecentsButton) { + launcher.getUserEventDispatcher().logActionCommand( + LauncherLogProto.Action.Command.RECENTS_BUTTON, + getContainerType(), + LauncherLogProto.ContainerType.TASKSWITCHER); + } + launcher.getStateManager().goToState(OVERVIEW); + return true; + } + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + return downHitTarget == HIT_TARGET_BACK || downHitTarget == HIT_TARGET_ROTATION; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + return homeBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + return true; + } + + @Override + public boolean supportsLongSwipe(Launcher activity) { + return !activity.getDeviceProfile().isVerticalBarLayout(); + } + + @Override + public LongSwipeHelper getLongSwipeController(Launcher activity, + RemoteAnimationTargetSet targetSet) { + if (activity.getDeviceProfile().isVerticalBarLayout()) { + return null; + } + return new LongSwipeHelper(activity, targetSet); + } + + @Override + public AlphaProperty getAlphaProperty(Launcher activity) { + return activity.getDragLayer().getAlphaProperty(DragLayer.ALPHA_INDEX_SWIPE_UP); + } + + @Override + public int getContainerType() { + final Launcher launcher = getVisibleLaucher(); + return launcher != null ? launcher.getStateManager().getState().containerType + : LauncherLogProto.ContainerType.APP; + } + } + + class FallbackActivityControllerHelper implements ActivityControlHelper { + + private final ComponentName mHomeComponent; + private final Handler mUiHandler = new Handler(Looper.getMainLooper()); + + public FallbackActivityControllerHelper(ComponentName homeComponent) { + mHomeComponent = homeComponent; + } + + @Override + public void onQuickInteractionStart(RecentsActivity activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + + // TODO: match user is as well + boolean startingFromHome = !activityVisible && + (taskInfo == null || Objects.equals(taskInfo.topActivity, mHomeComponent)); + controller.onQuickScrubStart(startingFromHome, this); + if (activityVisible) { + mUiHandler.postDelayed(controller::onFinishedTransitionToQuickScrub, + OVERVIEW_TRANSITION_MS); + } + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + return 0; + } + + @Override + public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) { + action.run(); + } + + @Override + public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) { + // TODO: + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateFallbackTaskSize(context, dp, outRect.rect); + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + hotseatInset; + } else { + return dp.heightPx - outRect.rect.bottom; + } + } + + @Override + public void onSwipeUpComplete(RecentsActivity activity) { + // TODO: + } + + @Override + public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible, + Consumer callback) { + if (activityVisible) { + return (transitionLength, interactionType) -> { }; + } + + RecentsView rv = activity.getOverviewPanel(); + rv.setContentAlpha(0); + + return new AnimationFactory() { + + boolean isAnimatingHome = false; + + @Override + public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { + isAnimatingHome = targets != null && targets.isAnimatingHome(); + if (!isAnimatingHome) { + rv.setContentAlpha(1); + } + createActivityController(getSwipeUpDestinationAndLength( + activity.getDeviceProfile(), activity, INTERACTION_NORMAL, + new TransformedRect()), INTERACTION_NORMAL); + } + + @Override + public void createActivityController(long transitionLength, int interactionType) { + if (!isAnimatingHome) { + return; + } + + ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1); + anim.setDuration(transitionLength).setInterpolator(LINEAR); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(anim); + callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength)); + } + }; + } + + @Override + public LayoutListener createLayoutListener(RecentsActivity activity) { + // We do not change anything as part of layout changes in fallback activity. Return a + // default layout listener. + return new LayoutListener() { + @Override + public void open() { } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { } + + @Override + public void finish() { } + }; + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new RecentsActivityTracker(onInitListener); + } + + @Nullable + @Override + public RecentsActivity getCreatedActivity() { + return RecentsActivityTracker.getCurrentActivity(); + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + RecentsActivity activity = getCreatedActivity(); + if (activity != null && activity.hasWindowFocus()) { + return activity.getOverviewPanel(); + } + return null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + // Always defer starting the activity when using fallback + return true; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + // TODO: Remove this once b/77875376 is fixed + return target.sourceContainerBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + // TODO: Remove this once b/77875376 is fixed + return false; + } + + @Override + public boolean supportsLongSwipe(RecentsActivity activity) { + return false; + } + + @Override + public LongSwipeHelper getLongSwipeController(RecentsActivity activity, + RemoteAnimationTargetSet targetSet) { + return null; + } + + @Override + public AlphaProperty getAlphaProperty(RecentsActivity activity) { + return activity.getDragLayer().getAlphaProperty(0); + } + + @Override + public int getContainerType() { + return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER; + } + } + + interface LayoutListener { + + void open(); + + void setHandler(WindowTransformSwipeHandler handler); + + void finish(); + } + + interface ActivityInitListener { + + void register(); + + void unregister(); + + void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration); + } + + interface AnimationFactory { + + default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { } + + void createActivityController(long transitionLength, @InteractionType int interactionType); + + default void onTransitionCancelled() { } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java new file mode 100644 index 0000000000000000000000000000000000000000..6604dabd1246a66f594e704cb9a96c4dd62ab80f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.FloatProperty; + +/** + * A mutable float which allows animating the value + */ +public class AnimatedFloat { + + public static FloatProperty VALUE = new FloatProperty("value") { + @Override + public void setValue(AnimatedFloat obj, float v) { + obj.updateValue(v); + } + + @Override + public Float get(AnimatedFloat obj) { + return obj.value; + } + }; + + private final Runnable mUpdateCallback; + private ObjectAnimator mValueAnimator; + + public float value; + + public AnimatedFloat(Runnable updateCallback) { + mUpdateCallback = updateCallback; + } + + public ObjectAnimator animateToValue(float start, float end) { + cancelAnimation(); + mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end); + mValueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (mValueAnimator == animator) { + mValueAnimator = null; + } + } + }); + return mValueAnimator; + } + + /** + * Changes the value and calls the callback. + * Note that the value can be directly accessed as well to avoid notifying the callback. + */ + public void updateValue(float v) { + if (Float.compare(v, value) != 0) { + value = v; + mUpdateCallback.run(); + } + } + + public void cancelAnimation() { + if (mValueAnimator != null) { + mValueAnimator.cancel(); + } + } + + public void finishAnimation() { + if (mValueAnimator != null && mValueAnimator.isRunning()) { + mValueAnimator.end(); + } + } + + public ObjectAnimator getCurrentAnimation() { + return mValueAnimator; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..4aac3e10c854d41ff7c4dbfd6aa61dad55e76e30 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; + +/** + * A TouchConsumer which defers all events on the UIThread until the consumer is created. + */ +@TargetApi(Build.VERSION_CODES.P) +public class DeferredTouchConsumer implements TouchConsumer { + + private final VelocityTracker mVelocityTracker; + private final DeferredTouchProvider mTouchProvider; + + private MotionEventQueue mMyQueue; + private TouchConsumer mTarget; + + public DeferredTouchConsumer(DeferredTouchProvider touchProvider) { + mVelocityTracker = VelocityTracker.obtain(); + mTouchProvider = touchProvider; + } + + @Override + public void accept(MotionEvent event) { + mTarget.accept(event); + } + + @Override + public void reset() { + mTarget.reset(); + } + + @Override + public void updateTouchTracking(int interactionType) { + mTarget.updateTouchTracking(interactionType); + } + + @Override + public void onQuickScrubEnd() { + mTarget.onQuickScrubEnd(); + } + + @Override + public void onQuickScrubProgress(float progress) { + mTarget.onQuickScrubProgress(progress); + } + + @Override + public void onQuickStep(MotionEvent ev) { + mTarget.onQuickStep(ev); + } + + @Override + public void onCommand(int command) { + mTarget.onCommand(command); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mMyQueue = queue; + return null; + } + + @Override + public void deferInit() { + mTarget = mTouchProvider.createTouchConsumer(mVelocityTracker); + mTarget.getIntrimChoreographer(mMyQueue); + } + + @Override + public boolean forceToLauncherConsumer() { + return mTarget.forceToLauncherConsumer(); + } + + @Override + public boolean deferNextEventToMainThread() { + // If our target is still null, defer the next target as well + TouchConsumer target = mTarget; + return target == null ? true : target.deferNextEventToMainThread(); + } + + @Override + public void onShowOverviewFromAltTab() { + mTarget.onShowOverviewFromAltTab(); + } + + public interface DeferredTouchProvider { + + TouchConsumer createTouchConsumer(VelocityTracker tracker); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..02afb35c1759a44204236c6228b118b97dd3ff45 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.InstantAppInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.InstantAppResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of InstantAppResolver using platform APIs + */ +@SuppressWarnings("unused") +public class InstantAppResolverImpl extends InstantAppResolver { + + private static final String TAG = "InstantAppResolverImpl"; + public static final String COMPONENT_CLASS_MARKER = "@instantapp"; + + private final PackageManager mPM; + + public InstantAppResolverImpl(Context context) + throws NoSuchMethodException, ClassNotFoundException { + mPM = context.getPackageManager(); + } + + @Override + public boolean isInstantApp(ApplicationInfo info) { + return info.isInstantApp(); + } + + @Override + public boolean isInstantApp(AppInfo info) { + ComponentName cn = info.getTargetComponent(); + return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER); + } + + @Override + public List getInstantApps() { + try { + List result = new ArrayList<>(); + for (InstantAppInfo iai : mPM.getInstantApps()) { + ApplicationInfo info = iai.getApplicationInfo(); + if (info != null) { + result.add(info); + } + } + return result; + } catch (SecurityException se) { + Log.w(TAG, "getInstantApps failed. Launcher may not be the default home app.", se); + } catch (Exception e) { + Log.e(TAG, "Error calling API: getInstantApps", e); + } + return super.getInstantApps(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..dcf32c75a7d32d2f4c88d45f16d688b08a08dcb3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Build; +import android.provider.SearchIndexablesContract.XmlResource; +import android.provider.SearchIndexablesProvider; +import android.util.Xml; + +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconShapeOverride; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; +import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; + +@TargetApi(Build.VERSION_CODES.O) +public class LauncherSearchIndexablesProvider extends SearchIndexablesProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryXmlResources(String[] strings) { + MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); + ResolveInfo settingsActivity = getContext().getPackageManager().resolveActivity( + new Intent(Intent.ACTION_APPLICATION_PREFERENCES) + .setPackage(getContext().getPackageName()), 0); + cursor.newRow() + .add(XmlResource.COLUMN_XML_RESID, R.xml.indexable_launcher_prefs) + .add(XmlResource.COLUMN_INTENT_ACTION, Intent.ACTION_APPLICATION_PREFERENCES) + .add(XmlResource.COLUMN_INTENT_TARGET_PACKAGE, getContext().getPackageName()) + .add(XmlResource.COLUMN_INTENT_TARGET_CLASS, settingsActivity.activityInfo.name); + return cursor; + } + + @Override + public Cursor queryRawData(String[] projection) { + return new MatrixCursor(INDEXABLES_RAW_COLUMNS); + } + + @Override + public Cursor queryNonIndexableKeys(String[] projection) { + MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); + if (!getContext().getSystemService(LauncherApps.class).hasShortcutHostPermission()) { + // We are not the current launcher. Hide all preferences + try (XmlResourceParser parser = getContext().getResources() + .getXml(R.xml.indexable_launcher_prefs)) { + final int depth = parser.getDepth(); + final int[] attrs = new int[] { android.R.attr.key }; + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.START_TAG) { + TypedArray a = getContext().obtainStyledAttributes( + Xml.asAttributeSet(parser), attrs); + cursor.addRow(new String[] {a.getString(0)}); + a.recycle(); + } + } + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException(e); + } + } else if (!IconShapeOverride.isSupported(getContext())) { + cursor.addRow(new String[] {IconShapeOverride.KEY_PREFERENCE}); + } + return cursor; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..f289f2eff4608fac5dbe3a429a157e917257816e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.Interpolators.OvershootParams; +import com.android.launcher3.uioverrides.PortraitStatesTouchController; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION; +import static com.android.quickstep.WindowTransformSwipeHandler.MIN_OVERSHOOT_DURATION; + +/** + * Utility class to handle long swipe from an app. + * This assumes the presence of Launcher activity as long swipe is not supported on the + * fallback activity. + */ +public class LongSwipeHelper { + + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS)); + + private final Launcher mLauncher; + private final RemoteAnimationTargetSet mTargetSet; + + private float mMaxSwipeDistance = 1; + private AnimatorPlaybackController mAnimator; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) { + mLauncher = launcher; + mTargetSet = targetSet; + init(); + } + + private void init() { + mFlingBlockCheck.blockFling(); + + // Init animations + AllAppsTransitionController controller = mLauncher.getAllAppsController(); + // TODO: Scale it down so that we can reach all-apps in screen space + mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange()); + + AnimatorSetBuilder builder = PortraitStatesTouchController.getOverviewToAllAppsAnimation(); + mAnimator = mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, builder, + Math.round(2 * mMaxSwipeDistance), null, LauncherStateManager.ANIM_ALL); + mAnimator.dispatchOnStart(); + } + + public void onMove(float displacement) { + mAnimator.setPlayFraction(displacement / mMaxSwipeDistance); + mFlingBlockCheck.onEvent(); + } + + public void destroy() { + // TODO: We can probably also show the task view + + mLauncher.getStateManager().goToState(OVERVIEW, false); + } + + public void end(float velocity, boolean isFling, Runnable callback) { + float velocityPxPerMs = velocity / 1000; + long duration = MAX_SWIPE_DURATION; + Interpolator interpolator = DEACCEL; + + final float currentFraction = mAnimator.getProgressFraction(); + final boolean toAllApps; + float endProgress; + + boolean blockedFling = isFling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + isFling = false; + } + + if (!isFling) { + toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS; + endProgress = toAllApps ? 1 : 0; + + long expectedDuration = Math.abs(Math.round((endProgress - currentFraction) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + + if (blockedFling && !toAllApps) { + Interpolators.OvershootParams overshoot = new OvershootParams(currentFraction, + currentFraction, endProgress, velocityPxPerMs, (int) mMaxSwipeDistance); + duration = (overshoot.duration + duration); + duration = Utilities.boundToRange(duration, MIN_OVERSHOOT_DURATION, + MAX_SWIPE_DURATION); + interpolator = overshoot.interpolator; + endProgress = overshoot.end; + } + } else { + toAllApps = velocity < 0; + endProgress = toAllApps ? 1 : 0; + + float minFlingVelocity = mLauncher.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) { + float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance; + + // we want the page's snap velocity to approximately match the velocity at + // which the user flings, so we scale the duration by a value near to the + // derivative of the scroll interpolator at zero, ie. 2. + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + + final boolean finalIsFling = isFling; + mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, finalIsFling, callback)); + + ValueAnimator animator = mAnimator.getAnimationPlayer(); + animator.setDuration(duration).setInterpolator(interpolator); + animator.setFloatValues(currentFraction, endProgress); + animator.start(); + } + + private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) { + mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false); + if (!toAllApps) { + DiscoveryBounce.showForOverviewIfNeeded(mLauncher); + mLauncher.getOverviewPanel().setSwipeDownShouldLaunchApp(true); + } + + mLauncher.getUserEventDispatcher().logStateChangeAction( + isFling ? Touch.FLING : Touch.SWIPE, Direction.UP, + ContainerType.NAVBAR, ContainerType.APP, + toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER, + 0); + + callback.run(); + } + + public float getTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) { + if (!(app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { + return 0; + } + return expectedAlpha; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..84e43d93bd7e408161053f6b686f849af8d6778f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.util.Log; +import android.view.Choreographer; +import android.view.MotionEvent; + +import com.android.systemui.shared.system.ChoreographerCompat; + +import java.util.ArrayList; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_MASK; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; + +/** + * Helper class for batching input events + */ +@TargetApi(Build.VERSION_CODES.O) +public class MotionEventQueue { + + private static final String TAG = "MotionEventQueue"; + + private static final int ACTION_VIRTUAL = ACTION_MASK - 1; + + private static final int ACTION_QUICK_SCRUB_START = + ACTION_VIRTUAL | (1 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_PROGRESS = + ACTION_VIRTUAL | (2 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_END = + ACTION_VIRTUAL | (3 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_RESET = + ACTION_VIRTUAL | (4 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_DEFER_INIT = + ACTION_VIRTUAL | (5 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_SHOW_OVERVIEW_FROM_ALT_TAB = + ACTION_VIRTUAL | (6 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_STEP = + ACTION_VIRTUAL | (7 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_COMMAND = + ACTION_VIRTUAL | (8 << ACTION_POINTER_INDEX_SHIFT); + + private final EventArray mEmptyArray = new EventArray(); + private final Object mExecutionLock = new Object(); + + // We use two arrays and swap the current index when one array is being consumed + private final EventArray[] mArrays = new EventArray[] {new EventArray(), new EventArray()}; + private int mCurrentIndex = 0; + + private final Runnable mMainFrameCallback = this::frameCallbackForMainChoreographer; + private final Runnable mInterimFrameCallback = this::frameCallbackForInterimChoreographer; + + private final Choreographer mMainChoreographer; + + private final TouchConsumer mConsumer; + + private Choreographer mInterimChoreographer; + private Choreographer mCurrentChoreographer; + + private Runnable mCurrentRunnable; + + public MotionEventQueue(Choreographer choreographer, TouchConsumer consumer) { + mMainChoreographer = choreographer; + mConsumer = consumer; + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + + setInterimChoreographer(consumer.getIntrimChoreographer(this)); + } + + public void setInterimChoreographer(Choreographer choreographer) { + synchronized (mExecutionLock) { + synchronized (mArrays) { + setInterimChoreographerLocked(choreographer); + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + } + } + + private void setInterimChoreographerLocked(Choreographer choreographer) { + mInterimChoreographer = choreographer; + if (choreographer == null) { + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + } else { + mCurrentChoreographer = mInterimChoreographer; + mCurrentRunnable = mInterimFrameCallback; + } + } + + public void queue(MotionEvent event) { + mConsumer.preProcessMotionEvent(event); + queueNoPreProcess(event); + } + + private void queueNoPreProcess(MotionEvent event) { + synchronized (mArrays) { + EventArray array = mArrays[mCurrentIndex]; + if (array.isEmpty()) { + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + + int eventAction = event.getAction(); + if (eventAction == ACTION_MOVE && array.lastEventAction == ACTION_MOVE) { + // Replace and recycle the last event + array.set(array.size() - 1, event).recycle(); + } else { + array.add(event); + array.lastEventAction = eventAction; + } + } + } + + private void frameCallbackForMainChoreographer() { + runFor(mMainChoreographer); + } + + private void frameCallbackForInterimChoreographer() { + runFor(mInterimChoreographer); + } + + private void runFor(Choreographer caller) { + synchronized (mExecutionLock) { + EventArray array = swapAndGetCurrentArray(caller); + int size = array.size(); + for (int i = 0; i < size; i++) { + MotionEvent event = array.get(i); + if (event.getActionMasked() == ACTION_VIRTUAL) { + switch (event.getAction()) { + case ACTION_QUICK_SCRUB_START: + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_SCRUB_PROGRESS: + mConsumer.onQuickScrubProgress(event.getX()); + break; + case ACTION_QUICK_SCRUB_END: + mConsumer.onQuickScrubEnd(); + break; + case ACTION_RESET: + mConsumer.reset(); + break; + case ACTION_DEFER_INIT: + mConsumer.deferInit(); + break; + case ACTION_SHOW_OVERVIEW_FROM_ALT_TAB: + mConsumer.onShowOverviewFromAltTab(); + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_STEP: + mConsumer.onQuickStep(event); + break; + case ACTION_COMMAND: + mConsumer.onCommand(event.getSource()); + break; + default: + Log.e(TAG, "Invalid virtual event: " + event.getAction()); + } + } else { + mConsumer.accept(event); + } + event.recycle(); + } + array.clear(); + array.lastEventAction = ACTION_CANCEL; + } + } + + private EventArray swapAndGetCurrentArray(Choreographer caller) { + synchronized (mArrays) { + if (caller != mCurrentChoreographer) { + return mEmptyArray; + } + EventArray current = mArrays[mCurrentIndex]; + mCurrentIndex = mCurrentIndex ^ 1; + return current; + } + } + + private void queueVirtualAction(int action, float progress) { + queueNoPreProcess(MotionEvent.obtain(0, 0, action, progress, 0, 0)); + } + + public void onQuickScrubStart() { + queueVirtualAction(ACTION_QUICK_SCRUB_START, 0); + } + + public void onOverviewShownFromAltTab() { + queueVirtualAction(ACTION_SHOW_OVERVIEW_FROM_ALT_TAB, 0); + } + + public void onQuickScrubProgress(float progress) { + queueVirtualAction(ACTION_QUICK_SCRUB_PROGRESS, progress); + } + + public void onQuickScrubEnd() { + queueVirtualAction(ACTION_QUICK_SCRUB_END, 0); + } + + public void onQuickStep(MotionEvent event) { + event.setAction(ACTION_QUICK_STEP); + queueNoPreProcess(event); + } + + public void reset() { + queueVirtualAction(ACTION_RESET, 0); + } + + public void deferInit() { + queueVirtualAction(ACTION_DEFER_INIT, 0); + } + + public void onCommand(int command) { + MotionEvent ev = MotionEvent.obtain(0, 0, ACTION_COMMAND, 0, 0, 0); + ev.setSource(command); + queueNoPreProcess(ev); + } + + public TouchConsumer getConsumer() { + return mConsumer; + } + + private static class EventArray extends ArrayList { + + public int lastEventAction = ACTION_CANCEL; + + public EventArray() { + super(4); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..ba07ff569f42a82a8493b8c4f6c646ab062fad7b --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.util.SparseArray; + +/** + * Utility class to help manage multiple callbacks based on different states. + */ +public class MultiStateCallback { + + private final SparseArray mCallbacks = new SparseArray<>(); + + private int mState = 0; + + /** + * Adds the provided state flags to the global state and executes any callbacks as a result. + * @param stateFlag + */ + public void setState(int stateFlag) { + mState = mState | stateFlag; + + int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + int state = mCallbacks.keyAt(i); + + if ((mState & state) == state) { + Runnable callback = mCallbacks.valueAt(i); + if (callback != null) { + // Set the callback to null, so that it does not run again. + mCallbacks.setValueAt(i, null); + callback.run(); + } + } + } + } + + /** + * Sets the callbacks to be run when the provided states are enabled. + * The callback is only run once. + */ + public void addCallback(int stateMask, Runnable callback) { + mCallbacks.put(stateMask, callback); + } + + public int getState() { + return mState; + } + + public boolean hasStates(int stateMask) { + return (mState & stateMask) == stateMask; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..07e4deb2fe509ef10b808c821b8189cebbd72508 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.TaskDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.UserHandle; +import android.util.LruCache; +import android.util.SparseArray; + +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.graphics.BitmapInfo; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.graphics.LauncherIcons; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; + +/** + * Extension of {@link IconLoader} with icon normalization support + */ +@TargetApi(Build.VERSION_CODES.O) +public class NormalizedIconLoader extends IconLoader { + + private final SparseArray mDefaultIcons = new SparseArray<>(); + private final DrawableFactory mDrawableFactory; + private LauncherIcons mLauncherIcons; + + public NormalizedIconLoader(Context context, TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + super(context, iconCache, activityInfoCache); + mDrawableFactory = DrawableFactory.get(context); + } + + @Override + public Drawable getDefaultIcon(int userId) { + synchronized (mDefaultIcons) { + BitmapInfo info = mDefaultIcons.get(userId); + if (info == null) { + info = getBitmapInfo(Resources.getSystem() + .getDrawable(android.R.drawable.sym_def_app_icon), userId, 0, false); + mDefaultIcons.put(userId, info); + } + + return new FastBitmapDrawable(info); + } + } + + @Override + protected Drawable createBadgedDrawable(Drawable drawable, int userId, TaskDescription desc) { + return new FastBitmapDrawable(getBitmapInfo(drawable, userId, desc.getPrimaryColor(), + false)); + } + + private synchronized BitmapInfo getBitmapInfo(Drawable drawable, int userId, + int primaryColor, boolean isInstantApp) { + if (mLauncherIcons == null) { + mLauncherIcons = LauncherIcons.obtain(mContext); + } + + mLauncherIcons.setWrapperBackgroundColor(primaryColor); + // User version code O, so that the icon is always wrapped in an adaptive icon container. + return mLauncherIcons.createBadgedIconBitmap(drawable, UserHandle.of(userId), + Build.VERSION_CODES.O, isInstantApp); + } + + @Override + protected Drawable getBadgedActivityIcon(ActivityInfo activityInfo, int userId, + TaskDescription desc) { + BitmapInfo bitmapInfo = getBitmapInfo( + activityInfo.loadUnbadgedIcon(mContext.getPackageManager()), + userId, + desc.getPrimaryColor(), + activityInfo.applicationInfo.isInstantApp()); + return mDrawableFactory.newIcon(bitmapInfo, activityInfo); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..a9a7b0c49c863ca8ea73a4642456db93b4e4d96e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.SystemClock; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.Display; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.AssistDataReceiver; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.NavigationBarCompat; +import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RecentsAnimationListener; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.MotionEvent.INVALID_POINTER_ID; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * Touch consumer for handling events originating from an activity other than Launcher + */ +@TargetApi(Build.VERSION_CODES.P) +public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer { + + private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150; + + private final SparseArray mAnimationStates = new SparseArray<>(); + private final RunningTaskInfo mRunningTask; + private final RecentsModel mRecentsModel; + private final Intent mHomeIntent; + private final ActivityControlHelper mActivityControlHelper; + private final MainThreadExecutor mMainThreadExecutor; + private final Choreographer mBackgroundThreadChoreographer; + private final OverviewCallbacks mOverviewCallbacks; + private final TaskOverlayFactory mTaskOverlayFactory; + + private final boolean mIsDeferredDownTarget; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private int mActivePointerId = INVALID_POINTER_ID; + private boolean mPassedInitialSlop; + // Used for non-deferred gestures to determine when to start dragging + private int mQuickStepDragSlop; + private float mStartDisplacement; + private WindowTransformSwipeHandler mInteractionHandler; + private int mDisplayRotation; + private Rect mStableInsets = new Rect(); + + private VelocityTracker mVelocityTracker; + private MotionEventQueue mEventQueue; + private boolean mIsGoingToHome; + + public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo, + RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl, + MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer, + @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks, + TaskOverlayFactory taskOverlayFactory, VelocityTracker velocityTracker) { + super(base); + + mRunningTask = runningTaskInfo; + mRecentsModel = recentsModel; + mHomeIntent = homeIntent; + mVelocityTracker = velocityTracker; + mActivityControlHelper = activityControl; + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadChoreographer = backgroundThreadChoreographer; + mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget); + mOverviewCallbacks = overviewCallbacks; + mTaskOverlayFactory = taskOverlayFactory; + } + + @Override + public void onShowOverviewFromAltTab() { + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + } + + @Override + public void accept(MotionEvent ev) { + if (mVelocityTracker == null) { + return; + } + switch (ev.getActionMasked()) { + case ACTION_DOWN: { + TraceHelper.beginSection("TouchInt"); + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mPassedInitialSlop = false; + mQuickStepDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); + + // Start the window animation on down to give more time for launcher to draw if the + // user didn't start the gesture over the back button + if (!mIsDeferredDownTarget) { + startTouchTrackingForWindowAnimation(ev.getEventTime()); + } + + Display display = getSystemService(WindowManager.class).getDefaultDisplay(); + mDisplayRotation = display.getRotation(); + WindowManagerWrapper.getInstance().getStableInsets(mStableInsets); + break; + } + case ACTION_POINTER_UP: { + int ptrIdx = ev.getActionIndex(); + int ptrId = ev.getPointerId(ptrIdx); + if (ptrId == mActivePointerId) { + final int newPointerIdx = ptrIdx == 0 ? 1 : 0; + mDownPos.set( + ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), + ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); + mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); + mActivePointerId = ev.getPointerId(newPointerIdx); + } + break; + } + case ACTION_MOVE: { + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + break; + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + float displacement = getDisplacement(ev); + if (!mPassedInitialSlop) { + if (!mIsDeferredDownTarget) { + // Normal gesture, ensure we pass the drag slop before we start tracking + // the gesture + if (Math.abs(displacement) > mQuickStepDragSlop) { + mPassedInitialSlop = true; + mStartDisplacement = displacement; + } + } + } + + if (mPassedInitialSlop && mInteractionHandler != null) { + // Move + mInteractionHandler.updateDisplacement(displacement - mStartDisplacement); + } + break; + } + case ACTION_CANCEL: + // TODO: Should be different than ACTION_UP + case ACTION_UP: { + TraceHelper.endSection("TouchInt"); + + finishTouchTracking(ev); + break; + } + } + } + + private void notifyGestureStarted() { + if (mInteractionHandler == null) { + return; + } + + mOverviewCallbacks.closeAllWindows(); + ActivityManagerWrapper.getInstance().closeSystemWindows( + CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + // Notify the handler that the gesture has actually started + mInteractionHandler.onGestureStarted(); + } + + private boolean isNavBarOnRight() { + return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0; + } + + private boolean isNavBarOnLeft() { + return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0; + } + + private void startTouchTrackingForWindowAnimation(long touchTimeMs) { + // Create the shared handler + RecentsAnimationState animationState = new RecentsAnimationState(); + final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler( + animationState.id, mRunningTask, this, touchTimeMs, mActivityControlHelper); + + // Preload the plan + mRecentsModel.loadTasks(mRunningTask.id, null); + mInteractionHandler = handler; + handler.setGestureEndCallback(mEventQueue::reset); + + CountDownLatch drawWaitLock = new CountDownLatch(1); + handler.setLauncherOnDrawCallback(() -> { + drawWaitLock.countDown(); + if (handler == mInteractionHandler) { + switchToMainChoreographer(); + } + }); + handler.initWhenReady(); + + TraceHelper.beginSection("RecentsController"); + + AssistDataReceiver assistDataReceiver = !mTaskOverlayFactory.needAssist() ? null : + new AssistDataReceiver() { + @Override + public void onHandleAssistData(Bundle bundle) { + if (mInteractionHandler == null) { + // Interaction is probably complete + mRecentsModel.preloadAssistData(mRunningTask.id, bundle); + } else if (handler == mInteractionHandler) { + handler.onAssistDataReceived(bundle); + } + } + }; + + Runnable startActivity = () -> ActivityManagerWrapper.getInstance().startRecentsActivity( + mHomeIntent, assistDataReceiver, animationState, null, null); + + if (Looper.myLooper() != Looper.getMainLooper()) { + startActivity.run(); + try { + drawWaitLock.await(LAUNCHER_DRAW_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // We have waited long enough for launcher to draw + } + } else { + // We should almost always get touch-town on background thread. This is an edge case + // when the background Choreographer has not yet initialized. + BackgroundExecutor.get().submit(startActivity); + } + } + + @Override + public void onCommand(int command) { + RecentsAnimationState state = mAnimationStates.get(command); + if (state != null) { + state.execute(); + } + } + + /** + * Called when the gesture has ended. Does not correlate to the completion of the interaction as + * the animation can still be running. + */ + private void finishTouchTracking(MotionEvent ev) { + if (mPassedInitialSlop && mInteractionHandler != null) { + mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); + + mVelocityTracker.computeCurrentVelocity(1000, + ViewConfiguration.get(this).getScaledMaximumFlingVelocity()); + + float velocity = isNavBarOnRight() ? mVelocityTracker.getXVelocity(mActivePointerId) + : isNavBarOnLeft() ? -mVelocityTracker.getXVelocity(mActivePointerId) + : mVelocityTracker.getYVelocity(mActivePointerId); + mInteractionHandler.onGestureEnded(velocity); + } else { + // Since we start touch tracking on DOWN, we may reach this state without actually + // starting the gesture. In that case, just cleanup immediately. + reset(); + + // Also clean up in case the system has handled the UP and canceled the animation before + // we had a chance to start the recents animation. In such a case, we will not receive + ActivityManagerWrapper.getInstance().cancelRecentsAnimation( + true /* restoreHomeStackPosition */); + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + @Override + public void reset() { + // Clean up the old interaction handler + if (mInteractionHandler != null) { + final WindowTransformSwipeHandler handler = mInteractionHandler; + mInteractionHandler = null; + mIsGoingToHome = handler.mIsGoingToHome; + mMainThreadExecutor.execute(handler::reset); + } + } + + @Override + public void updateTouchTracking(int interactionType) { + if (!mPassedInitialSlop && mIsDeferredDownTarget && mInteractionHandler == null) { + // If we deferred starting the window animation on touch down, then + // start tracking now + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + mPassedInitialSlop = true; + } + + if (mInteractionHandler != null) { + mInteractionHandler.updateInteractionType(interactionType); + } + notifyGestureStarted(); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mEventQueue = queue; + return mBackgroundThreadChoreographer; + } + + @Override + public void onQuickScrubEnd() { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubProgress(progress); + } + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mIsDeferredDownTarget) { + // Deferred gesture, start the animation and gesture tracking once we pass the actual + // touch slop + startTouchTrackingForWindowAnimation(ev.getEventTime()); + mPassedInitialSlop = true; + mStartDisplacement = getDisplacement(ev); + } + notifyGestureStarted(); + } + + private float getDisplacement(MotionEvent ev) { + float eventX = ev.getX(); + float eventY = ev.getY(); + float displacement = eventY - mDownPos.y; + if (isNavBarOnRight()) { + displacement = eventX - mDownPos.x; + } else if (isNavBarOnLeft()) { + displacement = mDownPos.x - eventX; + } + return displacement; + } + + public void switchToMainChoreographer() { + mEventQueue.setInterimChoreographer(null); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(ev); + if (ev.getActionMasked() == ACTION_POINTER_UP) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean forceToLauncherConsumer() { + return mIsGoingToHome; + } + + @Override + public boolean deferNextEventToMainThread() { + // TODO: Consider also check if the eventQueue is using mainThread of not. + return mInteractionHandler != null; + } + + private class RecentsAnimationState implements RecentsAnimationListener { + + private final int id; + + private RecentsAnimationControllerCompat mController; + private RemoteAnimationTargetSet mTargets; + private Rect mHomeContentInsets; + private Rect mMinimizedHomeBounds; + private boolean mCancelled; + + public RecentsAnimationState() { + id = mAnimationStates.size(); + mAnimationStates.put(id, this); + } + + @Override + public void onAnimationStart( + RecentsAnimationControllerCompat controller, + RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, + Rect minimizedHomeBounds) { + mController = controller; + mTargets = new RemoteAnimationTargetSet(apps, MODE_CLOSING); + mHomeContentInsets = homeContentInsets; + mMinimizedHomeBounds = minimizedHomeBounds; + mEventQueue.onCommand(id); + } + + @Override + public void onAnimationCanceled() { + mCancelled = true; + mEventQueue.onCommand(id); + } + + public void execute() { + if (mInteractionHandler == null || mInteractionHandler.id != id) { + if (!mCancelled && mController != null) { + TraceHelper.endSection("RecentsController", "Finishing no handler"); + mController.finish(false /* toHome */); + } + } else if (mCancelled) { + TraceHelper.endSection("RecentsController", + "Cancelled: " + mInteractionHandler); + mInteractionHandler.onRecentsAnimationCanceled(); + } else { + TraceHelper.partitionSection("RecentsController", "Received"); + mInteractionHandler.onRecentsAnimationStart(mController, mTargets, + mHomeContentInsets, mMinimizedHomeBounds); + } + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java new file mode 100644 index 0000000000000000000000000000000000000000..3f5096d9194683393e7c71d552ab6a695fcdb690 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; + +/** + * Callbacks related to overview/quicksteps. + */ +public class OverviewCallbacks { + + private static OverviewCallbacks sInstance; + + public static OverviewCallbacks get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(OverviewCallbacks.class, + context.getApplicationContext(), R.string.overview_callbacks_class); + } + return sInstance; + } + + public void onInitOverviewTransition() { } + + public void onResetOverview() { } + + public void closeAllWindows() { } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..520f5662cbfbeb8bdb92e52a4dada5c187c32b0c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ResolveInfo; +import android.graphics.Rect; +import android.os.Build; +import android.os.PatternMatcher; +import android.os.SystemClock; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; +import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.TransactionCompat; + +import java.util.ArrayList; + +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_CHANGED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Helper class to handle various atomic commands for switching between Overview. + */ +@TargetApi(Build.VERSION_CODES.P) +public class OverviewCommandHelper { + + private static final long RECENTS_LAUNCH_DURATION = 250; + + private static final String TAG = "OverviewCommandHelper"; + + private final Context mContext; + private final ActivityManagerWrapper mAM; + private final RecentsModel mRecentsModel; + private final MainThreadExecutor mMainThreadExecutor; + private final ComponentName mMyHomeComponent; + + private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private String mUpdateRegisteredPackage; + + public Intent overviewIntent; + public ComponentName overviewComponent; + private ActivityControlHelper mActivityControlHelper; + + private long mLastToggleTime; + + public OverviewCommandHelper(Context context) { + mContext = context; + mAM = ActivityManagerWrapper.getInstance(); + mMainThreadExecutor = new MainThreadExecutor(); + mRecentsModel = RecentsModel.getInstance(mContext); + + Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(mContext.getPackageName()); + ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); + mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); + + mContext.registerReceiver(mUserPreferenceChangeReceiver, + new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); + initOverviewTargets(); + } + + private void initOverviewTargets() { + ComponentName defaultHome = PackageManagerWrapper.getInstance() + .getHomeActivities(new ArrayList<>()); + + final String overviewIntentCategory; + if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) { + // User default home is same as out home app. Use Overview integrated in Launcher. + overviewComponent = mMyHomeComponent; + mActivityControlHelper = new LauncherActivityControllerHelper(); + overviewIntentCategory = Intent.CATEGORY_HOME; + + if (mUpdateRegisteredPackage != null) { + // Remove any update listener as we don't care about other packages. + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } else { + // The default home app is a different launcher. Use the fallback Overview instead. + overviewComponent = new ComponentName(mContext, RecentsActivity.class); + mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome); + overviewIntentCategory = Intent.CATEGORY_DEFAULT; + + // User's default home app can change as a result of package updates of this app (such + // as uninstalling the app or removing the "Launcher" feature in an update). + // Listen for package updates of this app (and remove any previously attached + // package listener). + if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) { + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + } + + mUpdateRegisteredPackage = defaultHome.getPackageName(); + IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); + updateReceiver.addAction(ACTION_PACKAGE_CHANGED); + updateReceiver.addAction(ACTION_PACKAGE_REMOVED); + updateReceiver.addDataScheme("package"); + updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage, + PatternMatcher.PATTERN_LITERAL); + mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver); + } + } + + overviewIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(overviewIntentCategory) + .setComponent(overviewComponent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public void onDestroy() { + mContext.unregisterReceiver(mUserPreferenceChangeReceiver); + + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } + + public void onOverviewToggle() { + // If currently screen pinning, do not enter overview + if (mAM.isScreenPinningActive()) { + return; + } + + mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + mMainThreadExecutor.execute(new RecentsActivityCommand<>()); + } + + public void onOverviewShown() { + mMainThreadExecutor.execute(new ShowRecentsCommand()); + } + + public void onTip(int actionType, int viewType) { + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + UserEventDispatcher.newInstance(mContext, + new InvariantDeviceProfile(mContext).getDeviceProfile(mContext)) + .logActionTip(actionType, viewType); + } + }); + } + + public ActivityControlHelper getActivityControlHelper() { + return mActivityControlHelper; + } + + private class ShowRecentsCommand extends RecentsActivityCommand { + + @Override + protected boolean handleCommand(long elapsedTime) { + return mHelper.getVisibleRecentsView() != null; + } + } + + private class RecentsActivityCommand implements Runnable { + + protected final ActivityControlHelper mHelper; + private final long mCreateTime; + private final int mRunningTaskId; + + private ActivityInitListener mListener; + private T mActivity; + private RecentsView mRecentsView; + private final long mToggleClickedTime = SystemClock.uptimeMillis(); + private boolean mUserEventLogged; + + public RecentsActivityCommand() { + mHelper = getActivityControlHelper(); + mCreateTime = SystemClock.elapsedRealtime(); + mRunningTaskId = mAM.getRunningTask().id; + + // Preload the plan + mRecentsModel.loadTasks(mRunningTaskId, null); + } + + @Override + public void run() { + long elapsedTime = mCreateTime - mLastToggleTime; + mLastToggleTime = mCreateTime; + + if (!handleCommand(elapsedTime)) { + // Start overview + if (!mHelper.switchToRecentsIfVisible(true)) { + mListener = mHelper.createActivityInitListener(this::onActivityReady); + mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation, + mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION); + } + } + } + + protected boolean handleCommand(long elapsedTime) { + // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows + // the menu activity which takes window focus, preventing the right condition from + // being run below + RecentsView recents = mHelper.getVisibleRecentsView(); + if (recents != null) { + // Launch the next task + recents.showNextTask(); + return true; + } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) { + // The user tried to launch back into overview too quickly, either after + // launching an app, or before overview has actually shown, just ignore for now + return true; + } + return false; + } + + private boolean onActivityReady(T activity, Boolean wasVisible) { + activity.getOverviewPanel().setCurrentTask(mRunningTaskId); + AbstractFloatingView.closeAllOpenViews(activity, wasVisible); + AnimationFactory factory = mHelper.prepareRecentsUI(activity, wasVisible, + (controller) -> { + controller.dispatchOnStart(); + ValueAnimator anim = controller.getAnimationPlayer() + .setDuration(RECENTS_LAUNCH_DURATION); + anim.setInterpolator(FAST_OUT_SLOW_IN); + anim.start(); + }); + factory.onRemoteAnimationReceived(null); + if (wasVisible) { + factory.createActivityController(RECENTS_LAUNCH_DURATION, INTERACTION_NORMAL); + } + mActivity = activity; + mRecentsView = mActivity.getOverviewPanel(); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + if (!mUserEventLogged) { + activity.getUserEventDispatcher().logActionCommand(Action.Command.RECENTS_BUTTON, + mHelper.getContainerType(), ContainerType.TASKSWITCHER); + mUserEventLogged = true; + } + return false; + } + + private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) { + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents( + (int) (SystemClock.uptimeMillis() - mToggleClickedTime)); + } + + if (mListener != null) { + mListener.unregister(); + } + AnimatorSet anim = new AnimatorSet(); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + if (mRecentsView != null) { + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, + true /* animate */); + } + } + }); + if (mActivity == null) { + Log.e(TAG, "Animation created, before activity"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + RemoteAnimationTargetSet targetSet = + new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING); + + // Use the top closing app to determine the insets for the animation + RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); + if (runningTaskTarget == null) { + Log.e(TAG, "No closing app"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + final ClipAnimationHelper clipHelper = new ClipAnimationHelper(); + + // At this point, the activity is already started and laid-out. Get the home-bounds + // relative to the screen using the rootView of the activity. + int loc[] = new int[2]; + View rootView = mActivity.getRootView(); + rootView.getLocationOnScreen(loc); + Rect homeBounds = new Rect(loc[0], loc[1], + loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); + clipHelper.updateSource(homeBounds, runningTaskTarget); + + TransformedRect targetRect = new TransformedRect(); + mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, + INTERACTION_NORMAL, targetRect); + clipHelper.updateTargetRect(targetRect); + clipHelper.prepareAnimation(false /* isOpening */); + + SyncRtSurfaceTransactionApplier syncTransactionApplier = + new SyncRtSurfaceTransactionApplier(rootView); + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); + valueAnimator.setDuration(RECENTS_LAUNCH_DURATION); + valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + valueAnimator.addUpdateListener((v) -> + clipHelper.applyTransform(targetSet, (float) v.getAnimatedValue(), + syncTransactionApplier)); + + if (targetSet.isAnimatingHome()) { + // If we are animating home, fade in the opening targets + RemoteAnimationTargetSet openingSet = + new RemoteAnimationTargetSet(targetCompats, MODE_OPENING); + + TransactionCompat transaction = new TransactionCompat(); + valueAnimator.addUpdateListener((v) -> { + for (RemoteAnimationTargetCompat app : openingSet.apps) { + transaction.setAlpha(app.leash, (float) v.getAnimatedValue()); + } + transaction.apply(); + }); + } + anim.play(valueAnimator); + return anim; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java new file mode 100644 index 0000000000000000000000000000000000000000..36ceef5a734547eb11f8a4e02944b4ae6c70b778 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.util.UiThreadHelper; +import com.android.systemui.shared.recents.ISystemUiProxy; + +import java.util.concurrent.ExecutionException; + +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; +import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; + +/** + * Sets overview interaction flags, such as: + * + * - FLAG_DISABLE_QUICK_SCRUB + * - FLAG_DISABLE_SWIPE_UP + * - FLAG_SHOW_OVERVIEW_BUTTON + * + * @see com.android.systemui.shared.system.NavigationBarCompat.InteractionType and associated flags. + */ +public class OverviewInteractionState { + + private static final String TAG = "OverviewFlags"; + + private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once"; + private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME = + "config_swipe_up_gesture_setting_available"; + private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME = + "config_swipe_up_gesture_default"; + + // We do not need any synchronization for this variable as its only written on UI thread. + private static OverviewInteractionState INSTANCE; + + public static OverviewInteractionState getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new OverviewInteractionState(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> OverviewInteractionState.getInstance(context)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private static final int MSG_SET_PROXY = 200; + private static final int MSG_SET_BACK_BUTTON_ALPHA = 201; + private static final int MSG_SET_SWIPE_UP_ENABLED = 202; + + private final SwipeUpGestureEnabledSettingObserver mSwipeUpSettingObserver; + + private final Context mContext; + private final Handler mUiHandler; + private final Handler mBgHandler; + + // These are updated on the background thread + private ISystemUiProxy mISystemUiProxy; + private boolean mSwipeUpEnabled = true; + private float mBackButtonAlpha = 1; + + private Runnable mOnSwipeUpSettingChangedListener; + + private OverviewInteractionState(Context context) { + mContext = context; + + // Data posted to the uihandler will be sent to the bghandler. Data is sent to uihandler + // because of its high send frequency and data may be very different than the previous value + // For example, send back alpha on uihandler to avoid flickering when setting its visibility + mUiHandler = new Handler(this::handleUiMessage); + mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage); + + if (getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME)) { + mSwipeUpSettingObserver = new SwipeUpGestureEnabledSettingObserver(mUiHandler, + context.getContentResolver()); + mSwipeUpSettingObserver.register(); + } else { + mSwipeUpSettingObserver = null; + mSwipeUpEnabled = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME); + } + } + + public boolean isSwipeUpGestureEnabled() { + return mSwipeUpEnabled; + } + + public float getBackButtonAlpha() { + return mBackButtonAlpha; + } + + public void setBackButtonAlpha(float alpha, boolean animate) { + if (!mSwipeUpEnabled) { + alpha = 1; + } + mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_ALPHA); + mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_ALPHA, animate ? 1 : 0, 0, alpha) + .sendToTarget(); + } + + public void setSystemUiProxy(ISystemUiProxy proxy) { + mBgHandler.obtainMessage(MSG_SET_PROXY, proxy).sendToTarget(); + } + + private boolean handleUiMessage(Message msg) { + if (msg.what == MSG_SET_BACK_BUTTON_ALPHA) { + mBackButtonAlpha = (float) msg.obj; + } + mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2, msg.obj).sendToTarget(); + return true; + } + + private boolean handleBgMessage(Message msg) { + switch (msg.what) { + case MSG_SET_PROXY: + mISystemUiProxy = (ISystemUiProxy) msg.obj; + break; + case MSG_SET_BACK_BUTTON_ALPHA: + applyBackButtonAlpha((float) msg.obj, msg.arg1 == 1); + return true; + case MSG_SET_SWIPE_UP_ENABLED: + mSwipeUpEnabled = msg.arg1 != 0; + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + + if (mOnSwipeUpSettingChangedListener != null) { + mOnSwipeUpSettingChangedListener.run(); + } + break; + } + applyFlags(); + return true; + } + + public void setOnSwipeUpSettingChangedListener(Runnable listener) { + mOnSwipeUpSettingChangedListener = listener; + } + + @WorkerThread + private void applyFlags() { + if (mISystemUiProxy == null) { + return; + } + + int flags = 0; + if (!mSwipeUpEnabled) { + flags = FLAG_DISABLE_SWIPE_UP | FLAG_DISABLE_QUICK_SCRUB | FLAG_SHOW_OVERVIEW_BUTTON; + } + try { + mISystemUiProxy.setInteractionState(flags); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview interaction flags", e); + } + } + + @WorkerThread + private void applyBackButtonAlpha(float alpha, boolean animate) { + if (mISystemUiProxy == null) { + return; + } + try { + mISystemUiProxy.setBackButtonAlpha(alpha, animate); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview back button alpha", e); + } + } + + private class SwipeUpGestureEnabledSettingObserver extends ContentObserver { + private Handler mHandler; + private ContentResolver mResolver; + private final int defaultValue; + + SwipeUpGestureEnabledSettingObserver(Handler handler, ContentResolver resolver) { + super(handler); + mHandler = handler; + mResolver = resolver; + defaultValue = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME) ? 1 : 0; + } + + public void register() { + mResolver.registerContentObserver(Settings.Secure.getUriFor(SWIPE_UP_SETTING_NAME), + false, this); + mSwipeUpEnabled = getValue(); + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED); + mHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED, getValue() ? 1 : 0, 0).sendToTarget(); + } + + private boolean getValue() { + return Settings.Secure.getInt(mResolver, SWIPE_UP_SETTING_NAME, defaultValue) == 1; + } + } + + private boolean getSystemBooleanRes(String resName) { + Resources res = Resources.getSystem(); + int resId = res.getIdentifier(resName, "bool", "android"); + + if (resId != 0) { + return res.getBoolean(resId); + } else { + Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); + return false; + } + } + + private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { + if (mSwipeUpEnabled && !Utilities.getPrefs(mContext).getBoolean( + HAS_ENABLED_QUICKSTEP_ONCE, true)) { + Utilities.getPrefs(mContext).edit() + .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true) + .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false) + .apply(); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java new file mode 100644 index 0000000000000000000000000000000000000000..9b362c01602404d466ca0b554390f270b3a3a899 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.animation.Interpolator; + +import com.android.launcher3.Alarm; +import com.android.launcher3.BaseActivity; +import com.android.launcher3.OnAlarmListener; +import com.android.launcher3.Utilities; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +/** + * Responds to quick scrub callbacks to page through and launch recent tasks. + * + * The behavior is to evenly divide the progress into sections, each of which scrolls one page. + * The first and last section set an alarm to auto-advance backwards or forwards, respectively. + */ +public class QuickScrubController implements OnAlarmListener { + + public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; + public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 200; + // We want the translation y to finish faster than the rest of the animation. + public static final float QUICK_SCRUB_TRANSLATION_Y_FACTOR = 5f / 6; + public static final Interpolator QUICK_SCRUB_START_INTERPOLATOR = FAST_OUT_SLOW_IN; + + /** + * Snap to a new page when crossing these thresholds. The first and last auto-advance. + */ + private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] { + 0.05f, 0.20f, 0.35f, 0.50f, 0.65f, 0.80f, 0.95f + }; + + private static final String TAG = "QuickScrubController"; + private static final boolean ENABLE_AUTO_ADVANCE = true; + private static final long AUTO_ADVANCE_DELAY = 500; + private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325; + private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60; + + private final Alarm mAutoAdvanceAlarm; + private final RecentsView mRecentsView; + private final BaseActivity mActivity; + + private boolean mInQuickScrub; + private boolean mWaitingForTaskLaunch; + private int mQuickScrubSection; + private boolean mStartedFromHome; + private boolean mFinishedTransitionToQuickScrub; + private Runnable mOnFinishedTransitionToQuickScrubRunnable; + private ActivityControlHelper mActivityControlHelper; + + public QuickScrubController(BaseActivity activity, RecentsView recentsView) { + mActivity = activity; + mRecentsView = recentsView; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm = new Alarm(); + mAutoAdvanceAlarm.setOnAlarmListener(this); + } + } + + public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) { + prepareQuickScrub(TAG); + mInQuickScrub = true; + mStartedFromHome = startingFromHome; + mQuickScrubSection = 0; + mFinishedTransitionToQuickScrub = false; + mActivityControlHelper = controlHelper; + + snapToNextTaskIfAvailable(); + mActivity.getUserEventDispatcher().resetActionDurationMillis(); + } + + public void onQuickScrubEnd() { + mInQuickScrub = false; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.cancelAlarm(); + } + int page = mRecentsView.getNextPage(); + Runnable launchTaskRunnable = () -> { + TaskView taskView = mRecentsView.getTaskViewAt(page); + if (taskView != null) { + mWaitingForTaskLaunch = true; + taskView.launchTask(true, (result) -> { + if (!result) { + taskView.notifyTaskLaunchFailed(TAG); + breakOutOfQuickScrub(); + } else { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(Touch.DRAGDROP, + LauncherLogProto.Action.Direction.NONE, page, + TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key)); + } + mWaitingForTaskLaunch = false; + }, taskView.getHandler()); + } else { + breakOutOfQuickScrub(); + } + mActivityControlHelper = null; + }; + int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) + * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; + if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { + // Settle on the page then launch it + mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable); + } else { + // No page move needed, just launch it + if (mFinishedTransitionToQuickScrub) { + launchTaskRunnable.run(); + } else { + mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable; + } + } + } + + public void cancelActiveQuickscrub() { + if (!mInQuickScrub) { + return; + } + Log.d(TAG, "Quickscrub was active, cancelling"); + mInQuickScrub = false; + mActivityControlHelper = null; + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + } + + /** + * Initializes the UI for quick scrub, returns true if success. + */ + public boolean prepareQuickScrub(String tag) { + if (mWaitingForTaskLaunch || mInQuickScrub) { + Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); + return false; + } + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + return true; + } + + public boolean isWaitingForTaskLaunch() { + return mWaitingForTaskLaunch; + } + + /** + * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction. + */ + private void breakOutOfQuickScrub() { + if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null + || !mActivityControlHelper.switchToRecentsIfVisible(false)) { + mActivity.onBackPressed(); + } + } + + public void onQuickScrubProgress(float progress) { + int quickScrubSection = 0; + for (float threshold : QUICK_SCRUB_THRESHOLDS) { + if (progress < threshold) { + break; + } + quickScrubSection++; + } + if (quickScrubSection != mQuickScrubSection) { + boolean cameFromAutoAdvance = mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + || mQuickScrubSection == 0; + int pageToGoTo = mRecentsView.getNextPage() + quickScrubSection - mQuickScrubSection; + if (mFinishedTransitionToQuickScrub && !cameFromAutoAdvance) { + goToPageWithHaptic(pageToGoTo); + } + if (ENABLE_AUTO_ADVANCE) { + if (quickScrubSection == QUICK_SCRUB_THRESHOLDS.length || quickScrubSection == 0) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } else { + mAutoAdvanceAlarm.cancelAlarm(); + } + } + mQuickScrubSection = quickScrubSection; + } + } + + public void onFinishedTransitionToQuickScrub() { + mFinishedTransitionToQuickScrub = true; + Runnable action = mOnFinishedTransitionToQuickScrubRunnable; + // Clear the runnable before executing it, to prevent potential recursion. + mOnFinishedTransitionToQuickScrubRunnable = null; + if (action != null) { + action.run(); + } + } + + public void snapToNextTaskIfAvailable() { + if (mInQuickScrub && mRecentsView.getChildCount() > 0) { + int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; + goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, + QUICK_SCRUB_START_INTERPOLATOR); + } + } + + private void goToPageWithHaptic(int pageToGoTo) { + goToPageWithHaptic(pageToGoTo, -1 /* overrideDuration */, false /* forceHaptic */, null); + } + + private void goToPageWithHaptic(int pageToGoTo, int overrideDuration, boolean forceHaptic, + Interpolator interpolator) { + pageToGoTo = Utilities.boundToRange(pageToGoTo, 0, mRecentsView.getTaskViewCount() - 1); + boolean snappingToPage = pageToGoTo != mRecentsView.getNextPage(); + if (snappingToPage) { + int duration = overrideDuration > -1 ? overrideDuration + : Math.abs(pageToGoTo - mRecentsView.getNextPage()) + * QUICKSCRUB_SNAP_DURATION_PER_PAGE; + mRecentsView.snapToPage(pageToGoTo, duration, interpolator); + } + if (snappingToPage || forceHaptic) { + mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + } + + @Override + public void onAlarm(Alarm alarm) { + int currPage = mRecentsView.getNextPage(); + boolean recentsVisible = mActivityControlHelper != null + && mActivityControlHelper.getVisibleRecentsView() != null; + if (!recentsVisible) { + Log.w(TAG, "Failed to auto advance; recents not visible"); + return; + } + if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + && currPage < mRecentsView.getTaskViewCount() - 1) { + goToPageWithHaptic(currPage + 1); + } else if (mQuickScrubSection == 0 && currPage > 0) { + goToPageWithHaptic(currPage - 1); + } + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..4adddd21f0b03cd58e1f1b3561c9c024e2d646bd --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; + +import com.android.launcher3.MainProcessInitializer; +import com.android.systemui.shared.system.ThreadedRendererCompat; + +@SuppressWarnings("unused") +public class QuickstepProcessInitializer extends MainProcessInitializer { + + public QuickstepProcessInitializer(Context context) { } + + @Override + protected void init(Context context) { + super.init(context); + + // Elevate GPU priority for Quickstep and Remote animations. + ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..3247abe12318d98269e8a016b4418120d1664de7 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAnimationRunner; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.badge.BadgeInfo; +import com.android.launcher3.uioverrides.UiFactory; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsRootView; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * A simple activity to show the recently launched tasks + */ +public class RecentsActivity extends BaseDraggingActivity { + + private Handler mUiHandler = new Handler(Looper.getMainLooper()); + private RecentsRootView mRecentsRootView; + private FallbackRecentsView mFallbackRecentsView; + + private Configuration mOldConfig; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mOldConfig = new Configuration(getResources().getConfiguration()); + initDeviceProfile(); + + setContentView(R.layout.fallback_recents_activity); + mRecentsRootView = findViewById(R.id.drag_layer); + mFallbackRecentsView = findViewById(R.id.overview_panel); + + mRecentsRootView.setup(); + + getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW, + Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText)); + RecentsActivityTracker.onRecentsActivityCreate(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int diff = newConfig.diff(mOldConfig); + if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) { + onHandleConfigChanged(); + } + mOldConfig.setTo(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { + onHandleConfigChanged(); + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); + } + + public void onRootViewSizeChanged() { + if (isInMultiWindowModeCompat()) { + onHandleConfigChanged(); + } + } + + private void onHandleConfigChanged() { + mUserEventDispatcher = null; + initDeviceProfile(); + + AbstractFloatingView.closeOpenViews(this, true, + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + dispatchDeviceProfileChanged(); + + mRecentsRootView.setup(); + reapplyUi(); + } + + @Override + protected void reapplyUi() { + mRecentsRootView.dispatchInsets(); + } + + private void initDeviceProfile() { + // In case we are reusing IDP, create a copy so that we dont conflict with Launcher + // activity. + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + if (isInMultiWindowModeCompat()) { + InvariantDeviceProfile idp = appState == null + ? new InvariantDeviceProfile(this) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(this); + mDeviceProfile = mRecentsRootView == null ? dp.copy(this) + : dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize()); + } else { + // If we are reusing the Invariant device profile, make a copy. + mDeviceProfile = appState == null + ? new InvariantDeviceProfile(this).getDeviceProfile(this) + : appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this); + } + onDeviceProfileInitiated(); + } + + @Override + public BaseDragLayer getDragLayer() { + return mRecentsRootView; + } + + @Override + public View getRootView() { + return mRecentsRootView; + } + + @Override + public T getOverviewPanel() { + return (T) mFallbackRecentsView; + } + + @Override + public BadgeInfo getBadgeInfoForItem(ItemInfo info) { + return null; + } + + @Override + public ActivityOptions getActivityLaunchOptions(final View v) { + if (!(v instanceof TaskView)) { + return null; + } + + final TaskView taskView = (TaskView) v; + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(composeRecentsLaunchAnimator(taskView, targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, RECENTS_LAUNCH_DURATION, + RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION + - STATUS_BAR_TRANSITION_PRE_DELAY)); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView, + RemoteAnimationTargetCompat[] targets) { + AnimatorSet target = new AnimatorSet(); + boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING); + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper) + .setDuration(RECENTS_LAUNCH_DURATION)); + + // Found a visible recents task that matches the opening app, lets launch the app from there + if (activityClosing) { + Animator adjacentAnimation = mFallbackRecentsView + .createAdjacentPageAnimForTaskLaunch(taskView, helper); + adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION); + adjacentAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mFallbackRecentsView.resetTaskVisuals(); + } + }); + target.play(adjacentAnimation); + } + return target; + } + + @Override + public void invalidateParent(ItemInfo info) { } + + @Override + protected void onStart() { + // Set the alpha to 1 before calling super, as it may get set back to 0 due to + // onActivityStart callback. + mFallbackRecentsView.setContentAlpha(1); + super.onStart(); + UiFactory.onStart(this); + mFallbackRecentsView.resetTaskVisuals(); + } + + @Override + protected void onStop() { + super.onStop(); + + // Workaround for b/78520668, explicitly trim memory once UI is hidden + onTrimMemory(TRIM_MEMORY_UI_HIDDEN); + } + + @Override + public void onEnterAnimationComplete() { + super.onEnterAnimationComplete(); + UiFactory.onEnterAnimationComplete(this); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + UiFactory.onTrimMemory(this, level); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + RecentsActivityTracker.onRecentsActivityNewIntent(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + RecentsActivityTracker.onRecentsActivityDestroy(this); + } + + @Override + public void onBackPressed() { + // TODO: Launch the task we came from + startHome(); + } + + public void startHome() { + startActivity(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(prefix + "Misc:"); + dumpMisc(writer); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..9e7369127351f845d67643ce42568b97ca773cff --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + +import com.android.launcher3.MainThreadExecutor; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.lang.ref.WeakReference; +import java.util.function.BiPredicate; + +/** + * Utility class to track create/destroy for RecentsActivity + */ +@TargetApi(Build.VERSION_CODES.P) +public class RecentsActivityTracker implements ActivityInitListener { + + private static WeakReference sCurrentActivity = new WeakReference<>(null); + private static final Scheduler sScheduler = new Scheduler(); + + private final BiPredicate mOnInitListener; + + public RecentsActivityTracker(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + public void register() { + sScheduler.schedule(this); + } + + @Override + public void unregister() { + sScheduler.clearReference(this); + } + + private boolean init(RecentsActivity activity, boolean visible) { + return mOnInitListener.test(activity, visible); + } + + public static RecentsActivity getCurrentActivity() { + return sCurrentActivity.get(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(intent, options); + } + + public static void onRecentsActivityCreate(RecentsActivity activity) { + sCurrentActivity = new WeakReference<>(activity); + sScheduler.initIfPending(activity, false); + } + + + public static void onRecentsActivityNewIntent(RecentsActivity activity) { + sScheduler.initIfPending(activity, activity.isStarted()); + } + + public static void onRecentsActivityDestroy(RecentsActivity activity) { + if (sCurrentActivity.get() == activity) { + sCurrentActivity.clear(); + } + } + + + private static class Scheduler implements Runnable { + + private WeakReference mPendingTracker = new WeakReference<>(null); + private MainThreadExecutor mMainThreadExecutor; + + public synchronized void schedule(RecentsActivityTracker tracker) { + mPendingTracker = new WeakReference<>(tracker); + if (mMainThreadExecutor == null) { + mMainThreadExecutor = new MainThreadExecutor(); + } + mMainThreadExecutor.execute(this); + } + + @Override + public void run() { + RecentsActivity activity = sCurrentActivity.get(); + if (activity != null) { + initIfPending(activity, activity.isStarted()); + } + } + + public synchronized boolean initIfPending(RecentsActivity activity, boolean alreadyOnHome) { + RecentsActivityTracker tracker = mPendingTracker.get(); + if (tracker != null) { + if (!tracker.init(activity, alreadyOnHome)) { + mPendingTracker.clear(); + } + return true; + } + return false; + } + + public synchronized boolean clearReference(RecentsActivityTracker tracker) { + if (mPendingTracker.get() == tracker) { + mPendingTracker.clear(); + return true; + } + return false; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..753cb4bb6abb619eb0bee16116b58d34cbff2752 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import com.android.launcher3.util.LooperExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.util.UiThreadHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; + +/** + * Wrapper around RecentsAnimationController to help with some synchronization + */ +public class RecentsAnimationWrapper { + + // A list of callbacks to run when we receive the recents animation target. There are different + // than the state callbacks as these run on the current worker thread. + private final ArrayList mCallbacks = new ArrayList<>(); + + public RemoteAnimationTargetSet targetSet; + + private RecentsAnimationControllerCompat mController; + private boolean mInputConsumerEnabled = false; + private boolean mBehindSystemBars = true; + private boolean mSplitScreenMinimized = false; + + private final ExecutorService mExecutorService = + new LooperExecutor(UiThreadHelper.getBackgroundLooper()); + + public synchronized void setController( + RecentsAnimationControllerCompat controller, RemoteAnimationTargetSet targetSet) { + TraceHelper.partitionSection("RecentsController", "Set controller " + controller); + this.mController = controller; + this.targetSet = targetSet; + + if (controller == null) { + return; + } + if (mInputConsumerEnabled) { + enableInputConsumer(); + } + + if (!mCallbacks.isEmpty()) { + for (Runnable action : new ArrayList<>(mCallbacks)) { + action.run(); + } + mCallbacks.clear(); + } + } + + public synchronized void runOnInit(Runnable action) { + if (targetSet == null) { + mCallbacks.add(action); + } else { + action.run(); + } + } + + /** + * @param onFinishComplete A callback that runs after the animation controller has finished + * on the background thread. + */ + public void finish(boolean toHome, Runnable onFinishComplete) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + mController = null; + TraceHelper.endSection("RecentsController", + "Finish " + controller + ", toHome=" + toHome); + if (controller != null) { + controller.setInputConsumerEnabled(false); + controller.finish(toHome); + if (onFinishComplete != null) { + onFinishComplete.run(); + } + } + }); + } + + public void enableInputConsumer() { + mInputConsumerEnabled = true; + if (mInputConsumerEnabled) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Enabling consumer on " + controller); + if (controller != null) { + controller.setInputConsumerEnabled(true); + } + }); + } + } + + public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { + if (mBehindSystemBars == behindSystemBars) { + return; + } + mBehindSystemBars = behindSystemBars; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting behind system bars on " + controller); + if (controller != null) { + controller.setAnimationTargetsBehindSystemBars(behindSystemBars); + } + }); + } + + /** + * NOTE: As a workaround for conflicting animations (Launcher animating the task leash, and + * SystemUI resizing the docked stack, which resizes the task), we currently only set the + * minimized mode, and not the inverse. + * TODO: Synchronize the minimize animation with the launcher animation + */ + public void setSplitScreenMinimizedForTransaction(boolean minimized) { + if (mSplitScreenMinimized || !minimized) { + return; + } + mSplitScreenMinimized = minimized; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting minimize dock on " + controller); + if (controller != null) { + controller.setSplitScreenMinimized(minimized); + } + }); + } + + public void hideCurrentInputMethod() { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Hiding currentinput method on " + controller); + if (controller != null) { + controller.hideCurrentInputMethod(); + } + }); + } + + public RecentsAnimationControllerCompat getController() { + return mController; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java new file mode 100644 index 0000000000000000000000000000000000000000..e0ea597f4d21898e7aa0eed639d8b853ed848dac --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.util.LruCache; +import android.util.SparseArray; +import android.view.accessibility.AccessibilityManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; + +/** + * Singleton class to load and manage recents model. + */ +@TargetApi(Build.VERSION_CODES.O) +public class RecentsModel extends TaskStackChangeListener { + // We do not need any synchronization for this variable as its only written on UI thread. + private static RecentsModel INSTANCE; + + public static RecentsModel getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new RecentsModel(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> RecentsModel.getInstance(context)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private final SparseArray mCachedAssistData = new SparseArray<>(1); + private final ArrayList mAssistDataListeners = new ArrayList<>(); + + private final Context mContext; + private final RecentsTaskLoader mRecentsTaskLoader; + private final MainThreadExecutor mMainThreadExecutor; + + private RecentsTaskLoadPlan mLastLoadPlan; + private int mLastLoadPlanId; + private int mTaskChangeId; + private ISystemUiProxy mSystemUiProxy; + private boolean mClearAssistCacheOnStackChange = true; + private final boolean mIsLowRamDevice; + private boolean mPreloadTasksInBackground; + private final AccessibilityManager mAccessibilityManager; + + private RecentsModel(Context context) { + mContext = context; + + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mIsLowRamDevice = activityManager.isLowRamDevice(); + mMainThreadExecutor = new MainThreadExecutor(); + + Resources res = context.getResources(); + mRecentsTaskLoader = new RecentsTaskLoader(mContext, + res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize), + res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0) { + + @Override + protected IconLoader createNewIconLoader(Context context, + TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + return new NormalizedIconLoader(context, iconCache, activityInfoCache); + } + }; + mRecentsTaskLoader.startLoader(mContext); + ActivityManagerWrapper.getInstance().registerTaskStackListener(this); + + mTaskChangeId = 1; + loadTasks(-1, null); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + } + + public RecentsTaskLoader getRecentsTaskLoader() { + return mRecentsTaskLoader; + } + + /** + * Preloads the task plan + * @param taskId The running task id or -1 + * @param callback The callback to receive the task plan once its complete or null. This is + * always called on the UI thread. + * @return the request id associated with this call. + */ + public int loadTasks(int taskId, Consumer callback) { + final int requestId = mTaskChangeId; + + // Fail fast if nothing has changed. + if (mLastLoadPlanId == mTaskChangeId) { + if (callback != null) { + final RecentsTaskLoadPlan plan = mLastLoadPlan; + mMainThreadExecutor.execute(() -> callback.accept(plan)); + } + return requestId; + } + + BackgroundExecutor.get().submit(() -> { + // Preload the plan + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext); + PreloadOptions opts = new PreloadOptions(); + opts.loadTitles = mAccessibilityManager.isEnabled(); + loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId()); + // Set the load plan on UI thread + mMainThreadExecutor.execute(() -> { + mLastLoadPlan = loadPlan; + mLastLoadPlanId = requestId; + + if (callback != null) { + callback.accept(loadPlan); + } + }); + }); + return requestId; + } + + public void setPreloadTasksInBackground(boolean preloadTasksInBackground) { + mPreloadTasksInBackground = preloadTasksInBackground && !mIsLowRamDevice; + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + mTaskChangeId++; + } + + @Override + public void onActivityUnpinned() { + mTaskChangeId++; + } + + @Override + public void onTaskStackChanged() { + mTaskChangeId++; + + Preconditions.assertUIThread(); + if (mClearAssistCacheOnStackChange) { + mCachedAssistData.clear(); + } else { + mClearAssistCacheOnStackChange = true; + } + } + + @Override + public void onTaskStackChangedBackground() { + int userId = UserHandle.myUserId(); + if (!mPreloadTasksInBackground || !checkCurrentOrManagedUserId(userId, mContext)) { + // TODO: Only register this for the current user + return; + } + + // Preload a fixed number of task icons/thumbnails in the background + ActivityManager.RunningTaskInfo runningTaskInfo = + ActivityManagerWrapper.getInstance().getRunningTask(); + RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext); + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1; + launchOpts.numVisibleTasks = 2; + launchOpts.numVisibleTaskThumbnails = 2; + launchOpts.onlyLoadForCache = true; + launchOpts.onlyLoadPausedActivities = true; + launchOpts.loadThumbnails = true; + PreloadOptions preloadOpts = new PreloadOptions(); + preloadOpts.loadTitles = mAccessibilityManager.isEnabled(); + plan.preloadPlan(preloadOpts, mRecentsTaskLoader, -1, userId); + mRecentsTaskLoader.loadTasks(plan, launchOpts); + } + + public boolean isLoadPlanValid(int resultId) { + return mTaskChangeId == resultId; + } + + public RecentsTaskLoadPlan getLastLoadPlan() { + return mLastLoadPlan; + } + + public void setSystemUiProxy(ISystemUiProxy systemUiProxy) { + mSystemUiProxy = systemUiProxy; + } + + public ISystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + + public void onStart() { + mRecentsTaskLoader.startLoader(mContext); + } + + public void onTrimMemory(int level) { + if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + // We already stop the loader in UI_HIDDEN, so stop the high res loader as well + mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(false); + } + mRecentsTaskLoader.onTrimMemory(level); + } + + public void onOverviewShown(boolean fromHome, String tag) { + if (mSystemUiProxy == null) { + return; + } + try { + mSystemUiProxy.onOverviewShown(fromHome); + } catch (RemoteException e) { + Log.w(tag, + "Failed to notify SysUI of overview shown from " + (fromHome ? "home" : "app") + + ": ", e); + } + } + + public void resetAssistCache() { + mCachedAssistData.clear(); + } + + @WorkerThread + public void preloadAssistData(int taskId, Bundle data) { + mMainThreadExecutor.execute(() -> { + mCachedAssistData.put(taskId, data); + // We expect a stack change callback after the assist data is set. So ignore the + // very next stack change callback. + mClearAssistCacheOnStackChange = false; + + int count = mAssistDataListeners.size(); + for (int i = 0; i < count; i++) { + mAssistDataListeners.get(i).onAssistDataReceived(taskId); + } + }); + } + + public Bundle getAssistData(int taskId) { + Preconditions.assertUIThread(); + return mCachedAssistData.get(taskId); + } + + public void addAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.add(listener); + } + + public void removeAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.remove(listener); + } + + /** + * Callback for receiving assist data + */ + public interface AssistDataListener { + + void onAssistDataReceived(int taskId); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java new file mode 100644 index 0000000000000000000000000000000000000000..1385f927f36ea378230a1fabdccff350a9a612b2 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.os.RemoteException; +import android.util.Log; + +@FunctionalInterface +public interface RemoteRunnable { + + void run() throws RemoteException; + + static void executeSafely(RemoteRunnable r) { + try { + r.run(); + } catch (final RemoteException e) { + Log.e("RemoteRunnable", "Error calling remote method", e); + } + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c83dce63999c64593da148075b763aad5fd86a04 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; +import android.graphics.Matrix; +import android.support.annotation.AnyThread; +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +/** + * Factory class to create and add an overlays on the TaskView + */ +public class TaskOverlayFactory { + + private static TaskOverlayFactory sInstance; + + public static TaskOverlayFactory get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(TaskOverlayFactory.class, + context.getApplicationContext(), R.string.task_overlay_factory_class); + } + return sInstance; + } + + @AnyThread + public boolean needAssist() { + return false; + } + + public TaskOverlay createOverlay(View thumbnailView) { + return new TaskOverlay(); + } + + public static class TaskOverlay { + + public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) { } + + public void reset() { } + + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java new file mode 100644 index 0000000000000000000000000000000000000000..c6e98fc1579323d158ea5518e6716daedf78d711 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.InstantAppResolver; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; +import com.android.systemui.shared.recents.view.RecentsTransition; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; + +/** + * Represents a system shortcut that can be shown for a recent task. + */ +public class TaskSystemShortcut extends SystemShortcut { + + private static final String TAG = "TaskSystemShortcut"; + + protected T mSystemShortcut; + + protected TaskSystemShortcut(T systemShortcut) { + super(systemShortcut.iconResId, systemShortcut.labelResId); + mSystemShortcut = systemShortcut; + } + + protected TaskSystemShortcut(int iconResId, int labelResId) { + super(iconResId, labelResId); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, ItemInfo itemInfo) { + return null; + } + + public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) { + Task task = view.getTask(); + + ShortcutInfo dummyInfo = new ShortcutInfo(); + dummyInfo.intent = new Intent(); + ComponentName component = task.getTopComponent(); + dummyInfo.intent.setComponent(component); + dummyInfo.user = UserHandle.of(task.key.userId); + dummyInfo.title = TaskUtils.getTitle(activity, task); + + return getOnClickListenerForTask(activity, task, dummyInfo); + } + + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) { + return mSystemShortcut.getOnClickListener(activity, dummyInfo); + } + + public static class AppInfo extends TaskSystemShortcut { + public AppInfo() { + super(new SystemShortcut.AppInfo()); + } + } + + public static class SplitScreen extends TaskSystemShortcut { + + private Handler mHandler; + + public SplitScreen() { + super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + if (activity.getDeviceProfile().isMultiWindowMode) { + return null; + } + final Task task = taskView.getTask(); + final int taskId = task.key.id; + if (!task.isDockable) { + return null; + } + final RecentsView recentsView = activity.getOverviewPanel(); + + final TaskThumbnailView thumbnailView = taskView.getThumbnail(); + return (v -> { + final View.OnLayoutChangeListener onLayoutChangeListener = + new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int l, int t, int r, int b, + int oldL, int oldT, int oldR, int oldB) { + taskView.getRootView().removeOnLayoutChangeListener(this); + recentsView.removeIgnoreResetTask(taskView); + + // Start animating in the side pages once launcher has been resized + recentsView.dismissTask(taskView, false, false); + } + }; + + final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = + new DeviceProfile.OnDeviceProfileChangeListener() { + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + activity.removeOnDeviceProfileChangeListener(this); + if (dp.isMultiWindowMode) { + taskView.getRootView().addOnLayoutChangeListener( + onLayoutChangeListener); + } + } + }; + + dismissTaskMenuView(activity); + + final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(); + if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { + return; + } + boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; + if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, + ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft))) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + try { + sysUiProxy.onSplitScreenInvoked(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify SysUI of split screen: ", e); + return; + } + activity.getUserEventDispatcher().logActionOnControl(TAP, + LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET); + // Add a device profile change listener to kick off animating the side tasks + // once we enter multiwindow mode and relayout + activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener); + + final Runnable animStartedListener = () -> { + // Hide the task view and wait for the window to be resized + // TODO: Consider animating in launcher and do an in-place start activity + // afterwards + recentsView.addIgnoreResetTask(taskView); + taskView.setAlpha(0f); + }; + + final int[] position = new int[2]; + thumbnailView.getLocationOnScreen(position); + final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX()); + final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY()); + final Rect taskBounds = new Rect(position[0], position[1], + position[0] + width, position[1] + height); + + // Take the thumbnail of the task without a scrim and apply it back after + float alpha = thumbnailView.getDimAlpha(); + thumbnailView.setDimAlpha(0); + Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( + taskBounds.width(), taskBounds.height(), thumbnailView, 1f, + Color.BLACK); + thumbnailView.setDimAlpha(alpha); + + AppTransitionAnimationSpecsFuture future = + new AppTransitionAnimationSpecsFuture(mHandler) { + @Override + public List composeSpecs() { + return Collections.singletonList(new AppTransitionAnimationSpecCompat( + taskId, thumbnail, taskBounds)); + } + }; + WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( + future, animStartedListener, mHandler, true /* scaleUp */); + } + }); + } + } + + public static class Pin extends TaskSystemShortcut { + + private static final String TAG = Pin.class.getSimpleName(); + + private Handler mHandler; + + public Pin() { + super(R.drawable.ic_pin, R.string.recent_task_option_pin); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy == null) { + return null; + } + if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { + return null; + } + if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { + // We shouldn't be able to pin while an app is locked. + return null; + } + return view -> { + Consumer resultCallback = success -> { + if (success) { + try { + sysUiProxy.startScreenPinning(taskView.getTask().key.id); + } catch (RemoteException e) { + Log.w(TAG, "Failed to start screen pinning: ", e); + } + } else { + taskView.notifyTaskLaunchFailed(TAG); + } + }; + taskView.launchTask(true, resultCallback, mHandler); + dismissTaskMenuView(activity); + }; + } + } + + public static class Install extends TaskSystemShortcut { + public Install() { + super(new SystemShortcut.Install()); + } + + @Override + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo itemInfo) { + if (InstantAppResolver.newInstance(activity).isInstantApp(activity, + task.getTopComponent().getPackageName())) { + return mSystemShortcut.createOnClickListener(activity, itemInfo); + } + return null; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3b8ee22bca4622d8ef150375d9bbd87137a9218a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.animation.ValueAnimator; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.RectF; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; + +import java.util.List; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Contains helpful methods for retrieving data from {@link Task}s. + */ +public class TaskUtils { + + private static final String TAG = "TaskUtils"; + + /** + * TODO: remove this once we switch to getting the icon and label from IconCache. + */ + public static CharSequence getTitle(Context context, Task task) { + LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context); + UserManagerCompat userManagerCompat = UserManagerCompat.getInstance(context); + PackageManager packageManager = context.getPackageManager(); + UserHandle user = UserHandle.of(task.key.userId); + ApplicationInfo applicationInfo = launcherAppsCompat.getApplicationInfo( + task.getTopComponent().getPackageName(), 0, user); + if (applicationInfo == null) { + Log.e(TAG, "Failed to get title for task " + task); + return ""; + } + return userManagerCompat.getBadgedLabelForUser( + applicationInfo.loadLabel(packageManager), user); + } + + public static ComponentKey getLaunchComponentKeyForTask(Task.TaskKey taskKey) { + final ComponentName cn = taskKey.sourceComponent != null + ? taskKey.sourceComponent + : taskKey.getComponent(); + return new ComponentKey(cn, UserHandle.of(taskKey.userId)); + } + + + /** + * Try to find a TaskView that corresponds with the component of the launched view. + * + * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation. + * Otherwise, we will assume we are using a normal app transition, but it's possible that the + * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView. + */ + public static TaskView findTaskViewToLaunch( + BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) { + if (v instanceof TaskView) { + return (TaskView) v; + } + RecentsView recentsView = activity.getOverviewPanel(); + + // It's possible that the launched view can still be resolved to a visible task view, check + // the task id of the opening task and see if we can find a match. + if (v.getTag() instanceof ItemInfo) { + ItemInfo itemInfo = (ItemInfo) v.getTag(); + ComponentName componentName = itemInfo.getTargetComponent(); + int userId = itemInfo.user.getIdentifier(); + if (componentName != null) { + for (int i = 0; i < recentsView.getTaskViewCount(); i++) { + TaskView taskView = recentsView.getTaskViewAt(i); + if (recentsView.isTaskViewVisible(taskView)) { + Task.TaskKey key = taskView.getTask().key; + if (componentName.equals(key.getComponent()) && userId == key.userId) { + return taskView; + } + } + } + } + } + + if (targets == null) { + return null; + } + // Resolve the opening task id + int openingTaskId = -1; + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + openingTaskId = target.taskId; + break; + } + } + + // If there is no opening task id, fall back to the normal app icon launch animation + if (openingTaskId == -1) { + return null; + } + + // If the opening task id is not currently visible in overview, then fall back to normal app + // icon launch animation + TaskView taskView = recentsView.getTaskView(openingTaskId); + if (taskView == null || !recentsView.isTaskViewVisible(taskView)) { + return null; + } + return taskView; + } + + /** + * @return Animator that controls the window of the opening targets for the recents launch + * animation. + */ + public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges, + RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) { + SyncRtSurfaceTransactionApplier syncTransactionApplier = + new SyncRtSurfaceTransactionApplier(v); + final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + + // Defer fading out the view until after the app window gets faded in + final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR); + final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR); + + final RemoteAnimationTargetSet mTargetSet; + + final RectF mThumbnailRect; + + { + mTargetSet = new RemoteAnimationTargetSet(targets, MODE_OPENING); + inOutHelper.setTaskAlphaCallback((t, alpha) -> mTaskAlpha.value); + + inOutHelper.prepareAnimation(true /* isOpening */); + inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), + mTargetSet.apps.length == 0 ? null : mTargetSet.apps[0]); + + mThumbnailRect = new RectF(inOutHelper.getTargetRect()); + mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY()); + Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX()); + } + + @Override + public void onUpdate(float percent) { + RectF taskBounds = inOutHelper.applyTransform(mTargetSet, 1 - percent, + syncTransactionApplier); + if (!skipViewChanges) { + float scale = taskBounds.width() / mThumbnailRect.width(); + v.setScaleX(scale); + v.setScaleY(scale); + v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX()); + v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY()); + v.setAlpha(mViewAlpha.value); + } + } + }); + return appAnimator; + } + + public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets, + int taskId, int mode) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == mode && target.taskId == taskId) { + return true; + } + } + return false; + } + + public static boolean checkCurrentOrManagedUserId(int currentUserId, Context context) { + if (currentUserId == UserHandle.myUserId()) { + return true; + } + List allUsers = UserManagerCompat.getInstance(context).getUserProfiles(); + for (int i = allUsers.size() - 1; i >= 0; i--) { + if (currentUserId == allUsers.get(i).getIdentifier()) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..faeb20527913787b9c70e0fc4a08809dd43d16d8 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.IntDef; +import android.view.Choreographer; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; + +@TargetApi(Build.VERSION_CODES.O) +@FunctionalInterface +public interface TouchConsumer extends Consumer { + + @IntDef(flag = true, value = { + INTERACTION_NORMAL, + INTERACTION_QUICK_SCRUB + }) + @Retention(RetentionPolicy.SOURCE) + @interface InteractionType {} + int INTERACTION_NORMAL = 0; + int INTERACTION_QUICK_SCRUB = 1; + + default void reset() { } + + default void updateTouchTracking(@InteractionType int interactionType) { } + + default void onQuickScrubEnd() { } + + default void onQuickScrubProgress(float progress) { } + + default void onQuickStep(MotionEvent ev) { } + + default void onCommand(int command) { } + + /** + * Called on the binder thread to allow the consumer to process the motion event before it is + * posted on a handler thread. + */ + default void preProcessMotionEvent(MotionEvent ev) { } + + default Choreographer getIntrimChoreographer(MotionEventQueue queue) { + return null; + } + + default void deferInit() { } + + default boolean deferNextEventToMainThread() { + return false; + } + + default boolean forceToLauncherConsumer() { + return false; + } + + default void onShowOverviewFromAltTab() {} +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java new file mode 100644 index 0000000000000000000000000000000000000000..b44cfb6b7e8a2d0bb4b37743aca861906d679b50 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.Service; +import android.content.Intent; +import android.graphics.PointF; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.IOverviewProxy; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.ChoreographerCompat; +import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +/** + * Service connected by system-UI for handling touch interaction. + */ +@TargetApi(Build.VERSION_CODES.O) +public class TouchInteractionService extends Service { + + private static final SparseArray sMotionEventNames; + + static { + sMotionEventNames = new SparseArray<>(3); + sMotionEventNames.put(ACTION_DOWN, "ACTION_DOWN"); + sMotionEventNames.put(ACTION_UP, "ACTION_UP"); + sMotionEventNames.put(ACTION_CANCEL, "ACTION_CANCEL"); + } + + public static final int EDGE_NAV_BAR = 1 << 8; + + private static final String TAG = "TouchInteractionService"; + + /** + * A background thread used for handling UI for another window. + */ + private static HandlerThread sRemoteUiThread; + + private final IBinder mMyBinder = new IOverviewProxy.Stub() { + + @Override + public void onPreMotionEvent(@HitTarget int downHitTarget) throws RemoteException { + TraceHelper.beginSection("SysUiBinder"); + setupTouchConsumer(downHitTarget); + TraceHelper.partitionSection("SysUiBinder", "Down target " + downHitTarget); + } + + @Override + public void onMotionEvent(MotionEvent ev) { + mEventQueue.queue(ev); + + String name = sMotionEventNames.get(ev.getActionMasked()); + if (name != null){ + TraceHelper.partitionSection("SysUiBinder", name); + } + } + + @Override + public void onBind(ISystemUiProxy iSystemUiProxy) { + mISystemUiProxy = iSystemUiProxy; + mRecentsModel.setSystemUiProxy(mISystemUiProxy); + mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy); + } + + @Override + public void onQuickScrubStart() { + mEventQueue.onQuickScrubStart(); + TraceHelper.partitionSection("SysUiBinder", "onQuickScrubStart"); + } + + @Override + public void onQuickScrubProgress(float progress) { + mEventQueue.onQuickScrubProgress(progress); + } + + @Override + public void onQuickScrubEnd() { + mEventQueue.onQuickScrubEnd(); + TraceHelper.endSection("SysUiBinder", "onQuickScrubEnd"); + } + + @Override + public void onOverviewToggle() { + mOverviewCommandHelper.onOverviewToggle(); + } + + @Override + public void onOverviewShown(boolean triggeredFromAltTab) { + if (triggeredFromAltTab) { + setupTouchConsumer(HIT_TARGET_NONE); + mEventQueue.onOverviewShownFromAltTab(); + } else { + mOverviewCommandHelper.onOverviewShown(); + } + } + + @Override + public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + if (triggeredFromAltTab && !triggeredFromHomeKey) { + // onOverviewShownFromAltTab initiates quick scrub. Ending it here. + mEventQueue.onQuickScrubEnd(); + } + } + + @Override + public void onQuickStep(MotionEvent motionEvent) { + mEventQueue.onQuickStep(motionEvent); + TraceHelper.endSection("SysUiBinder", "onQuickStep"); + + } + + @Override + public void onTip(int actionType, int viewType) { + mOverviewCommandHelper.onTip(actionType, viewType); + } + }; + + private final TouchConsumer mNoOpTouchConsumer = (ev) -> {}; + + private static boolean sConnected = false; + + public static boolean isConnected() { + return sConnected; + } + + private ActivityManagerWrapper mAM; + private RecentsModel mRecentsModel; + private MotionEventQueue mEventQueue; + private MainThreadExecutor mMainThreadExecutor; + private ISystemUiProxy mISystemUiProxy; + private OverviewCommandHelper mOverviewCommandHelper; + private OverviewInteractionState mOverviewInteractionState; + private OverviewCallbacks mOverviewCallbacks; + private TaskOverlayFactory mTaskOverlayFactory; + + private Choreographer mMainThreadChoreographer; + private Choreographer mBackgroundThreadChoreographer; + + @Override + public void onCreate() { + super.onCreate(); + mAM = ActivityManagerWrapper.getInstance(); + mRecentsModel = RecentsModel.getInstance(this); + mRecentsModel.setPreloadTasksInBackground(true); + mMainThreadExecutor = new MainThreadExecutor(); + mOverviewCommandHelper = new OverviewCommandHelper(this); + mMainThreadChoreographer = Choreographer.getInstance(); + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer); + mOverviewInteractionState = OverviewInteractionState.getInstance(this); + mOverviewCallbacks = OverviewCallbacks.get(this); + mTaskOverlayFactory = TaskOverlayFactory.get(this); + + sConnected = true; + + // Temporarily disable model preload + // new ModelPreload().start(this); + initBackgroundChoreographer(); + } + + @Override + public void onDestroy() { + mOverviewCommandHelper.onDestroy(); + sConnected = false; + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "Touch service connected"); + return mMyBinder; + } + + private void setupTouchConsumer(@HitTarget int downHitTarget) { + mEventQueue.reset(); + TouchConsumer oldConsumer = mEventQueue.getConsumer(); + if (oldConsumer.deferNextEventToMainThread()) { + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, + new DeferredTouchConsumer((v) -> getCurrentTouchConsumer(downHitTarget, + oldConsumer.forceToLauncherConsumer(), v))); + mEventQueue.deferInit(); + } else { + mEventQueue = new MotionEventQueue( + mMainThreadChoreographer, getCurrentTouchConsumer(downHitTarget, false, null)); + } + } + + private TouchConsumer getCurrentTouchConsumer( + @HitTarget int downHitTarget, boolean forceToLauncher, VelocityTracker tracker) { + RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0); + + if (runningTaskInfo == null && !forceToLauncher) { + return mNoOpTouchConsumer; + } else if (forceToLauncher || + runningTaskInfo.topActivity.equals(mOverviewCommandHelper.overviewComponent)) { + return getOverviewConsumer(); + } else { + if (tracker == null) { + tracker = VelocityTracker.obtain(); + } + return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel, + mOverviewCommandHelper.overviewIntent, + mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor, + mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks, + mTaskOverlayFactory, tracker); + } + } + + private TouchConsumer getOverviewConsumer() { + ActivityControlHelper activityHelper = mOverviewCommandHelper.getActivityControlHelper(); + BaseDraggingActivity activity = activityHelper.getCreatedActivity(); + if (activity == null) { + return mNoOpTouchConsumer; + } + return new OverviewTouchConsumer(activityHelper, activity); + } + + private static class OverviewTouchConsumer + implements TouchConsumer { + + private final ActivityControlHelper mActivityHelper; + private final T mActivity; + private final BaseDragLayer mTarget; + private final int[] mLocationOnScreen = new int[2]; + private final PointF mDownPos = new PointF(); + private final int mTouchSlop; + private final QuickScrubController mQuickScrubController; + + private boolean mTrackingStarted = false; + private boolean mInvalidated = false; + + private float mLastProgress = 0; + private boolean mStartPending = false; + private boolean mEndPending = false; + + OverviewTouchConsumer(ActivityControlHelper activityHelper, T activity) { + mActivityHelper = activityHelper; + mActivity = activity; + mTarget = activity.getDragLayer(); + mTouchSlop = ViewConfiguration.get(mTarget.getContext()).getScaledTouchSlop(); + + mQuickScrubController = mActivity.getOverviewPanel() + .getQuickScrubController(); + } + + @Override + public void accept(MotionEvent ev) { + if (mInvalidated) { + return; + } + int action = ev.getActionMasked(); + if (action == ACTION_DOWN) { + mTrackingStarted = false; + mDownPos.set(ev.getX(), ev.getY()); + } else if (!mTrackingStarted) { + switch (action) { + case ACTION_POINTER_UP: + case ACTION_POINTER_DOWN: + if (!mTrackingStarted) { + mInvalidated = true; + } + break; + case ACTION_MOVE: { + float displacement = ev.getY() - mDownPos.y; + if (Math.abs(displacement) >= mTouchSlop) { + mTarget.getLocationOnScreen(mLocationOnScreen); + + // Send a down event only when mTouchSlop is crossed. + MotionEvent down = MotionEvent.obtain(ev); + down.setAction(ACTION_DOWN); + sendEvent(down); + down.recycle(); + mTrackingStarted = true; + } + } + } + } + + if (mTrackingStarted) { + sendEvent(ev); + } + + if (action == ACTION_UP || action == ACTION_CANCEL) { + mInvalidated = true; + } + } + + private void sendEvent(MotionEvent ev) { + int flags = ev.getEdgeFlags(); + ev.setEdgeFlags(flags | EDGE_NAV_BAR); + ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); + if (!mTrackingStarted) { + mTarget.onInterceptTouchEvent(ev); + } + mTarget.onTouchEvent(ev); + ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]); + ev.setEdgeFlags(flags); + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mInvalidated) { + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + } + + @Override + public void updateTouchTracking(int interactionType) { + if (mInvalidated) { + return; + } + if (interactionType == INTERACTION_QUICK_SCRUB) { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + mStartPending = true; + Runnable action = () -> { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + mActivityHelper.onQuickInteractionStart(mActivity, null, true); + mQuickScrubController.onQuickScrubProgress(mLastProgress); + mStartPending = false; + + if (mEndPending) { + mQuickScrubController.onQuickScrubEnd(); + mEndPending = false; + } + }; + + mActivityHelper.executeOnWindowAvailable(mActivity, action); + } + } + + @Override + public void onQuickScrubEnd() { + if (mInvalidated) { + return; + } + if (mStartPending) { + mEndPending = true; + } else { + mQuickScrubController.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + mLastProgress = progress; + if (mInvalidated || mStartPending) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + } + + private void initBackgroundChoreographer() { + if (sRemoteUiThread == null) { + sRemoteUiThread = new HandlerThread("remote-ui"); + sRemoteUiThread.start(); + } + new Handler(sRemoteUiThread.getLooper()).post(() -> + mBackgroundThreadChoreographer = ChoreographerCompat.getSfInstance()); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0004a11f58bde64bdc9370ed8b45f996c970a425 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java @@ -0,0 +1,1102 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.UserHandle; +import android.support.annotation.AnyThread; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.view.ViewTreeObserver.OnDrawListener; +import android.view.WindowManager; +import android.view.animation.Interpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.WindowCallbacksCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.StringJoiner; +import java.util.function.BiFunction; + +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; +import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; +import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; + +@TargetApi(Build.VERSION_CODES.O) +public class WindowTransformSwipeHandler { + private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); + private static final boolean DEBUG_STATES = false; + + // Launcher UI related states + private static final int STATE_LAUNCHER_PRESENT = 1 << 0; + private static final int STATE_LAUNCHER_STARTED = 1 << 1; + private static final int STATE_LAUNCHER_DRAWN = 1 << 2; + private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 3; + + // Internal initialization states + private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4; + + // Interaction finish states + private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5; + private static final int STATE_SCALED_CONTROLLER_APP = 1 << 6; + + private static final int STATE_HANDLER_INVALIDATED = 1 << 7; + private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8; + private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 9; + private static final int STATE_GESTURE_CANCELLED = 1 << 10; + private static final int STATE_GESTURE_COMPLETED = 1 << 11; + + // States for quick switch/scrub + private static final int STATE_CURRENT_TASK_FINISHED = 1 << 12; + private static final int STATE_QUICK_SCRUB_START = 1 << 13; + private static final int STATE_QUICK_SCRUB_END = 1 << 14; + + private static final int STATE_CAPTURE_SCREENSHOT = 1 << 15; + private static final int STATE_SCREENSHOT_CAPTURED = 1 << 16; + + private static final int STATE_RESUME_LAST_TASK = 1 << 17; + private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 18; + + private static final int LAUNCHER_UI_STATES = + STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_LAUNCHER_STARTED; + + private static final int LONG_SWIPE_ENTER_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED; + + private static final int LONG_SWIPE_START_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; + + // For debugging, keep in sync with above states + private static final String[] STATES = new String[] { + "STATE_LAUNCHER_PRESENT", + "STATE_LAUNCHER_STARTED", + "STATE_LAUNCHER_DRAWN", + "STATE_ACTIVITY_MULTIPLIER_COMPLETE", + "STATE_APP_CONTROLLER_RECEIVED", + "STATE_SCALED_CONTROLLER_RECENTS", + "STATE_SCALED_CONTROLLER_APP", + "STATE_HANDLER_INVALIDATED", + "STATE_GESTURE_STARTED_QUICKSTEP", + "STATE_GESTURE_STARTED_QUICKSCRUB", + "STATE_GESTURE_CANCELLED", + "STATE_GESTURE_COMPLETED", + "STATE_CURRENT_TASK_FINISHED", + "STATE_QUICK_SCRUB_START", + "STATE_QUICK_SCRUB_END", + "STATE_CAPTURE_SCREENSHOT", + "STATE_SCREENSHOT_CAPTURED", + "STATE_RESUME_LAST_TASK", + "STATE_ASSIST_DATA_RECEIVED", + }; + + public static final long MAX_SWIPE_DURATION = 350; + public static final long MIN_SWIPE_DURATION = 80; + public static final long MIN_OVERSHOOT_DURATION = 120; + + public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f; + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); + + private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper(); + + protected Runnable mGestureEndCallback; + protected boolean mIsGoingToHome; + private DeviceProfile mDp; + private int mTransitionDragLength; + + // Shift in the range of [0, 1]. + // 0 => preview snapShot is completely visible, and hotseat is completely translated down + // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely + // visible. + private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + + // An increasing identifier per single instance of OtherActivityTouchConsumer. Generally one + // instance of OtherActivityTouchConsumer will only have one swipe handle, but sometimes we can + // end up with multiple handlers if we get recents command in the middle of a swipe gesture. + // This is used to match the corresponding activity manager callbacks in + // OtherActivityTouchConsumer + public final int id; + private final Context mContext; + private final ActivityControlHelper mActivityControlHelper; + private final ActivityInitListener mActivityInitListener; + + private final int mRunningTaskId; + private final RunningTaskInfo mRunningTaskInfo; + private ThumbnailData mTaskSnapshot; + + private MultiStateCallback mStateCallback; + private AnimatorPlaybackController mLauncherTransitionController; + + private T mActivity; + private LayoutListener mLayoutListener; + private RecentsView mRecentsView; + private SyncRtSurfaceTransactionApplier mSyncTransactionApplier; + private QuickScrubController mQuickScrubController; + private AnimationFactory mAnimationFactory = (t, i) -> { }; + + private Runnable mLauncherDrawnCallback; + + private boolean mWasLauncherAlreadyVisible; + + private boolean mPassedOverviewThreshold; + private boolean mGestureStarted; + private int mLogAction = Touch.SWIPE; + private float mCurrentQuickScrubProgress; + private boolean mQuickScrubBlocked; + + private @InteractionType int mInteractionType = INTERACTION_NORMAL; + + private InputConsumerController mInputConsumer = + InputConsumerController.getRecentsAnimationInputConsumer(); + + private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper(); + + private final long mTouchTimeMs; + private long mLauncherFrameDrawnTime; + + private boolean mBgLongSwipeMode = false; + private boolean mUiLongSwipeMode = false; + private float mLongSwipeDisplacement = 0; + private LongSwipeHelper mLongSwipeController; + + private Bundle mAssistData; + + WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, + long touchTimeMs, ActivityControlHelper controller) { + this.id = id; + mContext = context; + mRunningTaskInfo = runningTaskInfo; + mRunningTaskId = runningTaskInfo.id; + mTouchTimeMs = touchTimeMs; + mActivityControlHelper = controller; + mActivityInitListener = mActivityControlHelper + .createActivityInitListener(this::onActivityInit); + + initStateCallbacks(); + // Register the input consumer on the UI thread, to ensure that it runs after any pending + // unregister calls + executeOnUiThread(mInputConsumer::registerInputConsumer); + } + + private void initStateCallbacks() { + mStateCallback = new MultiStateCallback() { + @Override + public void setState(int stateFlag) { + debugNewState(stateFlag); + super.setState(stateFlag); + } + }; + + mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSCRUB, + this::initializeLauncherAnimationController); + mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSTEP, + this::initializeLauncherAnimationController); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, + this::launcherFrameDrawn); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSTEP, + this::notifyGestureStartedAsync); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSCRUB, + this::notifyGestureStartedAsync); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED + | STATE_GESTURE_CANCELLED, + this::resetStateForAnimationCancel); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, + this::sendRemoteAnimationsToAnimationFactory); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP, + this::resumeLastTaskForQuickstep); + mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, + this::resumeLastTask); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_CAPTURE_SCREENSHOT, + this::switchToScreenshot); + + mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED + | STATE_SCALED_CONTROLLER_RECENTS, + this::finishCurrentTransitionToHome); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS + | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED + | STATE_GESTURE_STARTED_QUICKSTEP, + this::setupLauncherUiAfterSwipeUpAnimation); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS + | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED + | STATE_GESTURE_STARTED_QUICKSTEP | STATE_ASSIST_DATA_RECEIVED, + this::preloadAssistData); + + mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, + this::invalidateHandlerWithLauncher); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED + | STATE_SCALED_CONTROLLER_APP, + this::notifyTransitionCancelled); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_APP_CONTROLLER_RECEIVED, this::onQuickScrubStart); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED + | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); + + mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); + mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); + } + + private void executeOnUiThread(Runnable action) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + action.run(); + } else { + postAsyncCallback(mMainThreadHandler, action); + } + } + + private void setStateOnUiThread(int stateFlag) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + mStateCallback.setState(stateFlag); + } else { + postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); + } + } + + private void initTransitionEndpoints(DeviceProfile dp) { + mDp = dp; + + TransformedRect tempRect = new TransformedRect(); + mTransitionDragLength = mActivityControlHelper + .getSwipeUpDestinationAndLength(dp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + } + + private long getFadeInDuration() { + if (mCurrentShift.getCurrentAnimation() != null) { + ObjectAnimator anim = mCurrentShift.getCurrentAnimation(); + long theirDuration = anim.getDuration() - anim.getCurrentPlayTime(); + + // TODO: Find a better heuristic + return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION)); + } else { + return MAX_SWIPE_DURATION; + } + } + + public void initWhenReady() { + mActivityInitListener.register(); + } + + private boolean onActivityInit(final T activity, Boolean alreadyOnHome) { + if (mActivity == activity) { + return true; + } + if (mActivity != null) { + // The launcher may have been recreated as a result of device rotation. + int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; + initStateCallbacks(); + mStateCallback.setState(oldState); + mLayoutListener.setHandler(null); + } + mWasLauncherAlreadyVisible = alreadyOnHome; + mActivity = activity; + // Override the visibility of the activity until the gesture actually starts and we swipe + // up, or until we transition home and the home animation is composed + if (alreadyOnHome) { + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } else { + mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + + mRecentsView = activity.getOverviewPanel(); + mSyncTransactionApplier = new SyncRtSurfaceTransactionApplier(mRecentsView); + mQuickScrubController = mRecentsView.getQuickScrubController(); + mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity); + + mStateCallback.setState(STATE_LAUNCHER_PRESENT); + if (alreadyOnHome) { + onLauncherStart(activity); + } else { + activity.setOnStartCallback(this::onLauncherStart); + } + return true; + } + + private void onLauncherStart(final T activity) { + if (mActivity != activity) { + return; + } + if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { + return; + } + + mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, + mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated); + AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); + + if (mWasLauncherAlreadyVisible) { + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); + } else { + TraceHelper.beginSection("WTS-init"); + View dragLayer = activity.getDragLayer(); + mActivityControlHelper.getAlphaProperty(activity).setValue(0); + dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { + + @Override + public void onDraw() { + TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); + dragLayer.post(() -> + dragLayer.getViewTreeObserver().removeOnDrawListener(this)); + if (activity != mActivity) { + return; + } + + mStateCallback.setState(STATE_LAUNCHER_DRAWN); + } + }); + } + + mRecentsView.showTask(mRunningTaskId); + mRecentsView.setRunningTaskHidden(true); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + mLayoutListener.open(); + mStateCallback.setState(STATE_LAUNCHER_STARTED); + } + + public void setLauncherOnDrawCallback(Runnable callback) { + mLauncherDrawnCallback = callback; + } + + private void launcherFrameDrawn() { + AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); + if (property.getValue() < 1) { + if (mGestureStarted) { + final MultiStateCallback callback = mStateCallback; + ObjectAnimator animator = ObjectAnimator.ofFloat( + property, MultiValueAlpha.VALUE, 1); + animator.setDuration(getFadeInDuration()).addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + }); + animator.start(); + } else { + property.setValue(1); + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + } + if (mLauncherDrawnCallback != null) { + mLauncherDrawnCallback.run(); + } + mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); + } + + private void sendRemoteAnimationsToAnimationFactory() { + mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); + } + + private void initializeLauncherAnimationController() { + mLayoutListener.setHandler(this); + buildAnimationController(); + + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); + } + + // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/ + // STATE_GESTURE_STARTED_QUICKSCRUB is set, so we can enable the high-res thumbnail loader + // here once we are sure that we will end up in an overview state + RecentsModel.getInstance(mContext).getRecentsTaskLoader() + .getHighResThumbnailLoader().setVisible(true); + } + + public void updateInteractionType(@InteractionType int interactionType) { + if (mInteractionType != INTERACTION_NORMAL) { + throw new IllegalArgumentException( + "Can't change interaction type from " + mInteractionType); + } + if (interactionType != INTERACTION_QUICK_SCRUB) { + throw new IllegalArgumentException( + "Can't change interaction type to " + interactionType); + } + mInteractionType = interactionType; + mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub); + + setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); + + // Start the window animation without waiting for launcher. + animateToProgress(mCurrentShift.value, 1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR, + true /* goingToHome */); + } + + private void shiftAnimationDestinationForQuickscrub() { + TransformedRect tempRect = new TransformedRect(); + mActivityControlHelper + .getSwipeUpDestinationAndLength(mDp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + + float offsetY = + mActivityControlHelper.getTranslationYForQuickScrub(tempRect, mDp, mContext); + float scale, offsetX; + Resources res = mContext.getResources(); + + if (ActivityManagerWrapper.getInstance().getRecentTasks(2, UserHandle.myUserId()).size() + < 2) { + // There are not enough tasks, we don't need to shift + offsetX = 0; + scale = 1; + } else { + offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing) + + tempRect.rect.width(); + float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 + + res.getDimensionPixelSize(R.dimen.recents_page_spacing); + float interpolation = Math.min(1, offsetX / distanceToReachEdge); + scale = TaskView.getCurveScaleForInterpolation(interpolation); + } + mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY, + QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); + } + + @WorkerThread + public void updateDisplacement(float displacement) { + // We are moving in the negative x/y direction + displacement = -displacement; + if (displacement > mTransitionDragLength) { + mCurrentShift.updateValue(1); + + if (!mBgLongSwipeMode) { + mBgLongSwipeMode = true; + executeOnUiThread(this::onLongSwipeEnabledUi); + } + mLongSwipeDisplacement = displacement - mTransitionDragLength; + executeOnUiThread(this::onLongSwipeDisplacementUpdated); + } else { + if (mBgLongSwipeMode) { + mBgLongSwipeMode = false; + executeOnUiThread(this::onLongSwipeDisabledUi); + } + float translation = Math.max(displacement, 0); + float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; + mCurrentShift.updateValue(shift); + } + } + + /** + * Called by {@link #mLayoutListener} when launcher layout changes + */ + public void buildAnimationController() { + initTransitionEndpoints(mActivity.getDeviceProfile()); + mAnimationFactory.createActivityController(mTransitionDragLength, mInteractionType); + } + + private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { + mLauncherTransitionController = anim; + mLauncherTransitionController.dispatchOnStart(); + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + @WorkerThread + private void updateFinalShift() { + float shift = mCurrentShift.value; + + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + + mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, shift, + Looper.myLooper() == mMainThreadHandler.getLooper() + ? mSyncTransactionApplier + : null); + + boolean passedThreshold = shift > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; + mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); + if (mActivityControlHelper.shouldMinimizeSplitScreen()) { + mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); + } + } + + executeOnUiThread(this::updateFinalShiftUi); + } + + private void updateFinalShiftUi() { + final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; + if (passed != mPassedOverviewThreshold) { + mPassedOverviewThreshold = passed; + if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null) { + mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + } + + if (mLauncherTransitionController == null || mLauncherTransitionController + .getAnimationPlayer().isStarted()) { + return; + } + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, + RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + InvariantDeviceProfile idp = appState == null ? + new InvariantDeviceProfile(mContext) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(mContext); + final Rect overviewStackBounds; + RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); + + if (minimizedHomeBounds != null && runningTaskTarget != null) { + overviewStackBounds = mActivityControlHelper + .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); + dp = dp.getMultiWindowProfile(mContext, + new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); + dp.updateInsets(homeContentInsets); + } else { + if (mActivity != null) { + int loc[] = new int[2]; + View rootView = mActivity.getRootView(); + rootView.getLocationOnScreen(loc); + overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), + loc[1] + rootView.getHeight()); + } else { + overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); + } + // If we are not in multi-window mode, home insets should be same as system insets. + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + dp = dp.copy(mContext); + dp.updateInsets(insets); + } + dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); + + if (runningTaskTarget != null) { + mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); + } + mClipAnimationHelper.prepareAnimation(false /* isOpening */); + initTransitionEndpoints(dp); + + mRecentsAnimationWrapper.setController(controller, targets); + setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); + + mPassedOverviewThreshold = false; + } + + public void onRecentsAnimationCanceled() { + mRecentsAnimationWrapper.setController(null, null); + mActivityInitListener.unregister(); + setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); + } + + public void onGestureStarted() { + notifyGestureStartedAsync(); + setStateOnUiThread(mInteractionType == INTERACTION_NORMAL + ? STATE_GESTURE_STARTED_QUICKSTEP : STATE_GESTURE_STARTED_QUICKSCRUB); + mGestureStarted = true; + mRecentsAnimationWrapper.hideCurrentInputMethod(); + mRecentsAnimationWrapper.enableInputConsumer(); + } + + /** + * Notifies the launcher that the swipe gesture has started. This can be called multiple times + * on both background and UI threads + */ + @AnyThread + private void notifyGestureStartedAsync() { + final T curActivity = mActivity; + if (curActivity != null) { + // Once the gesture starts, we can no longer transition home through the button, so + // reset the force override of the activity visibility + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + } + + @WorkerThread + public void onGestureEnded(float endVelocity) { + float flingThreshold = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_threshold_velocity); + boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; + setStateOnUiThread(STATE_GESTURE_COMPLETED); + + mLogAction = isFling ? Touch.FLING : Touch.SWIPE; + + if (mBgLongSwipeMode) { + executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling)); + } else { + handleNormalGestureEnd(endVelocity, isFling); + } + } + + private void handleNormalGestureEnd(float endVelocity, boolean isFling) { + float velocityPxPerMs = endVelocity / 1000; + long duration = MAX_SWIPE_DURATION; + float currentShift = mCurrentShift.value; + final boolean goingToHome; + float endShift; + final float startShift; + Interpolator interpolator = DEACCEL; + if (!isFling) { + goingToHome = currentShift >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted; + endShift = goingToHome ? 1 : 0; + long expectedDuration = Math.abs(Math.round((endShift - currentShift) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + startShift = currentShift; + interpolator = goingToHome ? OVERSHOOT_1_2 : DEACCEL; + } else { + goingToHome = endVelocity < 0; + endShift = goingToHome ? 1 : 0; + startShift = Utilities.boundToRange(currentShift - velocityPxPerMs + * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1); + float minFlingVelocity = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { + if (goingToHome) { + Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( + startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength); + endShift = overshoot.end; + interpolator = overshoot.interpolator; + duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, + MAX_SWIPE_DURATION); + } else { + float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; + + // we want the page's snap velocity to approximately match the velocity at + // which the user flings, so we scale the duration by a value near to the + // derivative of the scroll interpolator at zero, ie. 2. + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + } + animateToProgress(startShift, endShift, duration, interpolator, goingToHome); + } + + private void doLogGesture(boolean toLauncher) { + DeviceProfile dp = mDp; + if (dp == null) { + // We probably never received an animation controller, skip logging. + return; + } + final int direction; + if (dp.isVerticalBarLayout()) { + direction = (dp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT; + } else { + direction = toLauncher ? Direction.UP : Direction.DOWN; + } + + int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP; + UserEventDispatcher.newInstance(mContext, dp).logStateChangeAction( + mLogAction, direction, + ContainerType.NAVBAR, ContainerType.APP, + dstContainerType, + 0); + } + + /** Animates to the given progress, where 0 is the current app and 1 is overview. */ + private void animateToProgress(float start, float end, long duration, + Interpolator interpolator, boolean goingToHome) { + mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, + interpolator, goingToHome)); + } + + private void animateToProgressInternal(float start, float end, long duration, + Interpolator interpolator, boolean goingToHome) { + mIsGoingToHome = goingToHome; + ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration); + anim.setInterpolator(interpolator); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + setStateOnUiThread(mIsGoingToHome + ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT) + : STATE_SCALED_CONTROLLER_APP); + } + }); + anim.start(); + long startMillis = SystemClock.uptimeMillis(); + executeOnUiThread(() -> { + // Animate the launcher components at the same time as the window, always on UI thread. + if (mLauncherTransitionController != null && !mWasLauncherAlreadyVisible + && start != end && duration > 0) { + // Adjust start progress and duration in case we are on a different thread. + long elapsedMillis = SystemClock.uptimeMillis() - startMillis; + elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration); + float elapsedProgress = (float) elapsedMillis / duration; + float adjustedStart = Utilities.mapRange(elapsedProgress, start, end); + long adjustedDuration = duration - elapsedMillis; + // We want to use the same interpolator as the window, but need to adjust it to + // interpolate over the remaining progress (end - start). + mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress( + interpolator, adjustedStart, end)); + mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration); + mLauncherTransitionController.getAnimationPlayer().start(); + } + }); + } + + @UiThread + private void resumeLastTaskForQuickstep() { + setStateOnUiThread(STATE_RESUME_LAST_TASK); + doLogGesture(false /* toLauncher */); + reset(); + } + + @UiThread + private void resumeLastTask() { + mRecentsAnimationWrapper.finish(false /* toHome */, null); + } + + public void reset() { + if (mInteractionType != INTERACTION_QUICK_SCRUB) { + // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be + // invalidated after the quick scrub ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + } + + private void invalidateHandler() { + mCurrentShift.finishAnimation(); + + if (mGestureEndCallback != null) { + mGestureEndCallback.run(); + } + + mActivityInitListener.unregister(); + mInputConsumer.unregisterInputConsumer(); + mTaskSnapshot = null; + } + + private void invalidateHandlerWithLauncher() { + mLauncherTransitionController = null; + mLayoutListener.finish(); + mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); + + mRecentsView.setRunningTaskHidden(false); + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, false /* animate */); + mQuickScrubController.cancelActiveQuickscrub(); + } + + private void notifyTransitionCancelled() { + mAnimationFactory.onTransitionCancelled(); + } + + private void resetStateForAnimationCancel() { + boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; + mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); + + // Leave the pending invisible flag, as it may be used by wallpaper open animation. + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } + + public void layoutListenerClosed() { + if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) { + mLauncherTransitionController.setPlayFraction(1); + } + } + + private void switchToScreenshot() { + boolean finishTransitionPosted = false; + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + // Update the screenshot of the task + if (mTaskSnapshot == null) { + mTaskSnapshot = controller.screenshotTask(mRunningTaskId); + } + TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); + mRecentsView.setRunningTaskHidden(false); + if (taskView != null) { + // Defer finishing the animation until the next launcher frame with the + // new thumbnail + finishTransitionPosted = new WindowCallbacksCompat(taskView) { + + // The number of frames to defer until we actually finish the animation + private int mDeferFrameCount = 2; + + @Override + public void onPostDraw(Canvas canvas) { + if (mDeferFrameCount > 0) { + mDeferFrameCount--; + // Workaround, detach and reattach to invalidate the root node for + // another draw + detach(); + attach(); + taskView.invalidate(); + return; + } + + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + detach(); + } + }.attach(); + } + } + if (!finishTransitionPosted) { + // If we haven't posted a draw callback, set the state immediately. + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + } + } + + private void finishCurrentTransitionToHome() { + synchronized (mRecentsAnimationWrapper) { + mRecentsAnimationWrapper.finish(true /* toHome */, + () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); + } + } + + private void setupLauncherUiAfterSwipeUpAnimation() { + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + mActivityControlHelper.onSwipeUpComplete(mActivity); + + // Animate the first icon. + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); + mRecentsView.setSwipeDownShouldLaunchApp(true); + + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + + doLogGesture(true /* toLauncher */); + reset(); + } + + private void onQuickScrubStart() { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mQuickScrubBlocked = true; + setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); + return; + } + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + + mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false); + + // Inform the last progress in case we skipped before. + mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); + } + + private void onFinishedTransitionToQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onFinishedTransitionToQuickScrub(); + + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + } + + public void onQuickScrubProgress(float progress) { + mCurrentQuickScrubProgress = progress; + if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null + || mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + public void onQuickScrubEnd() { + setStateOnUiThread(STATE_QUICK_SCRUB_END); + } + + private void switchToFinalAppAfterQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubEnd(); + + // Normally this is handled in reset(), but since we are still scrubbing after the + // transition into recents, we need to defer the handler invalidation for quick scrub until + // after the gesture ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + + private void debugNewState(int stateFlag) { + if (!DEBUG_STATES) { + return; + } + + int state = mStateCallback.getState(); + StringJoiner currentStateStr = new StringJoiner(", ", "[", "]"); + String stateFlagStr = "Unknown-" + stateFlag; + for (int i = 0; i < STATES.length; i++) { + if ((state & (i << i)) != 0) { + currentStateStr.add(STATES[i]); + } + if (stateFlag == (1 << i)) { + stateFlagStr = STATES[i] + " (" + stateFlag + ")"; + } + } + Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to " + + currentStateStr); + } + + public void setGestureEndCallback(Runnable gestureEndCallback) { + mGestureEndCallback = gestureEndCallback; + } + + // Handling long swipe + private void onLongSwipeEnabledUi() { + mUiLongSwipeMode = true; + checkLongSwipeCanEnter(); + checkLongSwipeCanStart(); + } + + private void onLongSwipeDisabledUi() { + mUiLongSwipeMode = false; + + if (mLongSwipeController != null) { + mLongSwipeController.destroy(); + setTargetAlphaProvider((t, a1) -> a1); + + // Rebuild animations + buildAnimationController(); + } + } + + private void onLongSwipeDisplacementUpdated() { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + return; + } + + mLongSwipeController.onMove(mLongSwipeDisplacement); + } + + private void checkLongSwipeCanEnter() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + // We are entering long swipe mode, make sure the screen shot is captured. + mStateCallback.setState(STATE_CAPTURE_SCREENSHOT); + + } + + private void checkLongSwipeCanStart() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; + if (targetSet == null) { + // This can happen when cancelAnimation comes on the background thread, while we are + // processing the long swipe on the UI thread. + return; + } + + mLongSwipeController = mActivityControlHelper.getLongSwipeController( + mActivity, mRecentsAnimationWrapper.targetSet); + onLongSwipeDisplacementUpdated(); + setTargetAlphaProvider(mLongSwipeController::getTargetAlpha); + } + + private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + mUiLongSwipeMode = false; + handleNormalGestureEnd(velocity, isFling); + return; + } + mUiLongSwipeMode = false; + finishCurrentTransitionToHome(); + mLongSwipeController.end(velocity, isFling, + () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); + + } + + private void setTargetAlphaProvider( + BiFunction provider) { + mClipAnimationHelper.setTaskAlphaCallback(provider); + updateFinalShift(); + } + + public void onAssistDataReceived(Bundle assistData) { + mAssistData = assistData; + setStateOnUiThread(STATE_ASSIST_DATA_RECEIVED); + } + + private void preloadAssistData() { + RecentsModel.getInstance(mContext).preloadAssistData(mRunningTaskId, mAssistData); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java new file mode 100644 index 0000000000000000000000000000000000000000..4abf6e535873b80b77acda1556fddcfd1556d81a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.quickstep.RecentsActivity; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.views.RecentsView; + +public class FallbackRecentsView extends RecentsView { + + public FallbackRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOverviewStateEnabled(true); + getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + protected void startHome() { + mActivity.startHome(); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + updateEmptyMessage(); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + // Just use the activity task size for multi-window as well. + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java new file mode 100644 index 0000000000000000000000000000000000000000..1c865a0894ee781ec7641552c72011249686eaf3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.R; +import com.android.launcher3.util.Themes; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsActivity; + +public class RecentsRootView extends BaseDragLayer { + + private final RecentsActivity mActivity; + + private final Point mLastKnownSize = new Point(10, 10); + + public RecentsRootView(Context context, AttributeSet attrs) { + super(context, attrs, 1 /* alphaChannelCount */); + mActivity = (RecentsActivity) BaseActivity.fromContext(context); + setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + public Point getLastKnownSize() { + return mLastKnownSize; + } + + public void setup() { + mControllers = new TouchController[] { new RecentsTaskController(mActivity) }; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Check size changes before the actual measure, to avoid multiple measure calls. + int width = View.MeasureSpec.getSize(widthMeasureSpec); + int height = View.MeasureSpec.getSize(heightMeasureSpec); + if (mLastKnownSize.x != width || mLastKnownSize.y != height) { + mLastKnownSize.set(width, height); + mActivity.onRootViewSizeChanged(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @TargetApi(23) + @Override + protected boolean fitSystemWindows(Rect insets) { + // Update device profile before notifying the children. + mActivity.getDeviceProfile().updateInsets(insets); + setInsets(insets); + return true; // I'll take it from here + } + + @Override + public void setInsets(Rect insets) { + // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by + // modifying child layout params. + if (!insets.equals(mInsets)) { + super.setInsets(insets); + } + setBackground(insets.top == 0 ? null + : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim)); + } + + public void dispatchInsets() { + mActivity.getDeviceProfile().updateInsets(mInsets); + super.setInsets(mInsets); + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java new file mode 100644 index 0000000000000000000000000000000000000000..635175638e7f1b2a53cb6272d8182e9d1724bc1d --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import com.android.launcher3.uioverrides.TaskViewTouchController; +import com.android.quickstep.RecentsActivity; + +public class RecentsTaskController extends TaskViewTouchController { + + public RecentsTaskController(RecentsActivity activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.hasWindowFocus(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..b4b7f088bafa81565fb3683b967138cde85a1d6c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.logging; + +import android.content.Context; +import android.util.Log; + +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.systemui.shared.system.MetricsLoggerCompat; + +import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CANCEL_TARGET; +import static com.android.systemui.shared.system.LauncherEventUtil.DISMISS; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_QUICK_SCRUB_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_SWIPE_UP_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE; + +/** + * This class handles AOSP MetricsLogger function calls and logging around + * quickstep interactions. + */ +@SuppressWarnings("unused") +public class UserEventDispatcherExtension extends UserEventDispatcher { + + private static final String TAG = "UserEventDispatcher"; + + public UserEventDispatcherExtension(Context context) { } + + public void logStateChangeAction(int action, int dir, int srcChildTargetType, + int srcParentContainerType, int dstContainerType, + int pageIndex) { + new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY, + dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER); + super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType, + dstContainerType, pageIndex); + } + + public void logActionTip(int actionType, int viewType) { + LauncherLogProto.Action action = new LauncherLogProto.Action(); + LauncherLogProto.Target target = new LauncherLogProto.Target(); + switch(actionType) { + case VISIBLE: + action.type = LauncherLogProto.Action.Type.TIP; + target.type = LauncherLogProto.Target.Type.CONTAINER; + target.containerType = LauncherLogProto.ContainerType.TIP; + break; + case DISMISS: + action.type = LauncherLogProto.Action.Type.TOUCH; + action.touch = LauncherLogProto.Action.Touch.TAP; + target.type = LauncherLogProto.Target.Type.CONTROL; + target.controlType = CANCEL_TARGET; + break; + default: + Log.e(TAG, "Unexpected action type = " + actionType); + } + + switch(viewType) { + case RECENTS_QUICK_SCRUB_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.QUICK_SCRUB_TEXT; + break; + case RECENTS_SWIPE_UP_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.SWIPE_UP_TEXT; + break; + default: + Log.e(TAG, "Unexpected viewType = " + viewType); + } + LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target); + dispatchUserEvent(event, null); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..7a0d9931caa797526b0e963744c5fcd8b21fb9e8 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.RemoteException; +import android.support.annotation.Nullable; +import android.view.animation.Interpolator; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.utilities.RectFEvaluator; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams; +import com.android.systemui.shared.system.TransactionCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.function.BiFunction; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Utility class to handle window clip animation + */ +@TargetApi(Build.VERSION_CODES.P) +public class ClipAnimationHelper { + + // The bounds of the source app in device coordinates + private final Rect mSourceStackBounds = new Rect(); + // The insets of the source app + private final Rect mSourceInsets = new Rect(); + // The source app bounds with the source insets applied, in the source app window coordinates + private final RectF mSourceRect = new RectF(); + // The bounds of the task view in launcher window coordinates + private final RectF mTargetRect = new RectF(); + // Set when the final window destination is changed, such as offsetting for quick scrub + private final PointF mTargetOffset = new PointF(); + // The insets to be used for clipping the app window, which can be larger than mSourceInsets + // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In + // app window coordinates. + private final RectF mSourceWindowClipInsets = new RectF(); + + // The bounds of launcher (not including insets) in device coordinates + public final Rect mHomeStackBounds = new Rect(); + + // The clip rect in source app window coordinates + private final Rect mClipRect = new Rect(); + private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); + private final Matrix mTmpMatrix = new Matrix(); + private final RectF mTmpRectF = new RectF(); + + private float mTargetScale = 1f; + private float mOffsetScale = 1f; + private Interpolator mInterpolator = LINEAR; + // We translate y slightly faster than the rest of the animation for quick scrub. + private Interpolator mOffsetYInterpolator = LINEAR; + + // Whether to boost the opening animation target layers, or the closing + private int mBoostModeTargetLayers = -1; + // Wether or not applyTransform has been called yet since prepareAnimation() + private boolean mIsFirstFrame = true; + + private BiFunction mTaskAlphaCallback = + (t, a1) -> a1; + + private void updateSourceStack(RemoteAnimationTargetCompat target) { + mSourceInsets.set(target.contentInsets); + mSourceStackBounds.set(target.sourceContainerBounds); + + // TODO: Should sourceContainerBounds already have this offset? + mSourceStackBounds.offsetTo(target.position.x, target.position.y); + + } + + public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { + mHomeStackBounds.set(homeStackBounds); + updateSourceStack(target); + } + + public void updateTargetRect(TransformedRect targetRect) { + mOffsetScale = targetRect.scale; + mSourceRect.set(mSourceInsets.left, mSourceInsets.top, + mSourceStackBounds.width() - mSourceInsets.right, + mSourceStackBounds.height() - mSourceInsets.bottom); + mTargetRect.set(targetRect.rect); + Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale); + mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, + mHomeStackBounds.top - mSourceStackBounds.top); + + // Calculate the clip based on the target rect (since the content insets and the + // launcher insets may differ, so the aspect ratio of the target rect can differ + // from the source rect. The difference between the target rect (scaled to the + // source rect) is the amount to clip on each edge. + RectF scaledTargetRect = new RectF(mTargetRect); + Utilities.scaleRectFAboutCenter(scaledTargetRect, + mSourceRect.width() / mTargetRect.width()); + scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top); + mSourceWindowClipInsets.set( + Math.max(scaledTargetRect.left, 0), + Math.max(scaledTargetRect.top, 0), + Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0), + Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0)); + mSourceRect.set(scaledTargetRect); + } + + public void prepareAnimation(boolean isOpening) { + mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; + } + + public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress, + @Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier) { + RectF currentRect; + mTmpRectF.set(mTargetRect); + Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale); + float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress); + progress = mInterpolator.getInterpolation(progress); + currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); + + synchronized (mTargetOffset) { + // Stay lined up with the center of the target, since it moves for quick scrub. + currentRect.offset(mTargetOffset.x * mOffsetScale * progress, + mTargetOffset.y * offsetYProgress); + } + + mClipRect.left = (int) (mSourceWindowClipInsets.left * progress); + mClipRect.top = (int) (mSourceWindowClipInsets.top * progress); + mClipRect.right = (int) + (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress)); + mClipRect.bottom = (int) + (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress)); + + SurfaceParams[] params = new SurfaceParams[targetSet.unfilteredApps.length]; + for (int i = 0; i < targetSet.unfilteredApps.length; i++) { + RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i]; + mTmpMatrix.setTranslate(app.position.x, app.position.y); + Rect crop = app.sourceContainerBounds; + float alpha = 1f; + if (app.mode == targetSet.targetMode) { + if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL); + mTmpMatrix.postTranslate(app.position.x, app.position.y); + crop = mClipRect; + } + + if (app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + alpha = 1 - progress; + } + + alpha = mTaskAlphaCallback.apply(app, alpha); + } + + params[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, + RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers)); + } + applyParams(syncTransactionApplier, params); + return currentRect; + } + + private void applyParams(@Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier, + SurfaceParams[] params) { + if (syncTransactionApplier != null) { + syncTransactionApplier.scheduleApply(params); + } else { + TransactionCompat t = new TransactionCompat(); + for (SurfaceParams param : params) { + SyncRtSurfaceTransactionApplier.applyParams(t, param); + } + t.setEarlyWakeup(); + t.apply(); + } + } + + public void setTaskAlphaCallback( + BiFunction callback) { + mTaskAlphaCallback = callback; + } + + public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) { + synchronized (mTargetOffset) { + mTargetOffset.set(offsetX, offsetY); + } + mTargetScale = scale; + mInterpolator = interpolator; + mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0, + QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { + fromTaskThumbnailView(ttv, rv, null); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, + @Nullable RemoteAnimationTargetCompat target) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); + BaseDragLayer dl = activity.getDragLayer(); + + int[] pos = new int[2]; + dl.getLocationOnScreen(pos); + mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); + mHomeStackBounds.offset(pos[0], pos[1]); + + if (target != null) { + updateSourceStack(target); + } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { + updateStackBoundsToMultiWindowTaskSize(activity); + } else { + mSourceStackBounds.set(mHomeStackBounds); + mSourceInsets.set(activity.getDeviceProfile().getInsets()); + } + + TransformedRect targetRect = new TransformedRect(); + dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect); + updateTargetRect(targetRect); + + if (target == null) { + // Transform the clip relative to the target rect. Only do this in the case where we + // aren't applying the insets to the app windows (where the clip should be in target app + // space) + float scale = mTargetRect.width() / mSourceRect.width(); + mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; + mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; + mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; + mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; + } + } + + private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy != null) { + try { + mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); + return; + } catch (RemoteException e) { + // Use half screen size + } + } + + // Assume that the task size is half screen size (minus the insets and the divider size) + DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + int taskWidth = fullDp.availableWidthPx; + int taskHeight = fullDp.availableHeightPx; + int halfDividerSize = activity.getResources() + .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; + + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + + // Align the task to bottom left/right edge (closer to nav bar). + int left = activity.getDeviceProfile().isSeascape() ? insets.left + : (insets.left + fullDp.availableWidthPx - taskWidth); + mSourceStackBounds.set(0, 0, taskWidth, taskHeight); + mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); + } + + public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) { + RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect); + canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left, + mSourceStackBounds.top - mHomeStackBounds.top); + mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL); + + canvas.concat(mTmpMatrix); + canvas.translate(mTargetRect.left, mTargetRect.top); + + float insetProgress = (1 - progress); + ttv.drawOnCanvas(canvas, + -mSourceWindowClipInsets.left * insetProgress, + -mSourceWindowClipInsets.top * insetProgress, + ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress, + ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress, + ttv.getCornerRadius() * progress); + } + + public RectF getTargetRect() { + return mTargetRect; + } + + public RectF getSourceRect() { + return mSourceRect; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..4c7da40e69df61246e6e2a97e007d299174f57d3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public class LayoutUtils { + + private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1; + private static final int MULTI_WINDOW_STRATEGY_DEVICE_PROFILE = 2; + + @Retention(SOURCE) + @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE}) + private @interface MultiWindowStrategy {} + + public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) { + float extraSpace; + if (dp.isVerticalBarLayout()) { + extraSpace = 0; + } else { + extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx; + } + calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect); + } + + public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) { + calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect); + } + + @AnyThread + public static void calculateTaskSize(Context context, DeviceProfile dp, + float extraVerticalSpace, @MultiWindowStrategy int multiWindowStrategy, Rect outRect) { + float taskWidth, taskHeight, paddingHorz; + Resources res = context.getResources(); + Rect insets = dp.getInsets(); + + if (dp.isMultiWindowMode) { + if (multiWindowStrategy == MULTI_WINDOW_STRATEGY_HALF_SCREEN) { + DeviceProfile fullDp = dp.getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + taskWidth = fullDp.availableWidthPx; + taskHeight = fullDp.availableHeightPx; + float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) + / 2; + + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + } else { + // multiWindowStrategy == MULTI_WINDOW_STRATEGY_DEVICE_PROFILE + taskWidth = dp.widthPx; + taskHeight = dp.heightPx; + } + paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space); + } else { + taskWidth = dp.availableWidthPx; + taskHeight = dp.availableHeightPx; + paddingHorz = res.getDimension(dp.isVerticalBarLayout() + ? R.dimen.landscape_task_card_horz_space + : R.dimen.portrait_task_card_horz_space); + } + + float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin); + float paddingVert = res.getDimension(R.dimen.task_card_vert_space); + + // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless + // we override the insets ourselves. + int launcherVisibleWidth = dp.widthPx - insets.left - insets.right; + int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom; + + float availableHeight = launcherVisibleHeight + - topIconMargin - extraVerticalSpace - paddingVert; + float availableWidth = launcherVisibleWidth - paddingHorz; + + float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight); + float outWidth = scale * taskWidth; + float outHeight = scale * taskHeight; + + // Center in the visible space + float x = insets.left + (launcherVisibleWidth - outWidth) / 2; + float y = insets.top + Math.max(topIconMargin, + (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2); + outRect.set(Math.round(x), Math.round(y), + Math.round(x + outWidth), Math.round(y + outHeight)); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java new file mode 100644 index 0000000000000000000000000000000000000000..f35f1552dd38479f0a9e83a8c05c0047469e0374 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * Utility class to update multiple values with different interpolators and durations during + * the same animation. + */ +public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener { + + private final ArrayList mAllProperties = new ArrayList<>(); + + @Override + public final void onAnimationUpdate(ValueAnimator animator) { + final float percent = animator.getAnimatedFraction(); + final float currentPlayTime = percent * animator.getDuration(); + + for (int i = mAllProperties.size() - 1; i >= 0; i--) { + FloatProp prop = mAllProperties.get(i); + float time = Math.max(0, currentPlayTime - prop.mDelay); + float newPercent = Math.min(1f, time / prop.mDuration); + newPercent = prop.mInterpolator.getInterpolation(newPercent); + prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent); + } + onUpdate(percent); + } + + public abstract void onUpdate(float percent); + + public final class FloatProp { + + public float value; + + private final float mStart; + private final float mEnd; + private final float mDelay; + private final float mDuration; + private final Interpolator mInterpolator; + + public FloatProp(float start, float end, float delay, float duration, Interpolator i) { + value = mStart = start; + mEnd = end; + mDelay = delay; + mDuration = duration; + mInterpolator = i; + + mAllProperties.add(this); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..e3c9f6ecabae4f693fd7e5344260df18c04d6f79 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.os.Handler; + +import com.android.launcher3.LauncherAnimationRunner; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +@FunctionalInterface +public interface RemoteAnimationProvider { + + static final int Z_BOOST_BASE = 800570000; + + AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets); + + default ActivityOptions toActivityOptions(Handler handler, long duration) { + LauncherAnimationRunner runner = new LauncherAnimationRunner(handler, + false /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(createWindowAnimation(targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation( + new RemoteAnimationAdapterCompat(runner, duration, 0)); + } + + /** + * Prepares the given {@param targets} for a remote animation, and should be called with the + * transaction from the first frame of animation. + * + * @param boostModeTargets The mode indicating which targets to boost in z-order above other + * targets. + */ + static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets, + TransactionCompat t, int boostModeTargets) { + for (RemoteAnimationTargetCompat target : targets) { + t.setLayer(target.leash, getLayer(target, boostModeTargets)); + t.show(target.leash); + } + } + + static int getLayer(RemoteAnimationTargetCompat target, int boostModeTarget) { + return target.mode == boostModeTarget + ? Z_BOOST_BASE + target.prefixOrderIndex + : target.prefixOrderIndex; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java new file mode 100644 index 0000000000000000000000000000000000000000..d1097cdf167be316032ffc8bb832a57c3b7cd92f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.ArrayList; + +/** + * Holds a collection of RemoteAnimationTargets, filtered by different properties. + */ +public class RemoteAnimationTargetSet { + + public final RemoteAnimationTargetCompat[] unfilteredApps; + public final RemoteAnimationTargetCompat[] apps; + public final int targetMode; + + public RemoteAnimationTargetSet(RemoteAnimationTargetCompat[] apps, int targetMode) { + ArrayList filteredApps = new ArrayList<>(); + if (apps != null) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.mode == targetMode) { + filteredApps.add(target); + } + } + } + + this.unfilteredApps = apps; + this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]); + this.targetMode = targetMode; + } + + public RemoteAnimationTargetCompat findTask(int taskId) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.taskId == taskId) { + return target; + } + } + return null; + } + + public boolean isAnimatingHome() { + for (RemoteAnimationTargetCompat target : apps) { + if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java new file mode 100644 index 0000000000000000000000000000000000000000..4607fdc24029b2662c0725f932c5a50d40fca26c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +import static com.android.quickstep.util.RemoteAnimationProvider.prepareTargetsForFirstFrame; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * Animation listener which fades out the closing targets + */ +public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener { + + private final RemoteAnimationTargetSet mTarget; + private boolean mFirstFrame = true; + + public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] targets) { + mTarget = new RemoteAnimationTargetSet(targets, MODE_CLOSING); + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + TransactionCompat t = new TransactionCompat(); + if (mFirstFrame) { + prepareTargetsForFirstFrame(mTarget.unfilteredApps, t, MODE_CLOSING); + mFirstFrame = false; + } + + float alpha = 1 - valueAnimator.getAnimatedFraction(); + for (RemoteAnimationTargetCompat app : mTarget.apps) { + t.setAlpha(app.leash, alpha); + } + t.apply(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java new file mode 100644 index 0000000000000000000000000000000000000000..1d452866aa033bd643c51f73ec323e6b4808fde4 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.FloatProperty; +import android.view.View; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; + +public class TaskViewDrawable extends Drawable { + + public static final FloatProperty PROGRESS = + new FloatProperty("progress") { + @Override + public void setValue(TaskViewDrawable taskViewDrawable, float v) { + taskViewDrawable.setProgress(v); + } + + @Override + public Float get(TaskViewDrawable taskViewDrawable) { + return taskViewDrawable.mProgress; + } + }; + + /** + * The progress at which we play the atomic icon scale animation. + */ + private static final float ICON_SCALE_THRESHOLD = 0.95f; + + private final RecentsView mParent; + private final View mIconView; + private final int[] mIconPos; + + private final TaskThumbnailView mThumbnailView; + + private final ClipAnimationHelper mClipAnimationHelper; + + private float mProgress = 1; + private boolean mPassedIconScaleThreshold; + private ValueAnimator mIconScaleAnimator; + private float mIconScale; + + public TaskViewDrawable(TaskView tv, RecentsView parent) { + mParent = parent; + mIconView = tv.getIconView(); + mIconPos = new int[2]; + mIconScale = mIconView.getScaleX(); + Utilities.getDescendantCoordRelativeToAncestor(mIconView, parent, mIconPos, true); + + mThumbnailView = tv.getThumbnail(); + mClipAnimationHelper = new ClipAnimationHelper(); + mClipAnimationHelper.fromTaskThumbnailView(mThumbnailView, parent); + } + + public void setProgress(float progress) { + mProgress = progress; + mParent.invalidate(); + boolean passedIconScaleThreshold = progress <= ICON_SCALE_THRESHOLD; + if (mPassedIconScaleThreshold != passedIconScaleThreshold) { + mPassedIconScaleThreshold = passedIconScaleThreshold; + animateIconScale(mPassedIconScaleThreshold ? 0 : 1); + } + } + + private void animateIconScale(float toScale) { + if (mIconScaleAnimator != null) { + mIconScaleAnimator.cancel(); + } + mIconScaleAnimator = ValueAnimator.ofFloat(mIconScale, toScale); + mIconScaleAnimator.addUpdateListener(valueAnimator -> { + mIconScale = (float) valueAnimator.getAnimatedValue(); + if (mProgress > ICON_SCALE_THRESHOLD) { + // Speed up the icon scale to ensure it is 1 when progress is 1. + float iconProgress = (mProgress - ICON_SCALE_THRESHOLD) / (1 - ICON_SCALE_THRESHOLD); + if (iconProgress > mIconScale) { + mIconScale = iconProgress; + } + } + invalidateSelf(); + }); + mIconScaleAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIconScaleAnimator = null; + } + }); + mIconScaleAnimator.setDuration(TaskView.SCALE_ICON_DURATION); + mIconScaleAnimator.start(); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + canvas.translate(mParent.getScrollX(), mParent.getScrollY()); + mClipAnimationHelper.drawForProgress(mThumbnailView, canvas, mProgress); + canvas.restore(); + + canvas.save(); + canvas.translate(mIconPos[0], mIconPos[1]); + canvas.scale(mIconScale, mIconScale, mIconView.getWidth() / 2, mIconView.getHeight() / 2); + mIconView.draw(canvas); + canvas.restore(); + } + + public ClipAnimationHelper getClipAnimationHelper() { + return mClipAnimationHelper; + } + + @Override + public void setAlpha(int i) { } + + @Override + public void setColorFilter(ColorFilter colorFilter) { } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java new file mode 100644 index 0000000000000000000000000000000000000000..b1b0f4e0d84ae9931680023218404bc968cf8c7b --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.graphics.Rect; + +/** + * A wrapper around {@link Rect} with additional transformation properties + */ +public class TransformedRect { + + public final Rect rect = new Rect(); + public float scale = 1; + + public void set(TransformedRect transformedRect) { + rect.set(transformedRect.rect); + scale = transformedRect.scale; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java new file mode 100644 index 0000000000000000000000000000000000000000..a4e276cd2ad834022bf10033392dad0b26dfd607 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView.PageCallbacks; +import com.android.quickstep.views.RecentsView.ScrollState; + +public class ClearAllButton extends Button implements PageCallbacks { + + private float mScrollAlpha = 1; + private float mContentAlpha = 1; + + private final boolean mIsRtl; + + private int mScrollOffset; + + public ClearAllButton(Context context, AttributeSet attrs) { + super(context, attrs); + mIsRtl = Utilities.isRtl(context.getResources()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + RecentsView parent = (RecentsView) getParent(); + mScrollOffset = mIsRtl ? parent.getPaddingRight() / 2 : - parent.getPaddingLeft() / 2; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setContentAlpha(float alpha) { + if (mContentAlpha != alpha) { + mContentAlpha = alpha; + updateAlpha(); + } + } + + @Override + public void onPageScroll(ScrollState scrollState) { + float width = getWidth(); + if (width == 0) { + return; + } + + float shift = Math.min(scrollState.scrollFromEdge, width); + setTranslationX(mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift)); + mScrollAlpha = 1 - shift / width; + updateAlpha(); + } + + private void updateAlpha() { + final float alpha = mScrollAlpha * mContentAlpha; + setAlpha(alpha); + setClickable(alpha == 1); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java new file mode 100644 index 0000000000000000000000000000000000000000..81f57681d920f33c5761fa7f52de71d4ada7d141 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +public class IconView extends View { + + private Drawable mDrawable; + + public IconView(Context context) { + super(context); + } + + public IconView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setDrawable(Drawable d) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + mDrawable = d; + if (mDrawable != null) { + mDrawable.setCallback(this); + mDrawable.setBounds(0, 0, getWidth(), getHeight()); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mDrawable != null) { + mDrawable.setBounds(0, 0, w, h); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + final Drawable drawable = mDrawable; + if (drawable != null && drawable.isStateful() + && drawable.setState(getDrawableState())) { + invalidateDrawable(drawable); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDrawable != null) { + mDrawable.draw(canvas); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java new file mode 100644 index 0000000000000000000000000000000000000000..3a74851d030127d96eb92fd3d77cf5a6047ef599 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.graphics.Rect; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Insettable; +import com.android.launcher3.Launcher; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.WindowTransformSwipeHandler; + +import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK; +import static com.android.launcher3.states.RotationHelper.REQUEST_NONE; + +/** + * Floating view which shows the task snapshot allowing it to be dragged and placed. + */ +public class LauncherLayoutListener extends AbstractFloatingView + implements Insettable, LayoutListener { + + private final Launcher mLauncher; + private WindowTransformSwipeHandler mHandler; + + public LauncherLayoutListener(Launcher launcher) { + super(launcher, null); + mLauncher = launcher; + setVisibility(INVISIBLE); + + // For the duration of the gesture, lock the screen orientation to ensure that we do not + // rotate mid-quickscrub + launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK); + } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { + mHandler = handler; + } + + @Override + public void setInsets(Rect insets) { + if (mHandler != null) { + mHandler.buildAnimationController(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (mIsOpen) { + mIsOpen = false; + // We don't support animate. + mLauncher.getDragLayer().removeView(this); + + if (mHandler != null) { + mHandler.layoutListenerClosed(); + } + } + } + + @Override + public void open() { + if (!mIsOpen) { + mLauncher.getDragLayer().addView(this); + mIsOpen = true; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(1, 1); + } + + @Override + public void logActionCommand(int command) { + // We should probably log the weather + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_QUICKSTEP_PREVIEW) != 0; + } + + @Override + public void finish() { + setHandler(null); + close(false); + mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java new file mode 100644 index 0000000000000000000000000000000000000000..26f0bbfd02ecec0cca945b15b3ac98abbec071f0 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.view.View; +import android.view.ViewDebug; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.ScrimView; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.LayoutUtils; + +import static com.android.launcher3.LauncherAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN; +import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; + +/** + * {@link RecentsView} used in Launcher activity + */ +@TargetApi(Build.VERSION_CODES.O) +public class LauncherRecentsView extends RecentsView { + + public static final FloatProperty TRANSLATION_Y_FACTOR = + new FloatProperty("translationYFactor") { + + @Override + public void setValue(LauncherRecentsView view, float v) { + view.setTranslationYFactor(v); + } + + @Override + public Float get(LauncherRecentsView view) { + return view.mTranslationYFactor; + } + }; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mTranslationYFactor; + + public LauncherRecentsView(Context context) { + this(context, null); + } + + public LauncherRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setContentAlpha(0); + } + + @Override + protected void startHome() { + mActivity.getStateManager().goToState(NORMAL); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setTranslationYFactor(mTranslationYFactor); + } + + public void setTranslationYFactor(float translationFactor) { + mTranslationYFactor = translationFactor; + setTranslationY(computeTranslationYForFactor(mTranslationYFactor)); + } + + public float computeTranslationYForFactor(float translationYFactor) { + return translationYFactor * (getPaddingBottom() - getPaddingTop()); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + protected void onTaskStackUpdated() { + // Lazily update the empty message only when the task stack is reapplied + updateEmptyMessage(); + } + + /** + * Animates adjacent tasks and translate hotseat off screen as well. + */ + @Override + public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv, + ClipAnimationHelper helper) { + AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper); + + if (!OverviewInteractionState.getInstance(mActivity).isSwipeUpGestureEnabled()) { + // Hotseat doesn't move when opening recents with the button, + // so don't animate it here either. + return anim; + } + + float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN; + LauncherState state = mActivity.getStateManager().getState(); + if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) { + float maxShiftRange = mActivity.getDeviceProfile().heightPx; + float currShiftRange = mActivity.getAllAppsController().getShiftRange(); + allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange; + } + anim.play(ObjectAnimator.ofFloat( + mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen)); + + ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt( + mActivity.findViewById(R.id.scrim_view), ScrimView.DRAG_HANDLE_ALPHA, 0); + dragHandleAnim.setInterpolator(Interpolators.ACCEL_2); + anim.play(dragHandleAnim); + + return anim; + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect); + } + + @Override + protected void onTaskLaunched(boolean success) { + if (success) { + mActivity.getStateManager().goToState(NORMAL, false /* animate */); + } else { + LauncherState state = mActivity.getStateManager().getState(); + mActivity.getAllAppsController().setState(state); + } + super.onTaskLaunched(success); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + return mActivity.isInMultiWindowModeCompat(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java new file mode 100644 index 0000000000000000000000000000000000000000..e9c68cd35c126d61e77f8149b3755edbca361cb5 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.UserHandle; +import android.support.annotation.Nullable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.SparseBooleanArray; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ListView; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Insettable; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.PropertyListBuilder; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.Themes; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.TaskViewDrawable; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.TaskStack; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.function.Consumer; + +import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; +import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW; + +/** + * A list of recent tasks. + */ +@TargetApi(Build.VERSION_CODES.P) +public abstract class RecentsView extends PagedView implements Insettable { + + private static final String TAG = RecentsView.class.getSimpleName(); + + public static final FloatProperty CONTENT_ALPHA = + new FloatProperty("contentAlpha") { + @Override + public void setValue(RecentsView view, float v) { + view.setContentAlpha(v); + } + + @Override + public Float get(RecentsView view) { + return view.getContentAlpha(); + } + }; + + private final Rect mTempRect = new Rect(); + + private static final int DISMISS_TASK_DURATION = 300; + // The threshold at which we update the SystemUI flags when animating from the task into the app + public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; + + private static final float[] sTempFloatArray = new float[3]; + + protected final T mActivity; + private final QuickScrubController mQuickScrubController; + private final float mFastFlingVelocity; + private final RecentsModel mModel; + private final int mTaskTopMargin; + private final ClearAllButton mClearAllButton; + private final Rect mClearAllButtonDeadZoneRect = new Rect(); + private final Rect mTaskViewDeadZoneRect = new Rect(); + + private final ScrollState mScrollState = new ScrollState(); + // Keeps track of the previously known visible tasks for purposes of loading/unloading task data + private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); + + /** + * TODO: Call reloadIdNeeded in onTaskStackChanged. + */ + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { + if (!mHandleTaskStackChanges) { + return; + } + updateThumbnail(taskId, snapshot); + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + if (!mHandleTaskStackChanges) { + return; + } + // Check this is for the right user + if (!checkCurrentOrManagedUserId(userId, getContext())) { + return; + } + + // Remove the task immediately from the task list + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + removeView(taskView); + } + } + + @Override + public void onActivityUnpinned() { + if (!mHandleTaskStackChanges) { + return; + } + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + + @Override + public void onTaskRemoved(int taskId) { + if (!mHandleTaskStackChanges) { + return; + } + BackgroundExecutor.get().submit(() -> { + TaskView taskView = getTaskView(taskId); + if (taskView == null) { + return; + } + Handler handler = taskView.getHandler(); + if (handler == null) { + return; + } + + // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and + // remove all these checks + Task.TaskKey taskKey = taskView.getTask().key; + if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), + taskKey.userId) == null) { + // The package was uninstalled + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } else { + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext()); + RecentsTaskLoadPlan.PreloadOptions opts = + new RecentsTaskLoadPlan.PreloadOptions(); + opts.loadTitles = false; + loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1, + UserHandle.myUserId()); + if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) { + // The task was removed from the recents list + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } + } + }); + } + + @Override + public void onPinnedStackAnimationStarted() { + // Needed for activities that auto-enter PiP, which will not trigger a remote + // animation to be created + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + }; + + private int mLoadPlanId = -1; + + // Only valid until the launcher state changes to NORMAL + private int mRunningTaskId = -1; + private boolean mRunningTaskTileHidden; + private Task mTmpRunningTask; + + private boolean mRunningTaskIconScaledDown = false; + + private boolean mOverviewStateEnabled; + private boolean mHandleTaskStackChanges; + private Runnable mNextPageSwitchRunnable; + private boolean mSwipeDownShouldLaunchApp; + private boolean mTouchDownToStartHome; + private final int mTouchSlop; + private int mDownX; + private int mDownY; + + private PendingAnimation mPendingAnimation; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mContentAlpha = 1; + + // Keeps track of task views whose visual state should not be reset + private ArraySet mIgnoreResetTaskViews = new ArraySet<>(); + + // Variables for empty state + private final Drawable mEmptyIcon; + private final CharSequence mEmptyMessage; + private final TextPaint mEmptyMessagePaint; + private final Point mLastMeasureSize = new Point(); + private final int mEmptyMessagePadding; + private boolean mShowEmptyMessage; + private Layout mEmptyTextLayout; + + private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = + (inMultiWindowMode) -> { + if (!inMultiWindowMode && mOverviewStateEnabled) { + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + }; + + public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); + enableFreeScroll(true); + + mFastFlingVelocity = getResources() + .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); + mActivity = (T) BaseActivity.fromContext(context); + mQuickScrubController = new QuickScrubController(mActivity, this); + mModel = RecentsModel.getInstance(context); + + mClearAllButton = (ClearAllButton) LayoutInflater.from(context) + .inflate(R.layout.overview_clear_all_button, this, false); + mClearAllButton.setOnClickListener(this::dismissAllTasks); + + mIsRtl = !Utilities.isRtl(getResources()); + setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); + mTaskTopMargin = getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); + mEmptyIcon.setCallback(this); + mEmptyMessage = context.getText(R.string.recents_empty_message); + mEmptyMessagePaint = new TextPaint(); + mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); + mEmptyMessagePaint.setTextSize(getResources() + .getDimension(R.dimen.recents_empty_message_text_size)); + mEmptyMessagePadding = getResources() + .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); + setWillNotDraw(false); + updateEmptyMessage(); + } + + public boolean isRtl() { + return mIsRtl; + } + + public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); + } + return taskView; + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + updateTaskStackListenerState(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateTaskStackListenerState(); + mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + updateTaskStackListenerState(); + mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + + // Clear the task data for the removed child if it was visible + if (child != mClearAllButton) { + Task task = ((TaskView) child).getTask(); + if (mHasVisibleTaskData.get(task.key.id)) { + mHasVisibleTaskData.delete(task.key.id); + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + } + } + + public boolean isTaskViewVisible(TaskView tv) { + // For now, just check if it's the active task or an adjacent task + return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; + } + + public TaskView getTaskView(int taskId) { + for (int i = 0; i < getTaskViewCount(); i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask().key.id == taskId) { + return tv; + } + } + return null; + } + + public void setOverviewStateEnabled(boolean enabled) { + mOverviewStateEnabled = enabled; + updateTaskStackListenerState(); + } + + public void setNextPageSwitchRunnable(Runnable r) { + mNextPageSwitchRunnable = r; + } + + @Override + protected void onPageEndTransition() { + super.onPageEndTransition(); + if (mNextPageSwitchRunnable != null) { + mNextPageSwitchRunnable.run(); + mNextPageSwitchRunnable = null; + } + if (getNextPage() > 0) { + setSwipeDownShouldLaunchApp(true); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + super.onTouchEvent(ev); + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + switch (ev.getAction()) { + case MotionEvent.ACTION_UP: + if (mShowEmptyMessage) { + onAllTasksRemoved(); + } + if (mTouchDownToStartHome) { + startHome(); + } + mTouchDownToStartHome = false; + break; + case MotionEvent.ACTION_CANCEL: + mTouchDownToStartHome = false; + break; + case MotionEvent.ACTION_MOVE: + // Passing the touch slop will not allow dismiss to home + if (mTouchDownToStartHome && Math.hypot(mDownX - x, mDownY - y) > mTouchSlop) { + mTouchDownToStartHome = false; + } + break; + case MotionEvent.ACTION_DOWN: + // Touch down anywhere but the deadzone around the visible clear all button and + // between the task views will start home on touch up + if (mTouchState == TOUCH_STATE_REST) { + updateDeadZoneRects(); + final boolean clearAllButtonDeadZoneConsumed = mClearAllButton.getAlpha() == 1 + && mClearAllButtonDeadZoneRect.contains(x, y); + if (!clearAllButtonDeadZoneConsumed + && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { + mTouchDownToStartHome = true; + } + } + mDownX = x; + mDownY = y; + break; + } + + + // Do not let touch escape to siblings below this view. + return true; + } + + private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { + if (mPendingAnimation != null) { + mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan)); + return; + } + TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; + if (stack == null) { + removeAllViews(); + onTaskStackUpdated(); + return; + } + + int oldChildCount = getChildCount(); + + // Ensure there are as many views as there are tasks in the stack (adding and trimming as + // necessary) + final LayoutInflater inflater = LayoutInflater.from(getContext()); + final ArrayList tasks = new ArrayList<>(stack.getTasks()); + + final int requiredTaskCount = tasks.size(); + if (getTaskViewCount() != requiredTaskCount) { + if (oldChildCount > 0) { + removeView(mClearAllButton); + } + for (int i = getChildCount(); i < requiredTaskCount; i++) { + final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false); + addView(taskView); + } + while (getChildCount() > requiredTaskCount) { + final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1); + removeView(taskView); + } + if (requiredTaskCount > 0) { + addView(mClearAllButton); + } + } + + // Unload existing visible task data + unloadVisibleTaskData(); + + // Rebind and reset all task views + for (int i = requiredTaskCount - 1; i >= 0; i--) { + final int pageIndex = requiredTaskCount - i - 1; + final Task task = tasks.get(i); + final TaskView taskView = (TaskView) getChildAt(pageIndex); + taskView.bind(task); + } + resetTaskVisuals(); + + if (oldChildCount != getChildCount()) { + mQuickScrubController.snapToNextTaskIfAvailable(); + } + onTaskStackUpdated(); + } + + public int getTaskViewCount() { + // Account for the clear all button. + int childCount = getChildCount(); + return childCount == 0 ? 0 : childCount - 1; + } + + protected void onTaskStackUpdated() { } + + public void resetTaskVisuals() { + for (int i = getTaskViewCount() - 1; i >= 0; i--) { + TaskView taskView = (TaskView) getChildAt(i); + if (!mIgnoreResetTaskViews.contains(taskView)) { + taskView.resetVisualProperties(); + } + } + if (mRunningTaskTileHidden) { + setRunningTaskHidden(mRunningTaskTileHidden); + } + applyIconScale(false /* animate */); + + updateCurveProperties(); + // Update the set of visible task's data + loadVisibleTaskData(); + } + + private void updateTaskStackListenerState() { + boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() + && getWindowVisibility() == VISIBLE; + if (handleTaskStackChanges != mHandleTaskStackChanges) { + mHandleTaskStackChanges = handleTaskStackChanges; + if (handleTaskStackChanges) { + reloadIfNeeded(); + } + } + } + + @Override + public void setInsets(Rect insets) { + mInsets.set(insets); + DeviceProfile dp = mActivity.getDeviceProfile(); + getTaskSize(dp, mTempRect); + + // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub. + mTempRect.top -= mTaskTopMargin; + setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, + dp.availableWidthPx + mInsets.left - mTempRect.right, + dp.availableHeightPx + mInsets.top - mTempRect.bottom); + } + + protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); + + public void getTaskSize(Rect outRect) { + getTaskSize(mActivity.getDeviceProfile(), outRect); + } + + @Override + protected boolean computeScrollHelper() { + boolean scrolling = super.computeScrollHelper(); + boolean isFlingingFast = false; + updateCurveProperties(); + if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) { + if (scrolling) { + // Check if we are flinging quickly to disable high res thumbnail loading + isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; + } + + // After scrolling, update the visible task's data + loadVisibleTaskData(); + } + + // Update the high res thumbnail loader + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast); + return scrolling; + } + + /** + * Scales and adjusts translation of adjacent pages as if on a curved carousel. + */ + public void updateCurveProperties() { + if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { + return; + } + int scrollX = getScrollX(); + final int halfPageWidth = getNormalChildWidth() / 2; + final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth; + final int halfScreenWidth = getMeasuredWidth() / 2; + final int pageSpacing = mPageSpacing; + mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX); + + final int pageCount = getPageCount(); + for (int i = 0; i < pageCount; i++) { + View page = getPageAt(i); + float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; + float distanceFromScreenCenter = screenCenter - pageCenter; + float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; + mScrollState.linearInterpolation = Math.min(1, + Math.abs(distanceFromScreenCenter) / distanceToReachEdge); + ((PageCallbacks) page).onPageScroll(mScrollState); + } + } + + /** + * Iterates through all thet asks, and loads the associated task data for newly visible tasks, + * and unloads the associated task data for tasks that are no longer visible. + */ + public void loadVisibleTaskData() { + if (!mOverviewStateEnabled) { + // Skip loading visible task data if we've already left the overview state + return; + } + + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + int centerPageIndex = getPageNearestToCenterOfScreen(); + int numChildren = getTaskViewCount(); + int lower = Math.max(0, centerPageIndex - 2); + int upper = Math.min(centerPageIndex + 2, numChildren - 1); + + // Update the task data for the in/visible children + for (int i = 0; i < numChildren; i++) { + TaskView taskView = (TaskView) getChildAt(i); + Task task = taskView.getTask(); + boolean visible = lower <= i && i <= upper; + if (visible) { + if (task == mTmpRunningTask) { + // Skip loading if this is the task that we are animating into + continue; + } + if (!mHasVisibleTaskData.get(task.key.id)) { + loader.loadTaskData(task); + loader.getHighResThumbnailLoader().onTaskVisible(task); + } + mHasVisibleTaskData.put(task.key.id, visible); + } else { + if (mHasVisibleTaskData.get(task.key.id)) { + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + mHasVisibleTaskData.delete(task.key.id); + } + } + } + + /** + * Unloads any associated data from the currently visible tasks + */ + private void unloadVisibleTaskData() { + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + for (int i = 0; i < mHasVisibleTaskData.size(); i++) { + if (mHasVisibleTaskData.valueAt(i)) { + TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); + Task task = taskView.getTask(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + } + mHasVisibleTaskData.clear(); + } + + protected void onAllTasksRemoved() { + startHome(); + } + + protected abstract void startHome(); + + public void reset() { + mRunningTaskId = -1; + mRunningTaskTileHidden = false; + + unloadVisibleTaskData(); + setCurrentPage(0); + + OverviewCallbacks.get(getContext()).onResetOverview(); + } + + /** + * Reloads the view if anything in recents changed. + */ + public void reloadIfNeeded() { + if (!mModel.isLoadPlanValid(mLoadPlanId)) { + mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); + } + } + + /** + * Ensures that the first task in the view represents {@param task} and reloads the view + * if needed. This allows the swipe-up gesture to assume that the first tile always + * corresponds to the correct task. + * All subsequent calls to reload will keep the task as the first item until {@link #reset()} + * is called. + * Also scrolls the view to this task + */ + public void showTask(int runningTaskId) { + if (getChildCount() == 0) { + // Add an empty view for now until the task plan is loaded and applied + final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) + .inflate(R.layout.task, this, false); + addView(taskView); + addView(mClearAllButton); + + // The temporary running task is only used for the duration between the start of the + // gesture and the task list is loaded and applied + mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), + new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0, + false, true, false, false, new ActivityManager.TaskDescription(), 0, + new ComponentName("", ""), false); + taskView.bind(mTmpRunningTask); + } + setCurrentTask(runningTaskId); + } + + /** + * Hides the tile associated with {@link #mRunningTaskId} + */ + public void setRunningTaskHidden(boolean isHidden) { + mRunningTaskTileHidden = isHidden; + TaskView runningTask = getTaskView(mRunningTaskId); + if (runningTask != null) { + runningTask.setAlpha(isHidden ? 0 : mContentAlpha); + } + } + + /** + * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile. + */ + public void setCurrentTask(int runningTaskId) { + boolean runningTaskTileHidden = mRunningTaskTileHidden; + boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown; + + setRunningTaskIconScaledDown(false, false); + setRunningTaskHidden(false); + mRunningTaskId = runningTaskId; + setRunningTaskIconScaledDown(runningTaskIconScaledDown, false); + setRunningTaskHidden(runningTaskTileHidden); + + setCurrentPage(0); + + // Load the tasks (if the loading is already + mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan); + } + + public void showNextTask() { + TaskView runningTaskView = getTaskView(mRunningTaskId); + if (runningTaskView == null) { + // Launch the first task + if (getTaskViewCount() > 0) { + ((TaskView) getChildAt(0)).launchTask(true /* animate */); + } + } else { + // Get the next launch task + int runningTaskIndex = indexOfChild(runningTaskView); + int nextTaskIndex = Math.max(0, Math.min(getTaskViewCount() - 1, runningTaskIndex + 1)); + if (nextTaskIndex < getTaskViewCount()) { + ((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */); + } + } + } + + public QuickScrubController getQuickScrubController() { + return mQuickScrubController; + } + + public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) { + if (mRunningTaskIconScaledDown == isScaledDown) { + return; + } + mRunningTaskIconScaledDown = isScaledDown; + applyIconScale(animate); + } + + private void applyIconScale(boolean animate) { + float scale = mRunningTaskIconScaledDown ? 0 : 1; + TaskView firstTask = getTaskView(mRunningTaskId); + if (firstTask != null) { + if (animate) { + firstTask.animateIconToScaleAndDim(scale); + } else { + firstTask.setIconScaleAndDim(scale); + } + } + } + + public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { + mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; + } + + public boolean shouldSwipeDownLaunchApp() { + return mSwipeDownShouldLaunchApp; + } + + public interface PageCallbacks { + + /** + * Updates the page UI based on scroll params. + */ + default void onPageScroll(ScrollState scrollState) {} + } + + public static class ScrollState { + + /** + * The progress from 0 to 1, where 0 is the center + * of the screen and 1 is the edge of the screen. + */ + public float linearInterpolation; + + /** + * The amount by which all the content is scrolled relative to the end of the list. + */ + public float scrollFromEdge; + } + + public void addIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.add(taskView); + } + + public void removeIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.remove(taskView); + } + + private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { + addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); + addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), + duration, LINEAR, anim); + } + + private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, + boolean shouldLog) { + if (task != null) { + ActivityManagerWrapper.getInstance().removeTask(task.key.id); + if (shouldLog) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.UP, index, + TaskUtils.getLaunchComponentKeyForTask(task.key)); + } + } + } + + public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, + boolean shouldRemoveTask, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getPageCount(); + if (count == 0) { + return pendingAnimation; + } + + int[] oldScroll = new int[count]; + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + + int[] newScroll = new int[count]; + getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); + + int taskCount = getTaskViewCount(); + int scrollDiffPerPage = 0; + if (count > 1) { + scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); + } + int draggedIndex = indexOfChild(taskView); + + boolean needsCurveUpdates = false; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child == taskView) { + if (animateTaskView) { + addDismissedTaskAnimations(taskView, anim, duration); + } + } else { + // If we just take newScroll - oldScroll, everything to the right of dragged task + // translates to the left. We need to offset this in some cases: + // - In RTL, add page offset to all pages, since we want pages to move to the right + // Additionally, add a page offset if: + // - Current page is rightmost page (leftmost for RTL) + // - Dragging an adjacent page on the left side (right side for RTL) + int offset = mIsRtl ? scrollDiffPerPage : 0; + if (mCurrentPage == draggedIndex) { + int lastPage = taskCount - 1; + if (mCurrentPage == lastPage) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } else { + // Dragging an adjacent page. + int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) + if (draggedIndex == negativeAdjacent) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } + int scrollDiff = newScroll[i] - oldScroll[i] + offset; + if (scrollDiff != 0) { + addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), + duration, ACCEL, anim); + needsCurveUpdates = true; + } + } + } + + if (needsCurveUpdates) { + ValueAnimator va = ValueAnimator.ofFloat(0, 1); + va.addUpdateListener((a) -> updateCurveProperties()); + anim.play(va); + } + + // Add a tiny bit of translation Z, so that it draws on top of other views + if (animateTaskView) { + taskView.setTranslationZ(0.1f); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + if (shouldRemoveTask) { + removeTask(taskView.getTask(), draggedIndex, onEndListener, true); + } + int pageToSnapTo = mCurrentPage; + if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount() - 1)) { + pageToSnapTo -= 1; + } + removeView(taskView); + + if (getTaskViewCount() == 0) { + removeView(mClearAllButton); + onAllTasksRemoved(); + } else { + snapToPageImmediately(pageToSnapTo); + } + } + resetTaskVisuals(); + mPendingAnimation = null; + }); + return pendingAnimation; + } + + public PendingAnimation createAllTasksDismissAnimation(long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getTaskViewCount(); + for (int i = 0; i < count; i++) { + addDismissedTaskAnimations(getChildAt(i), anim, duration); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + int taskViewCount = getTaskViewCount(); + for (int i = 0; i < taskViewCount; i++) { + removeTask(getTaskViewAt(i).getTask(), -1, onEndListener, false); + } + removeAllViews(); + onAllTasksRemoved(); + } + mPendingAnimation = null; + }); + return pendingAnimation; + } + + private static void addAnim(ObjectAnimator anim, long duration, + TimeInterpolator interpolator, AnimatorSet set) { + anim.setDuration(duration).setInterpolator(interpolator); + set.play(anim); + } + + private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { + if (pageCount == 0) { + return false; + } + final int newPageUnbound = getNextPage() + delta; + if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { + return false; + } + snapToPage((newPageUnbound + pageCount) % pageCount); + getChildAt(getNextPage()).requestFocus(); + return true; + } + + private void runDismissAnimation(PendingAnimation pendingAnim) { + AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( + pendingAnim.anim, DISMISS_TASK_DURATION); + controller.dispatchOnStart(); + controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); + controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); + controller.start(); + } + + public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { + runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, + DISMISS_TASK_DURATION)); + } + + @SuppressWarnings("unused") + private void dismissAllTasks(View view) { + runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); + } + + private void dismissCurrentTask() { + TaskView taskView = getTaskView(getNextPage()); + if (taskView != null) { + dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_TAB: + return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, + event.isAltPressed() /* cycle */); + case KeyEvent.KEYCODE_DPAD_RIGHT: + return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); + case KeyEvent.KEYCODE_DPAD_LEFT: + return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); + case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_FORWARD_DEL: + dismissCurrentTask(); + return true; + case KeyEvent.KEYCODE_NUMPAD_DOT: + if (event.isAltPressed()) { + // Numpad DEL pressed while holding Alt. + dismissCurrentTask(); + return true; + } + } + } + return super.dispatchKeyEvent(event); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, + @Nullable Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (gainFocus && getChildCount() > 0) { + switch (direction) { + case FOCUS_FORWARD: + setCurrentPage(0); + break; + case FOCUS_BACKWARD: + case FOCUS_RIGHT: + case FOCUS_LEFT: + setCurrentPage(getChildCount() - 1); + break; + } + } + } + + public float getContentAlpha() { + return mContentAlpha; + } + + public void setContentAlpha(float alpha) { + if (alpha == mContentAlpha) { + return; + } + alpha = Utilities.boundToRange(alpha, 0, 1); + mContentAlpha = alpha; + for (int i = getTaskViewCount() - 1; i >= 0; i--) { + TaskView child = getTaskViewAt(i); + if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { + getChildAt(i).setAlpha(alpha); + } + } + mClearAllButton.setContentAlpha(mContentAlpha); + + int alphaInt = Math.round(alpha * 255); + mEmptyMessagePaint.setAlpha(alphaInt); + mEmptyIcon.setAlpha(alphaInt); + + setVisibility(alpha > 0 ? VISIBLE : GONE); + } + + private float[] getAdjacentScaleAndTranslation(TaskView currTask, + float currTaskToScale, float currTaskToTranslationY) { + float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale()); + sTempFloatArray[0] = currTaskToScale; + sTempFloatArray[1] = mIsRtl ? -displacement : displacement; + sTempFloatArray[2] = currTaskToTranslationY; + return sTempFloatArray; + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + child.setAlpha(mContentAlpha); + } + + public TaskView getTaskViewAt(int index) { + View child = getChildAt(index); + return child == mClearAllButton ? null : (TaskView) child; + } + + public void updateEmptyMessage() { + boolean isEmpty = getChildCount() == 0; + boolean hasSizeChanged = mLastMeasureSize.x != getWidth() + || mLastMeasureSize.y != getHeight(); + if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { + return; + } + setContentDescription(isEmpty ? mEmptyMessage : ""); + mShowEmptyMessage = isEmpty; + updateEmptyStateUi(hasSizeChanged); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateEmptyStateUi(changed); + + // Set the pivot points to match the task preview center + setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) + + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); + setPivotX(((mInsets.left + getPaddingLeft()) + + (getWidth() - mInsets.right - getPaddingRight())) / 2); + } + + private void updateDeadZoneRects() { + // Get the deadzone rect surrounding the clear all button to not dismiss overview to home + mClearAllButtonDeadZoneRect.setEmpty(); + if (mClearAllButton.getWidth() > 0) { + int verticalMargin = getResources() + .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); + mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); + mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); + } + + // Get the deadzone rect between the task views + mTaskViewDeadZoneRect.setEmpty(); + int count = getTaskViewCount(); + if (count > 0) { + final View taskView = getTaskViewAt(0); + getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); + mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), + taskView.getBottom()); + } + } + + private void updateEmptyStateUi(boolean sizeChanged) { + boolean hasValidSize = getWidth() > 0 && getHeight() > 0; + if (sizeChanged && hasValidSize) { + mEmptyTextLayout = null; + mLastMeasureSize.set(getWidth(), getHeight()); + } + + if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { + int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; + mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), + mEmptyMessagePaint, availableWidth) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .build(); + int totalHeight = mEmptyTextLayout.getHeight() + + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); + + int top = (mLastMeasureSize.y - totalHeight) / 2; + int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; + mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), + top + mEmptyIcon.getIntrinsicHeight()); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); + } + + protected void maybeDrawEmptyMessage(Canvas canvas) { + if (mShowEmptyMessage && mEmptyTextLayout != null) { + // Offset to center in the visible (non-padded) part of RecentsView + mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), + mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); + canvas.save(); + canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, + (mTempRect.top - mTempRect.bottom) / 2); + mEmptyIcon.draw(canvas); + canvas.translate(mEmptyMessagePadding, + mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); + mEmptyTextLayout.draw(canvas); + canvas.restore(); + } + } + + /** + * Animate adjacent tasks off screen while scaling up. + * + * If launching one of the adjacent tasks, parallax the center task and other adjacent task + * to the right. + */ + public AnimatorSet createAdjacentPageAnimForTaskLaunch( + TaskView tv, ClipAnimationHelper clipAnimationHelper) { + AnimatorSet anim = new AnimatorSet(); + + int taskIndex = indexOfChild(tv); + int centerTaskIndex = getCurrentPage(); + boolean launchingCenterTask = taskIndex == centerTaskIndex; + + float toScale = clipAnimationHelper.getSourceRect().width() + / clipAnimationHelper.getTargetRect().width(); + float toTranslationY = clipAnimationHelper.getSourceRect().centerY() + - clipAnimationHelper.getTargetRect().centerY(); + if (launchingCenterTask) { + TaskView centerTask = getTaskViewAt(centerTaskIndex); + if (taskIndex - 1 >= 0) { + TaskView adjacentTask = getTaskViewAt(taskIndex - 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + scaleAndTranslation[1] = -scaleAndTranslation[1]; + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + if (taskIndex + 1 < getTaskViewCount()) { + TaskView adjacentTask = getTaskViewAt(taskIndex + 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + } else { + // We are launching an adjacent task, so parallax the center and other adjacent task. + float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); + anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, + mIsRtl ? -displacementX : displacementX)); + + int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); + if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { + anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), + new PropertyListBuilder() + .translationX(mIsRtl ? -displacementX : displacementX) + .scale(1) + .build())); + } + } + return anim; + } + + private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) { + AnimatorSet anim = new AnimatorSet(); + anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0])); + anim.play(ObjectAnimator.ofPropertyValuesHolder(child, + new PropertyListBuilder() + .translationX(toScaleAndTranslation[1]) + .translationY(toScaleAndTranslation[2]) + .build())); + return anim; + } + + public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + + int count = getChildCount(); + if (count == 0) { + return new PendingAnimation(new AnimatorSet()); + } + + tv.setVisibility(INVISIBLE); + int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); + TaskViewDrawable drawable = new TaskViewDrawable(tv, this); + getOverlay().add(drawable); + + final boolean[] passedOverviewThreshold = new boolean[] {false}; + ObjectAnimator drawableAnim = + ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0); + drawableAnim.setInterpolator(LINEAR); + drawableAnim.addUpdateListener((animator) -> { + // Once we pass a certain threshold, update the sysui flags to match the target tasks' + // flags + mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, + animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD + ? targetSysUiFlags + : 0); + + // Passing the threshold from taskview to fullscreen app will vibrate + final boolean passed = animator.getAnimatedFraction() >= MIN_PROGRESS_FOR_OVERVIEW; + if (passed != passedOverviewThreshold[0]) { + passedOverviewThreshold[0] = passed; + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + }); + + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, + drawable.getClipAnimationHelper()); + anim.play(drawableAnim); + anim.setDuration(duration); + + Consumer onTaskLaunchFinish = (result) -> { + onTaskLaunched(result); + tv.setVisibility(VISIBLE); + getOverlay().remove(drawable); + }; + + mPendingAnimation = new PendingAnimation(anim); + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + Consumer onLaunchResult = (result) -> { + onTaskLaunchFinish.accept(result); + if (!result) { + tv.notifyTaskLaunchFailed(TAG); + } + }; + tv.launchTask(false, onLaunchResult, getHandler()); + Task task = tv.getTask(); + if (task != null) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.DOWN, indexOfChild(tv), + TaskUtils.getLaunchComponentKeyForTask(task.key)); + } + } else { + onTaskLaunchFinish.accept(false); + } + mPendingAnimation = null; + }); + return mPendingAnimation; + } + + public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); + + protected void onTaskLaunched(boolean success) { + resetTaskVisuals(); + } + + @Override + protected void notifyPageSwitchListener(int prevPage) { + super.notifyPageSwitchListener(prevPage); + loadVisibleTaskData(); + } + + @Override + protected String getCurrentPageDescription() { + return ""; + } + + @Override + public void addChildrenForAccessibility(ArrayList outChildren) { + // Add children in reverse order + for (int i = getChildCount() - 1; i >= 0; --i) { + outChildren.add(getChildAt(i)); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + final AccessibilityNodeInfo.CollectionInfo + collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( + 1, getTaskViewCount(), false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); + info.setCollectionInfo(collectionInfo); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + final int taskViewCount = getTaskViewCount(); + event.setScrollable(taskViewCount > 0); + + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + final int[] visibleTasks = getVisibleChildrenRange(); + event.setFromIndex(taskViewCount - visibleTasks[1] - 1); + event.setToIndex(taskViewCount - visibleTasks[0] - 1); + event.setItemCount(taskViewCount); + } + } + + @Override + public CharSequence getAccessibilityClassName() { + // To hear position-in-list related feedback from Talkback. + return ListView.class.getName(); + } + + @Override + protected boolean isPageOrderFlipped() { + return true; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java new file mode 100644 index 0000000000000000000000000000000000000000..524724cc8ad2e6f1e3dd51bd5317dca3a4bffcf3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Path.Op; +import android.util.AttributeSet; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ScrimView; + +import static android.support.v4.graphics.ColorUtils.setAlphaComponent; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +/** + * Scrim used for all-apps and shelf in Overview + * In transposed layout, it behaves as a simple color scrim. + * In portrait layout, it draws a rounded rect such that + * From normal state to overview state, the shelf just fades in and does not move + * From overview state to all-apps state the shelf moves up and fades in to cover the screen + */ +public class ShelfScrimView extends ScrimView { + + // If the progress is more than this, shelf follows the finger, otherwise it moves faster to + // cover the whole screen + private static final float SCRIM_CATCHUP_THRESHOLD = 0.2f; + + // In transposed layout, we simply draw a flat color. + private boolean mDrawingFlatColor; + + // For shelf mode + private final int mEndAlpha; + private final float mRadius; + private final int mMaxScrimAlpha; + private final Paint mPaint; + + // Mid point where the alpha changes + private int mMidAlpha; + private float mMidProgress; + + private float mShiftRange; + + private final float mShelfOffset; + private float mTopOffset; + private float mShelfTop; + private float mShelfTopAtThreshold; + + private int mShelfColor; + private int mRemainingScreenColor; + + private final Path mTempPath = new Path(); + private final Path mRemainingScreenPath = new Path(); + private boolean mRemainingScreenPathValid = false; + + public ShelfScrimView(Context context, AttributeSet attrs) { + super(context, attrs); + mMaxScrimAlpha = Math.round(OVERVIEW.getWorkspaceScrimAlpha(mLauncher) * 255); + + mEndAlpha = Color.alpha(mEndScrim); + mRadius = mLauncher.getResources().getDimension(R.dimen.shelf_surface_radius); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mShelfOffset = context.getResources().getDimension(R.dimen.shelf_surface_offset); + // Just assume the easiest UI for now, until we have the proper layout information. + mDrawingFlatColor = true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mRemainingScreenPathValid = false; + } + + @Override + public void reInitUi() { + DeviceProfile dp = mLauncher.getDeviceProfile(); + mDrawingFlatColor = dp.isVerticalBarLayout(); + + if (!mDrawingFlatColor) { + mRemainingScreenPathValid = false; + mShiftRange = mLauncher.getAllAppsController().getShiftRange(); + + mMidProgress = OVERVIEW.getVerticalProgress(mLauncher); + mMidAlpha = mMidProgress >= 1 ? 0 + : Themes.getAttrInteger(getContext(), R.attr.allAppsInterimScrimAlpha); + + mTopOffset = dp.getInsets().top - mShelfOffset; + mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset; + updateColors(); + } + updateDragHandleAlpha(); + invalidate(); + } + + @Override + public void updateColors() { + super.updateColors(); + if (mDrawingFlatColor) { + mDragHandleOffset = 0; + return; + } + + mDragHandleOffset = mShelfOffset - mDragHandleSize; + if (mProgress >= SCRIM_CATCHUP_THRESHOLD) { + mShelfTop = mShiftRange * mProgress + mTopOffset; + } else { + mShelfTop = Utilities.mapRange(mProgress / SCRIM_CATCHUP_THRESHOLD, -mRadius, + mShelfTopAtThreshold); + } + + if (mProgress >= 1) { + mRemainingScreenColor = 0; + mShelfColor = 0; + } else if (mProgress >= mMidProgress) { + mRemainingScreenColor = 0; + + int alpha = Math.round(Utilities.mapToRange( + mProgress, mMidProgress, 1, mMidAlpha, 0, ACCEL)); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + } else { + mDragHandleOffset += mShiftRange * (mMidProgress - mProgress); + + // Note that these ranges and interpolators are inverted because progress goes 1 to 0. + int alpha = Math.round( + Utilities.mapToRange(mProgress, (float) 0, mMidProgress, (float) mEndAlpha, + (float) mMidAlpha, Interpolators.clampToProgress(ACCEL, 0.5f, 1f))); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + + int remainingScrimAlpha = Math.round( + Utilities.mapToRange(mProgress, (float) 0, mMidProgress, mMaxScrimAlpha, + (float) 0, LINEAR)); + mRemainingScreenColor = setAlphaComponent(mScrimColor, remainingScrimAlpha); + } + } + + @Override + protected void onDraw(Canvas canvas) { + drawBackground(canvas); + drawDragHandle(canvas); + } + + private void drawBackground(Canvas canvas) { + if (mDrawingFlatColor) { + if (mCurrentFlatColor != 0) { + canvas.drawColor(mCurrentFlatColor); + } + return; + } + + if (Color.alpha(mShelfColor) == 0) { + return; + } else if (mProgress <= 0) { + canvas.drawColor(mShelfColor); + return; + } + + int height = getHeight(); + int width = getWidth(); + // Draw the scrim over the remaining screen if needed. + if (mRemainingScreenColor != 0) { + if (!mRemainingScreenPathValid) { + mTempPath.reset(); + // Using a arbitrary '+10' in the bottom to avoid any left-overs at the + // corners due to rounding issues. + mTempPath.addRoundRect(0, height - mRadius, width, height + mRadius + 10, + mRadius, mRadius, Direction.CW); + mRemainingScreenPath.reset(); + mRemainingScreenPath.addRect(0, 0, width, height, Direction.CW); + mRemainingScreenPath.op(mTempPath, Op.DIFFERENCE); + } + + float offset = height - mRadius - mShelfTop; + canvas.translate(0, -offset); + mPaint.setColor(mRemainingScreenColor); + canvas.drawPath(mRemainingScreenPath, mPaint); + canvas.translate(0, offset); + } + + mPaint.setColor(mShelfColor); + canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java new file mode 100644 index 0000000000000000000000000000000000000000..2c8a86fae8d0ff7d0cf56a17f7f444677185875a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; + +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA; + +/** + * Contains options for a recent task when long-pressing its icon. + */ +public class TaskMenuView extends AbstractFloatingView { + + private static final Rect sTempRect = new Rect(); + + /** Note that these will be shown in order from top to bottom, if available for the task. */ + public static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[] { + new TaskSystemShortcut.AppInfo(), + new TaskSystemShortcut.SplitScreen(), + new TaskSystemShortcut.Pin(), + new TaskSystemShortcut.Install(), + }; + + private static final int REVEAL_OPEN_DURATION = 150; + private static final int REVEAL_CLOSE_DURATION = 100; + + private BaseDraggingActivity mActivity; + private TextView mTaskIconAndName; + private AnimatorSet mOpenCloseAnimator; + private TaskView mTaskView; + private LinearLayout mOptionLayout; + + public TaskMenuView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mActivity = BaseDraggingActivity.fromContext(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTaskIconAndName = findViewById(R.id.task_icon_and_name); + mOptionLayout = findViewById(R.id.menu_option_layout); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + BaseDragLayer dl = mActivity.getDragLayer(); + if (!dl.isEventOverView(this, ev)) { + // TODO: log this once we have a new container type for it? + close(true); + return true; + } + } + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (animate) { + animateClose(); + } else { + closeComplete(); + } + } + + @Override + public void logActionCommand(int command) { + // TODO + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_TASK_MENU) != 0; + } + + public static boolean showForTask(TaskView taskView) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext()); + final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( + R.layout.task_menu, activity.getDragLayer(), false); + return taskMenuView.populateAndShowForTask(taskView); + } + + private boolean populateAndShowForTask(TaskView taskView) { + if (isAttachedToWindow()) { + return false; + } + mActivity.getDragLayer().addView(this); + mTaskView = taskView; + addMenuOptions(mTaskView); + orientAroundTaskView(mTaskView); + post(this::animateOpen); + return true; + } + + private void addMenuOptions(TaskView taskView) { + Drawable icon = taskView.getTask().icon.getConstantState().newDrawable(); + int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size); + icon.setBounds(0, 0, iconSize, iconSize); + mTaskIconAndName.setCompoundDrawables(null, icon, null, null); + mTaskIconAndName.setText(TaskUtils.getTitle(getContext(), taskView.getTask())); + mTaskIconAndName.setOnClickListener(v -> close(true)); + + // Move the icon and text up half an icon size to lay over the TaskView + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mTaskIconAndName.getLayoutParams(); + params.topMargin = (int) -getResources().getDimension(R.dimen.task_thumbnail_top_margin); + mTaskIconAndName.setLayoutParams(params); + + for (TaskSystemShortcut menuOption : MENU_OPTIONS) { + View.OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, taskView); + if (onClickListener != null) { + addMenuOption(menuOption, onClickListener); + } + } + } + + private void addMenuOption(TaskSystemShortcut menuOption, View.OnClickListener onClickListener) { + ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate( + R.layout.task_view_menu_option, this, false); + menuOptionView.findViewById(R.id.icon).setBackgroundResource(menuOption.iconResId); + ((TextView) menuOptionView.findViewById(R.id.text)).setText(menuOption.labelResId); + menuOptionView.setOnClickListener(onClickListener); + mOptionLayout.addView(menuOptionView); + } + + private void orientAroundTaskView(TaskView taskView) { + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect); + Rect insets = mActivity.getDragLayer().getInsets(); + BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); + params.width = sTempRect.width(); + params.gravity = Gravity.LEFT; + setLayoutParams(params); + setX(sTempRect.left - insets.left); + setY(sTempRect.top + getResources().getDimension(R.dimen.task_thumbnail_top_margin) + - insets.top); + } + + private void animateOpen() { + animateOpenOrClosed(false); + mIsOpen = true; + } + + private void animateClose() { + animateOpenOrClosed(true); + } + + private void animateOpenOrClosed(boolean closing) { + if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { + return; + } + mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet(); + + final Animator revealAnimator = createOpenCloseOutlineProvider() + .createRevealAnimator(this, closing); + revealAnimator.setInterpolator(Interpolators.DEACCEL); + mOpenCloseAnimator.play(revealAnimator); + mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA, + closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); + mOpenCloseAnimator.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationStart(Animator animation) { + setVisibility(VISIBLE); + } + + @Override + public void onAnimationSuccess(Animator animator) { + if (closing) { + closeComplete(); + } + } + }); + mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); + mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); + mOpenCloseAnimator.start(); + } + + private void closeComplete() { + mIsOpen = false; + mActivity.getDragLayer().removeView(this); + } + + private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { + float radius = getResources().getDimension(R.dimen.task_corner_radius); + Rect fromRect = new Rect(0, 0, getWidth(), 0); + Rect toRect = new Rect(0, 0, getWidth(), getHeight()); + return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java new file mode 100644 index 0000000000000000000000000000000000000000..0a0c27333fb80be8f9d1ee9a68bec788288f8a8c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Property; +import android.view.View; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.quickstep.TaskOverlayFactory; +import com.android.quickstep.TaskOverlayFactory.TaskOverlay; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; + +/** + * A task in the Recents view. + */ +public class TaskThumbnailView extends View { + + private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256]; + private static final LightingColorFilter[] sHighlightFilterCache = new LightingColorFilter[256]; + + public static final Property DIM_ALPHA_MULTIPLIER = + new FloatProperty("dimAlphaMultiplier") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlphaMultiplier) { + thumbnail.setDimAlphaMultipler(dimAlphaMultiplier); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlphaMultiplier; + } + }; + + public static final Property DIM_ALPHA = + new FloatProperty("dimAlpha") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { + thumbnail.setDimAlpha(dimAlpha); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlpha; + } + }; + + private final float mCornerRadius; + + private final BaseActivity mActivity; + private final TaskOverlay mOverlay; + private final boolean mIsDarkTextTheme; + private final Paint mPaint = new Paint(); + private final Paint mBackgroundPaint = new Paint(); + + private final Matrix mMatrix = new Matrix(); + + private float mClipBottom = -1; + + private Task mTask; + private ThumbnailData mThumbnailData; + protected BitmapShader mBitmapShader; + + private float mDimAlpha = 1f; + private float mDimAlphaMultiplier = 1f; + + public TaskThumbnailView(Context context) { + this(context, null); + } + + public TaskThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius); + mOverlay = TaskOverlayFactory.get(context).createOverlay(this); + mPaint.setFilterBitmap(true); + mBackgroundPaint.setColor(Color.WHITE); + mActivity = BaseActivity.fromContext(context); + mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); + } + + public void bind() { + mOverlay.reset(); + } + + /** + * Updates this thumbnail. + */ + public void setThumbnail(Task task, ThumbnailData thumbnailData) { + mTask = task; + int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; + mPaint.setColor(color); + mBackgroundPaint.setColor(color); + + if (thumbnailData != null && thumbnailData.thumbnail != null) { + Bitmap bm = thumbnailData.thumbnail; + bm.prepareToDraw(); + mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mPaint.setShader(mBitmapShader); + mThumbnailData = thumbnailData; + updateThumbnailMatrix(); + } else { + mBitmapShader = null; + mThumbnailData = null; + mPaint.setShader(null); + mOverlay.reset(); + } + updateThumbnailPaintFilter(); + } + + public void setDimAlphaMultipler(float dimAlphaMultipler) { + mDimAlphaMultiplier = dimAlphaMultipler; + setDimAlpha(mDimAlpha); + } + + /** + * Sets the alpha of the dim layer on top of this view. + * + * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. + */ + public void setDimAlpha(float dimAlpha) { + mDimAlpha = dimAlpha; + updateThumbnailPaintFilter(); + } + + public float getDimAlpha() { + return mDimAlpha; + } + + public Rect getInsets() { + if (mThumbnailData != null) { + return mThumbnailData.insets; + } + return new Rect(); + } + + public int getSysUiStatusNavFlags() { + if (mThumbnailData != null) { + int flags = 0; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_STATUS + : SystemUiController.FLAG_DARK_STATUS; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_NAV + : SystemUiController.FLAG_DARK_NAV; + return flags; + } + return 0; + } + + @Override + protected void onDraw(Canvas canvas) { + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + } + + public float getCornerRadius() { + return mCornerRadius; + } + + public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, + float cornerRadius) { + // Draw the background in all cases, except when the thumbnail data is opaque + final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null + || mThumbnailData == null; + if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); + if (drawBackgroundOnly) { + return; + } + } + + if (mClipBottom > 0) { + canvas.save(); + canvas.clipRect(x, y, width, mClipBottom); + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + canvas.restore(); + } else { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + } + } + + private void updateThumbnailPaintFilter() { + int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); + if (mBitmapShader != null) { + LightingColorFilter filter = getDimmingColorFilter(mul, mIsDarkTextTheme); + mPaint.setColorFilter(filter); + mBackgroundPaint.setColorFilter(filter); + } else { + mPaint.setColorFilter(null); + mPaint.setColor(Color.argb(255, mul, mul, mul)); + } + invalidate(); + } + + private void updateThumbnailMatrix() { + boolean rotate = false; + mClipBottom = -1; + if (mBitmapShader != null && mThumbnailData != null) { + float scale = mThumbnailData.scale; + Rect thumbnailInsets = mThumbnailData.insets; + final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - + (thumbnailInsets.left + thumbnailInsets.right) * scale; + final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - + (thumbnailInsets.top + thumbnailInsets.bottom) * scale; + + final float thumbnailScale; + final DeviceProfile profile = mActivity.getDeviceProfile(); + + if (getMeasuredWidth() == 0) { + // If we haven't measured , skip the thumbnail drawing and only draw the background + // color + thumbnailScale = 0f; + } else { + final Configuration configuration = + getContext().getResources().getConfiguration(); + // Rotate the screenshot if not in multi-window mode + rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && + configuration.orientation != mThumbnailData.orientation && + !mActivity.isInMultiWindowModeCompat() && + mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; + // Scale the screenshot to always fit the width of the card. + thumbnailScale = rotate + ? getMeasuredWidth() / thumbnailHeight + : getMeasuredWidth() / thumbnailWidth; + } + + if (rotate) { + int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; + mMatrix.setRotate(90 * rotationDir); + int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top; + int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right; + mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale); + if (rotationDir == -1) { + // Crop the right/bottom side of the screenshot rather than left/top + float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight(); + mMatrix.postTranslate(0, -excessHeight); + } + // Move the screenshot to the thumbnail window (rotation moved it out). + if (rotationDir == 1) { + mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0); + } else { + mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth()); + } + } else { + mMatrix.setTranslate(-mThumbnailData.insets.left * scale, + -mThumbnailData.insets.top * scale); + } + mMatrix.postScale(thumbnailScale, thumbnailScale); + mBitmapShader.setLocalMatrix(mMatrix); + + float bitmapHeight = Math.max((rotate ? thumbnailWidth : thumbnailHeight) + * thumbnailScale, 0); + if (Math.round(bitmapHeight) < getMeasuredHeight()) { + mClipBottom = bitmapHeight; + } + mPaint.setShader(mBitmapShader); + } + + if (rotate) { + // The overlay doesn't really work when the screenshot is rotated, so don't add it. + mOverlay.reset(); + } else { + mOverlay.setTaskInfo(mTask, mThumbnailData, mMatrix); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateThumbnailMatrix(); + } + + private static LightingColorFilter getDimmingColorFilter(int intensity, boolean shouldLighten) { + intensity = Utilities.boundToRange(intensity, 0, 255); + if (intensity == 255) { + return null; + } + if (shouldLighten) { + if (sHighlightFilterCache[intensity] == null) { + int colorAdd = 255 - intensity; + sHighlightFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), + Color.argb(255, colorAdd, colorAdd, colorAdd)); + } + return sHighlightFilterCache[intensity]; + } else { + if (sDimFilterCache[intensity] == null) { + sDimFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), 0); + } + return sDimFilterCache[intensity]; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java new file mode 100644 index 0000000000000000000000000000000000000000..67cdef4076b97a5ea356d6f8cf4a78c9a1cefe4a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.os.Bundle; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.Toast; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.R; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.views.RecentsView.PageCallbacks; +import com.android.quickstep.views.RecentsView.ScrollState; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskCallbacks; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +import java.util.function.Consumer; + +import static android.widget.Toast.LENGTH_SHORT; +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER; + +/** + * A task in the Recents view. + */ +public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks { + + private static final String TAG = TaskView.class.getSimpleName(); + + /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ + private static final TimeInterpolator CURVE_INTERPOLATOR + = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; + + /** + * The alpha of a black scrim on a page in the carousel as it leaves the screen. + * In the resting position of the carousel, the adjacent pages have about half this scrim. + */ + public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; + + /** + * How much to scale down pages near the edge of the screen. + */ + private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; + + public static final long SCALE_ICON_DURATION = 120; + private static final long DIM_ANIM_DURATION = 700; + + public static final Property ZOOM_SCALE = + new FloatProperty("zoomScale") { + @Override + public void setValue(TaskView taskView, float v) { + taskView.setZoomScale(v); + } + + @Override + public Float get(TaskView taskView) { + return taskView.mZoomScale; + } + }; + + private Task mTask; + private TaskThumbnailView mSnapshotView; + private IconView mIconView; + private float mCurveScale; + private float mZoomScale; + private Animator mDimAlphaAnim; + + public TaskView(Context context) { + this(context, null); + } + + public TaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOnClickListener((view) -> { + if (getTask() == null) { + return; + } + launchTask(true /* animate */); + BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss( + Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this), + TaskUtils.getLaunchComponentKeyForTask(getTask().key)); + }); + setOutlineProvider(new TaskOutlineProvider(getResources())); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSnapshotView = findViewById(R.id.snapshot); + mIconView = findViewById(R.id.icon); + } + + /** + * Updates this task view to the given {@param task}. + */ + public void bind(Task task) { + if (mTask != null) { + mTask.removeCallback(this); + } + mTask = task; + mSnapshotView.bind(); + task.addCallback(this); + setContentDescription(task.titleDescription); + } + + public Task getTask() { + return mTask; + } + + public TaskThumbnailView getThumbnail() { + return mSnapshotView; + } + + public IconView getIconView() { + return mIconView; + } + + public void launchTask(boolean animate) { + launchTask(animate, (result) -> { + if (!result) { + notifyTaskLaunchFailed(TAG); + } + }, getHandler()); + } + + public void launchTask(boolean animate, Consumer resultCallback, + Handler resultCallbackHandler) { + if (mTask != null) { + final ActivityOptions opts; + if (animate) { + opts = BaseDraggingActivity.fromContext(getContext()) + .getActivityLaunchOptions(this); + } else { + opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0); + } + ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, + opts, resultCallback, resultCallbackHandler); + } + } + + @Override + public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { + mSnapshotView.setThumbnail(task, thumbnailData); + mIconView.setDrawable(task.icon); + mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this)); + mIconView.setOnLongClickListener(icon -> { + requestDisallowInterceptTouchEvent(true); + return TaskMenuView.showForTask(this); + }); + } + + @Override + public void onTaskDataUnloaded() { + mSnapshotView.setThumbnail(null, null); + mIconView.setDrawable(null); + mIconView.setOnLongClickListener(null); + } + + @Override + public void onTaskWindowingModeChanged() { + // Do nothing + } + + public void animateIconToScaleAndDim(float scale) { + mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start(); + mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale, + scale); + mDimAlphaAnim.setDuration(DIM_ANIM_DURATION); + mDimAlphaAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDimAlphaAnim = null; + } + }); + mDimAlphaAnim.start(); + } + + protected void setIconScaleAndDim(float iconScale) { + mIconView.animate().cancel(); + mIconView.setScaleX(iconScale); + mIconView.setScaleY(iconScale); + if (mDimAlphaAnim != null) { + mDimAlphaAnim.cancel(); + } + mSnapshotView.setDimAlphaMultipler(iconScale); + } + + public void resetVisualProperties() { + setZoomScale(1); + setTranslationX(0f); + setTranslationY(0f); + setTranslationZ(0); + setAlpha(1f); + setIconScaleAndDim(1); + } + + @Override + public void onPageScroll(ScrollState scrollState) { + float curveInterpolation = + CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); + + mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); + setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setPivotX((right - left) * 0.5f); + setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); + } + + public static float getCurveScaleForInterpolation(float linearInterpolation) { + float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); + return getCurveScaleForCurveInterpolation(curveInterpolation); + } + + private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { + return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; + } + + private void setCurveScale(float curveScale) { + mCurveScale = curveScale; + onScaleChanged(); + } + + public float getCurveScale() { + return mCurveScale; + } + + public void setZoomScale(float adjacentScale) { + mZoomScale = adjacentScale; + onScaleChanged(); + } + + private void onScaleChanged() { + float scale = mCurveScale * mZoomScale; + setScaleX(scale); + setScaleY(scale); + } + + @Override + public boolean hasOverlappingRendering() { + // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. + return false; + } + + private static final class TaskOutlineProvider extends ViewOutlineProvider { + + private final int mMarginTop; + private final float mRadius; + + TaskOutlineProvider(Resources res) { + mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + mRadius = res.getDimension(R.dimen.task_corner_radius); + } + + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, mMarginTop, view.getWidth(), + view.getHeight(), mRadius); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + info.addAction( + new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task, + getContext().getText(R.string.accessibility_close_task))); + + final Context context = getContext(); + final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(context); + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + OnClickListener onClickListener = menuOption.getOnClickListener(activity, this); + if (onClickListener != null) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId, + context.getText(menuOption.labelResId))); + } + } + + final RecentsView recentsView = getRecentsView(); + final AccessibilityNodeInfo.CollectionItemInfo itemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain( + 0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1, + false); + info.setCollectionItemInfo(itemInfo); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action == R.string.accessibility_close_task) { + getRecentsView().dismissTask(this, true /*animateTaskView*/, + true /*removeTask*/); + return true; + } + + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + if (action == menuOption.labelResId) { + OnClickListener onClickListener = menuOption.getOnClickListener( + BaseDraggingActivity.fromContext(getContext()), this); + if (onClickListener != null) { + onClickListener.onClick(this); + } + return true; + } + } + + return super.performAccessibilityAction(action, arguments); + } + + private RecentsView getRecentsView() { + return (RecentsView) getParent(); + } + + public void notifyTaskLaunchFailed(String tag) { + String msg = "Failed to launch task"; + if (mTask != null) { + msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; + } + Log.w(tag, msg); + Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java new file mode 100644 index 0000000000000000000000000000000000000000..3e49925cb50a98add4bc8ad565146702d7030278 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; + +import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; + +/** + * Definition for AllApps state + */ +public class AllAppsState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY; + + private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + + public AllAppsState(int id) { + super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, STATE_FLAGS); + } + + @Override + public void onStateEnabled(Launcher launcher) { + AbstractFloatingView.closeAllOpenViews(launcher); + dispatchWindowStateChanged(launcher); + } + + @Override + public String getDescription(Launcher launcher) { + AllAppsContainerView appsView = launcher.getAppsView(); + return appsView.getDescription(); + } + + @Override + public float getVerticalProgress(Launcher launcher) { + return 0f; + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + float[] scaleAndTranslation = LauncherState.OVERVIEW.getWorkspaceScaleAndTranslation( + launcher); + scaleAndTranslation[0] = 1; + return scaleAndTranslation; + } + + @Override + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return PAGE_ALPHA_PROVIDER; + } + + @Override + public int getVisibleElements(Launcher launcher) { + return ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {0.9f, -0.2f}; + } + + @Override + public LauncherState getHistoryForState(LauncherState previousState) { + return previousState == OVERVIEW ? OVERVIEW : NORMAL; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..f2917f770707932360156df85b06531d9434dba4 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.quickstep.OverviewInteractionState; + +public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler { + + private static final String TAG = "BackButtonAlphaHandler"; + + private final Launcher mLauncher; + private final OverviewInteractionState mOverviewInteractionState; + + public BackButtonAlphaHandler(Launcher launcher) { + mLauncher = launcher; + mOverviewInteractionState = OverviewInteractionState.getInstance(mLauncher); + } + + @Override + public void setState(LauncherState state) { + UiFactory.onLauncherStateOrFocusChanged(mLauncher); + } + + @Override + public void setStateWithAnimation(LauncherState toState, + AnimatorSetBuilder builder, LauncherStateManager.AnimationConfig config) { + if (!config.playNonAtomicComponent()) { + return; + } + float fromAlpha = mOverviewInteractionState.getBackButtonAlpha(); + float toAlpha = toState.hideBackButton ? 0 : 1; + if (Float.compare(fromAlpha, toAlpha) != 0) { + ValueAnimator anim = ValueAnimator.ofFloat(fromAlpha, toAlpha); + anim.setDuration(config.duration); + anim.addUpdateListener(valueAnimator -> { + final float alpha = (float) valueAnimator.getAnimatedValue(); + mOverviewInteractionState.setBackButtonAlpha(alpha, false); + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Reapply the final alpha in case some state (e.g. window focus) changed. + UiFactory.onLauncherStateOrFocusChanged(mLauncher); + } + }); + builder.play(anim); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c51fb8f8b2f45d0a46b15fb1196a9e7cb34848e3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.shared.system.RotationWatcher; + +/** + * Utility class for listening for rotation changes + */ +public class DisplayRotationListener extends RotationWatcher { + + private final Runnable mCallback; + private Handler mHandler; + + public DisplayRotationListener(Context context, Runnable callback) { + super(context); + mCallback = callback; + } + + @Override + public void enable() { + if (mHandler == null) { + mHandler = new Handler(); + } + super.enable(); + } + + @Override + protected void onRotationChanged(int i) { + mHandler.post(mCallback); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java new file mode 100644 index 0000000000000000000000000000000000000000..1da29d2dd3a89ba03f9b4c2f4c21e2ba14acfe16 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.views.RecentsView; + +/** + * Extension of overview state used for QuickScrub + */ +public class FastOverviewState extends OverviewState { + + private static final float MAX_PREVIEW_SCALE_UP = 1.3f; + /** + * Vertical transition of the task previews relative to the full container. + */ + public static final float OVERVIEW_TRANSLATION_FACTOR = 0.4f; + + private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_DISABLE_INTERACTION + | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON | FLAG_DISABLE_ACCESSIBILITY; + + public FastOverviewState(int id) { + super(id, QuickScrubController.QUICK_SCRUB_FROM_HOME_START_DURATION, STATE_FLAGS); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + super.onStateTransitionEnd(launcher); + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + public int getVisibleElements(Launcher launcher) { + return NONE; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getTaskSize(sTempRect); + + return new float[] {getOverviewScale(launcher.getDeviceProfile(), sTempRect, launcher), + OVERVIEW_TRANSLATION_FACTOR}; + } + + public static float getOverviewScale(DeviceProfile dp, Rect taskRect, Context context) { + if (dp.isVerticalBarLayout()) { + return 1f; + } + + Resources res = context.getResources(); + float usedHeight = taskRect.height() + res.getDimension(R.dimen.task_thumbnail_top_margin); + float usedWidth = taskRect.width() + 2 * (res.getDimension(R.dimen.recents_page_spacing) + + res.getDimension(R.dimen.quickscrub_adjacent_visible_width)); + return Math.min(Math.min(dp.availableHeightPx / usedHeight, + dp.availableWidthPx / usedWidth), MAX_PREVIEW_SCALE_UP); + } + + @Override + public void onStateDisabled(Launcher launcher) { + super.onStateDisabled(launcher); + launcher.getOverviewPanel().getQuickScrubController().cancelActiveQuickscrub(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java new file mode 100644 index 0000000000000000000000000000000000000000..1d00e4ec17c0c4a73832be259bcd8b754f58aa7d --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java @@ -0,0 +1,79 @@ +package foundation.e.blisslauncher.uioverrides; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.quickstep.RecentsModel; + +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR; + +/** + * Touch controller for handling edge swipes in landscape/seascape UI + */ +public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchController { + + private static final String TAG = "LandscapeEdgeSwipeCtrl"; + + public LandscapeEdgeSwipeController(Launcher l) { + super(l, SwipeDetector.HORIZONTAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + boolean draggingFromNav = mLauncher.getDeviceProfile().isSeascape() != isDragTowardPositive; + return draggingFromNav ? OVERVIEW : NORMAL; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.NAVBAR; + } + + @Override + protected float getShiftRange() { + return mLauncher.getDragLayer().getWidth(); + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponent) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState, + maxAccuracy, animComponent); + return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range; + } + + @Override + protected int getDirectionForLog() { + return mLauncher.getDeviceProfile().isSeascape() ? Direction.RIGHT : Direction.LEFT; + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java new file mode 100644 index 0000000000000000000000000000000000000000..026c58f4f53b295015b132fd4076f7d3a82ccffe --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.Workspace; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; + +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; +import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; + +/** + * Definition for overview state + */ +public class OverviewState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED + | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY; + + public OverviewState(int id) { + this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS); + } + + protected OverviewState(int id, int transitionDuration, int stateFlags) { + super(id, ContainerType.TASKSWITCHER, transitionDuration, stateFlags); + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + Workspace workspace = launcher.getWorkspace(); + View workspacePage = workspace.getPageAt(workspace.getCurrentPage()); + float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0 + ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx; + recentsView.getTaskSize(sTempRect); + float scale = (float) sTempRect.width() / workspacePageWidth; + float parallaxFactor = 0.5f; + return new float[]{scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor}; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {1f, 0f}; + } + + @Override + public void onStateEnabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(true); + AbstractFloatingView.closeAllOpenViews(launcher); + } + + @Override + public void onStateDisabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(false); + RecentsModel.getInstance(launcher).resetAssistCache(); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE); + DiscoveryBounce.showForOverviewIfNeeded(launcher); + } + + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + } + + @Override + public int getVisibleElements(Launcher launcher) { + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return VERTICAL_SWIPE_INDICATOR; + } else { + return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | + (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent() + ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS); + } + } + + @Override + public float getWorkspaceScrimAlpha(Launcher launcher) { + return 0.5f; + } + + @Override + public float getVerticalProgress(Launcher launcher) { + if ((getVisibleElements(launcher) & ALL_APPS_HEADER_EXTRA) == 0) { + // We have no all apps content, so we're still at the fully down progress. + return super.getVerticalProgress(launcher); + } + return 1 - (getDefaultSwipeHeight(launcher) + / launcher.getAllAppsController().getShiftRange()); + } + + @Override + public String getDescription(Launcher launcher) { + return launcher.getString(R.string.accessibility_desc_recent_apps); + } + + public static float getDefaultSwipeHeight(Launcher launcher) { + DeviceProfile dp = launcher.getDeviceProfile(); + return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java new file mode 100644 index 0000000000000000000000000000000000000000..b988ab86de559b31a7490d6c94b4ab30f654bc7a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; + +/** + * Touch controller from going from OVERVIEW to ALL_APPS. + * + * This is used in landscape mode. It is also used in portrait mode for the fallback recents. + */ +public class OverviewToAllAppsTouchController extends PortraitStatesTouchController { + + public OverviewToAllAppsTouchController(Launcher l) { + super(l); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + return mLauncher.getAppsView().shouldContainerScroll(ev); + } else if (mLauncher.isInState(NORMAL)) { + return true; + } else if (mLauncher.isInState(OVERVIEW)) { + RecentsView rv = mLauncher.getOverviewPanel(); + return ev.getY() > (rv.getBottom() - rv.getPaddingBottom()); + } else { + return false; + } + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (isDragTowardPositive) { + return ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.WORKSPACE; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java new file mode 100644 index 0000000000000000000000000000000000000000..acbde49fdaad09a87530bd695c7ae0820f79a6f7 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.MotionEvent; +import android.view.animation.Interpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +/** + * Touch controller for handling various state transitions in portrait UI. + */ +public class PortraitStatesTouchController extends AbstractStateChangeTouchController { + + private static final String TAG = "PortraitStatesTouchCtrl"; + + /** + * The progress at which all apps content will be fully visible when swiping up from overview. + */ + private static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f; + + /** + * The progress at which recents will begin fading out when swiping up from overview. + */ + private static final float RECENTS_FADE_THRESHOLD = 0.88f; + + private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper(); + + // If true, we will finish the current animation instantly on second touch. + private boolean mFinishFastOnSecondTouch; + + + public PortraitStatesTouchController(Launcher l) { + super(l, SwipeDetector.VERTICAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + if (mFinishFastOnSecondTouch) { + // TODO: Animate to finish instead. + mCurrentAnimation.getAnimationPlayer().end(); + } + + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()) { + // If we are already animating from a previous state, we can intercept as long as + // the touch is below the current all apps progress (to allow for double swipe). + return true; + } + // Otherwise, make sure everything is settled and don't intercept so they can scroll + // recents, dismiss a task, etc. + if (mAtomicAnim != null) { + mAtomicAnim.end(); + } + return false; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + if (!mLauncher.getAppsView().shouldContainerScroll(ev)) { + return false; + } + } else { + // For all other states, only listen if the event originated below the hotseat height + DeviceProfile dp = mLauncher.getDeviceProfile(); + int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; + if (ev.getY() < (mLauncher.getDragLayer().getHeight() - hotseatHeight)) { + return false; + } + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return true; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (fromState == OVERVIEW) { + return isDragTowardPositive ? ALL_APPS : NORMAL; + } else if (fromState == NORMAL && isDragTowardPositive) { + return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return ContainerType.HOTSEAT; + } + + private AnimatorSetBuilder getNormalToOverviewAnimation() { + mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; + + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper); + return builder; + } + + public static AnimatorSetBuilder getOverviewToAllAppsAnimation() { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, + 0, ALL_APPS_CONTENT_FADE_THRESHOLD)); + builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL, + RECENTS_FADE_THRESHOLD, 1)); + return builder; + } + + private AnimatorSetBuilder getAllAppsToOverviewAnimation() { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, + 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1)); + builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL, + 0f, 1 - RECENTS_FADE_THRESHOLD)); + return builder; + } + + @Override + protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState, + LauncherState toState) { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + if (fromState == NORMAL && toState == OVERVIEW) { + builder = getNormalToOverviewAnimation(); + } else if (fromState == OVERVIEW && toState == ALL_APPS) { + builder = getOverviewToAllAppsAnimation(); + } else if (fromState == ALL_APPS && toState == OVERVIEW) { + builder = getAllAppsToOverviewAnimation(); + } + return builder; + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponents) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + + float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range; + float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range; + + float totalShift = endVerticalShift - startVerticalShift; + + final AnimatorSetBuilder builder = totalShift == 0 ? new AnimatorSetBuilder() + : getAnimatorSetBuilderForStates(mFromState, mToState); + + cancelPendingAnim(); + + RecentsView recentsView = mLauncher.getOverviewPanel(); + TaskView taskView = recentsView.getTaskViewAt(recentsView.getNextPage()); + if (recentsView.shouldSwipeDownLaunchApp() && mFromState == OVERVIEW && mToState == NORMAL + && taskView != null) { + // Reset the state manager, when changing the interaction mode + mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */); + mPendingAnimation = recentsView.createTaskLauncherAnimation(taskView, maxAccuracy); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + Runnable onCancelRunnable = () -> { + cancelPendingAnim(); + clearState(); + }; + mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy, + onCancelRunnable); + mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); + } else { + mCurrentAnimation = mLauncher.getStateManager() + .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState, + animComponents); + } + + if (totalShift == 0) { + totalShift = Math.signum(mFromState.ordinal - mToState.ordinal) + * OverviewState.getDefaultSwipeHeight(mLauncher); + } + return 1 / totalShift; + } + + private void cancelPendingAnim() { + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } + + @Override + protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, + LauncherState targetState, float velocity, boolean isFling) { + super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, + velocity, isFling); + handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); + } + + private void handleFirstSwipeToOverview(final ValueAnimator animator, + final long expectedDuration, final LauncherState targetState, final float velocity, + final boolean isFling) { + if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) { + mFinishFastOnSecondTouch = true; + if (isFling && expectedDuration != 0) { + // Update all apps interpolator to add a bit of overshoot starting from currFraction + final float currFraction = mCurrentAnimation.getProgressFraction(); + mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress( + Interpolators.overshootInterpolatorForVelocity(velocity), currFraction, 1); + animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION)) + .setInterpolator(LINEAR); + } + } else { + mFinishFastOnSecondTouch = false; + } + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } + + private static class InterpolatorWrapper implements Interpolator { + + public TimeInterpolator baseInterpolator = LINEAR; + + @Override + public float getInterpolation(float v) { + return baseInterpolator.getInterpolation(v); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java new file mode 100644 index 0000000000000000000000000000000000000000..807cbcb54ef5cbde959caf8d2513a2f861863658 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationConfig; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.PropertySetter; +import com.android.quickstep.views.LauncherRecentsView; + +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; + +@TargetApi(Build.VERSION_CODES.O) +public class RecentsViewStateController implements StateHandler { + + private final Launcher mLauncher; + private final LauncherRecentsView mRecentsView; + + public RecentsViewStateController(Launcher launcher) { + mLauncher = launcher; + mRecentsView = launcher.getOverviewPanel(); + } + + @Override + public void setState(LauncherState state) { + mRecentsView.setContentAlpha(state.overviewUi ? 1 : 0); + float[] scaleTranslationYFactor = state.getOverviewScaleAndTranslationYFactor(mLauncher); + SCALE_PROPERTY.set(mRecentsView, scaleTranslationYFactor[0]); + mRecentsView.setTranslationYFactor(scaleTranslationYFactor[1]); + if (state.overviewUi) { + mRecentsView.updateEmptyMessage(); + mRecentsView.resetTaskVisuals(); + } + } + + @Override + public void setStateWithAnimation(final LauncherState toState, + AnimatorSetBuilder builder, AnimationConfig config) { + if (!config.playAtomicComponent()) { + // The entire recents animation is played atomically. + return; + } + PropertySetter setter = config.getPropertySetter(builder); + float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher); + Interpolator scaleAndTransYInterpolator = builder.getInterpolator( + ANIM_OVERVIEW_SCALE, LINEAR); + if (mLauncher.getStateManager().getState() == OVERVIEW && toState == FAST_OVERVIEW) { + scaleAndTransYInterpolator = Interpolators.clampToProgress( + QUICK_SCRUB_START_INTERPOLATOR, 0, QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleTranslationYFactor[0], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsView, CONTENT_ALPHA, toState.overviewUi ? 1 : 0, + builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); + + if (!toState.overviewUi) { + builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals); + } + + if (toState.overviewUi) { + ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1); + updateAnim.addUpdateListener(valueAnimator -> { + // While animating into recents, update the visible task data as needed + mRecentsView.loadVisibleTaskData(); + }); + updateAnim.setDuration(config.duration); + builder.play(updateAnim); + mRecentsView.updateEmptyMessage(); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java new file mode 100644 index 0000000000000000000000000000000000000000..2730234f108ac22901cad2c6299e0701f966b755 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.content.SharedPreferences; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; +import com.android.launcher3.touch.TouchEventTranslator; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.RecentsModel; +import com.android.systemui.shared.recents.ISystemUiProxy; + +public class StatusBarTouchController implements TouchController { + private static final String TAG = StatusBarTouchController.class.getSimpleName(); + private static final String PREF_STATUSBAR_EXPAND = "pref_expand_statusbar"; + + private boolean mCanIntercept; + private ISystemUiProxy mSysUiProxy; + + private final Launcher mLauncher; + private final SharedPreferences mSharedPreferences; + private final float mTouchSlop; + + protected final TouchEventTranslator mTranslator = + new TouchEventTranslator(this::dispatchTouchEvent); + + public StatusBarTouchController(Launcher launcher) { + mLauncher = launcher; + mSharedPreferences = Utilities.getPrefs(launcher); + mTouchSlop = ViewConfiguration.get(launcher).getScaledTouchSlop() * 2; + } + + private void dispatchTouchEvent(MotionEvent ev) { + try { + if (mSysUiProxy != null) { + mSysUiProxy.onStatusBarMotionEvent(ev); + } + } catch (RemoteException e) { + Log.e(TAG, "Remote exception on sysUiProxy.", e); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mCanIntercept = canInterceptTouch(ev); + if (!mCanIntercept) { + return false; + } + mTranslator.reset(); + mTranslator.setDownParameters(0, ev); + } else if (action == MotionEvent.ACTION_POINTER_DOWN) { + mTranslator.setDownParameters(ev.getActionIndex(), ev); + } + + if (mCanIntercept && action == MotionEvent.ACTION_MOVE) { + float dy = ev.getY() - mTranslator.getDownY(); + float dx = ev.getX() - mTranslator.getDownX(); + if (dy > mTouchSlop && dy > Math.abs(dx)) { + mTranslator.dispatchDownEvents(ev); + mTranslator.processMotionEvent(ev); + return true; + } else if (Math.abs(dx) > mTouchSlop) { + mCanIntercept = false; + } + } + return false; + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + mTranslator.processMotionEvent(ev); + return true; + } + + private boolean canInterceptTouch(MotionEvent ev) { + if (!mSharedPreferences.getBoolean(PREF_STATUSBAR_EXPAND, true)) { + return false; + } + + if (mLauncher.isInState(LauncherState.NORMAL)) { + if (AbstractFloatingView.getTopOpenViewWithType( + mLauncher, AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) == null) { + if (ev.getY() > mLauncher.getDragLayer().getHeight() - + mLauncher.getDeviceProfile().getInsets().bottom) { + return false; + } + mSysUiProxy = RecentsModel.getInstance(mLauncher).getSystemUiProxy(); + return mSysUiProxy != null; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java new file mode 100644 index 0000000000000000000000000000000000000000..8444ed2b15543866d74819ade8f5745437a80773 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; + +/** + * Touch controller for handling task view card swipes + */ +public abstract class TaskViewTouchController + extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { + + private static final String TAG = "OverviewSwipeController"; + + // Progress after which the transition is assumed to be a success in case user does not fling + private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; + + protected final T mActivity; + private final SwipeDetector mDetector; + private final RecentsView mRecentsView; + private final int[] mTempCords = new int[2]; + + private PendingAnimation mPendingAnimation; + private AnimatorPlaybackController mCurrentAnimation; + private boolean mCurrentAnimationIsGoingUp; + + private boolean mNoIntercept; + + private float mDisplacementShift; + private float mProgressMultiplier; + private float mEndDisplacement; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + private TaskView mTaskBeingDragged; + + public TaskViewTouchController(T activity) { + mActivity = activity; + mRecentsView = activity.getOverviewPanel(); + mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL); + } + + private boolean canInterceptTouch() { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mActivity) != null) { + return false; + } + return isRecentsInteractive(); + } + + protected abstract boolean isRecentsInteractive(); + + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + } + + @Override + public void onAnimationCancel(Animator animation) { + if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { + clearState(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !canInterceptTouch(); + if (mNoIntercept) { + return false; + } + + // Now figure out which direction scroll events the controller will start + // calling the callbacks. + int directionsToDetectScroll = 0; + boolean ignoreSlopWhenSettling = false; + if (mCurrentAnimation != null) { + directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + ignoreSlopWhenSettling = true; + } else { + mTaskBeingDragged = null; + + for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) { + TaskView view = mRecentsView.getTaskViewAt(i); + if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() + .isEventOverView(view, ev)) { + mTaskBeingDragged = view; + if (!OverviewInteractionState.getInstance(mActivity) + .isSwipeUpGestureEnabled()) { + // Don't allow swipe down to open if we don't support swipe up + // to enter overview. + directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + } else { + // The task can be dragged up to dismiss it, + // and down to open if it's the current page. + directionsToDetectScroll = i == mRecentsView.getCurrentPage() + ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; + } + break; + } + } + if (mTaskBeingDragged == null) { + mNoIntercept = true; + return false; + } + } + + mDetector.setDetectableScrollConditions( + directionsToDetectScroll, ignoreSlopWhenSettling); + } + + if (mNoIntercept) { + return false; + } + + onControllerTouchEvent(ev); + return mDetector.isDraggingOrSettling(); + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + private void reInitAnimationController(boolean goingUp) { + if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { + // No need to init + return; + } + int scrollDirections = mDetector.getScrollDirections(); + if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) + || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { + // Trying to re-init in an unsupported direction. + return; + } + if (mCurrentAnimation != null) { + mCurrentAnimation.setPlayFraction(0); + } + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + + mCurrentAnimationIsGoingUp = goingUp; + BaseDragLayer dl = mActivity.getDragLayer(); + long maxDuration = (long) (2 * dl.getHeight()); + + if (goingUp) { + mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, + true /* animateTaskView */, true /* removeTask */, maxDuration); + + mEndDisplacement = -mTaskBeingDragged.getHeight(); + } else { + mPendingAnimation = mRecentsView.createTaskLauncherAnimation( + mTaskBeingDragged, maxDuration); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + mTempCords[1] = mTaskBeingDragged.getHeight(); + dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords); + mEndDisplacement = dl.getHeight() - mTempCords[1]; + } + + if (mCurrentAnimation != null) { + mCurrentAnimation.setOnCancelRunnable(null); + } + mCurrentAnimation = AnimatorPlaybackController + .wrap(mPendingAnimation.anim, maxDuration, this::clearState); + onUserControlledAnimationCreated(mCurrentAnimation); + mCurrentAnimation.getTarget().addListener(this); + mCurrentAnimation.dispatchOnStart(); + mProgressMultiplier = 1 / mEndDisplacement; + } + + @Override + public void onDragStart(boolean start) { + if (mCurrentAnimation == null) { + reInitAnimationController(mDetector.wasInitialTouchPositive()); + mDisplacementShift = 0; + } else { + mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; + mCurrentAnimation.pause(); + } + mFlingBlockCheck.unblockFling(); + } + + @Override + public boolean onDrag(float displacement, float velocity) { + float totalDisplacement = displacement + mDisplacementShift; + boolean isGoingUp = + totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; + if (isGoingUp != mCurrentAnimationIsGoingUp) { + reInitAnimationController(isGoingUp); + mFlingBlockCheck.blockFling(); + } else { + mFlingBlockCheck.onEvent(); + } + mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); + return true; + } + + @Override + public void onDragEnd(float velocity, boolean fling) { + final boolean goingToEnd; + final int logAction; + boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + fling = false; + } + float progress = mCurrentAnimation.getProgressFraction(); + float interpolatedProgress = mCurrentAnimation.getInterpolator().getInterpolation(progress); + if (fling) { + logAction = Touch.FLING; + boolean goingUp = velocity < 0; + goingToEnd = goingUp == mCurrentAnimationIsGoingUp; + } else { + logAction = Touch.SWIPE; + goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; + } + long animationDuration = SwipeDetector.calculateDuration( + velocity, goingToEnd ? (1 - progress) : progress); + if (blockedFling && !goingToEnd) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); + } + + float nextFrameProgress = Utilities.boundToRange( + progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); + + mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction)); + + ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); + anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); + anim.setDuration(animationDuration); + anim.setInterpolator(scrollInterpolatorForVelocity(velocity)); + anim.start(); + } + + private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) { + if (mPendingAnimation != null) { + mPendingAnimation.finish(wasSuccess, logAction); + mPendingAnimation = null; + } + clearState(); + } + + private void clearState() { + mDetector.finishedScrolling(); + mDetector.setDetectableScrollConditions(0, false); + mTaskBeingDragged = null; + mCurrentAnimation = null; + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..dc93d2548e8225e60046102c647a0d207abcad6f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.os.CancellationSignal; +import android.util.Base64; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppTransitionManagerImpl; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.RemoteFadeOutAnimationListener; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.zip.Deflater; + +import static android.view.View.VISIBLE; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN; +import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN; + +public class UiFactory { + + public static TouchController[] createTouchControllers(Launcher launcher) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + if (!swipeUpEnabled) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LandscapeEdgeSwipeController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } else { + return new TouchController[] { + launcher.getDragController(), + new PortraitStatesTouchController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } + } + + public static void setOnTouchControllersChangedListener(Context context, Runnable listener) { + OverviewInteractionState.getInstance(context).setOnSwipeUpSettingChangedListener(listener); + } + + public static StateHandler[] getStateHandler(Launcher launcher) { + return new StateHandler[] {launcher.getAllAppsController(), launcher.getWorkspace(), + new RecentsViewStateController(launcher), new BackButtonAlphaHandler(launcher)}; + } + + /** + * Sets the back button visibility based on the current state/window focus. + */ + public static void onLauncherStateOrFocusChanged(Launcher launcher) { + boolean shouldBackButtonBeHidden = launcher != null + && launcher.getStateManager().getState().hideBackButton + && launcher.hasWindowFocus(); + if (shouldBackButtonBeHidden) { + // Show the back button if there is a floating view visible. + shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher, + TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null; + } + OverviewInteractionState.getInstance(launcher) + .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */); + } + + public static void resetOverview(Launcher launcher) { + RecentsView recents = launcher.getOverviewPanel(); + recents.reset(); + } + + public static void onCreate(Launcher launcher) { + if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled + && finalState == ALL_APPS && prevState == NORMAL))) { + launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + + if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (finalState == ALL_APPS && prevState == OVERVIEW) { + launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + } + + public static void onStart(Context context) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onStart(); + } + } + + public static void onEnterAnimationComplete(Context context) { + // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled + // as a part of quickstep/scrub, so that high-res thumbnails can load the next time we + // enter overview + RecentsModel.getInstance(context).getRecentsTaskLoader() + .getHighResThumbnailLoader().setVisible(true); + } + + public static void onLauncherStateOrResumeChanged(Launcher launcher) { + LauncherState state = launcher.getStateManager().getState(); + DeviceProfile profile = launcher.getDeviceProfile(); + WindowManagerWrapper.getInstance().setShelfHeight( + (state == NORMAL || state == OVERVIEW) && launcher.isUserActive() + && !profile.isVerticalBarLayout(), + profile.hotseatBarSizePx); + + if (state == NORMAL) { + launcher.getOverviewPanel().setSwipeDownShouldLaunchApp(false); + } + } + + public static void onTrimMemory(Context context, int level) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onTrimMemory(level); + } + } + + public static void useFadeOutAnimationForLauncherStart(Launcher launcher, + CancellationSignal cancellationSignal) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager(); + appTransitionManager.setRemoteAnimationProvider((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + + ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0); + fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(targets)); + AnimatorSet anim = new AnimatorSet(); + anim.play(fadeAnimation); + return anim; + }, cancellationSignal); + } + + public static boolean dumpActivity(Activity activity, PrintWriter writer) { + if (!Utilities.IS_DEBUG_DEVICE) { + return false; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) { + return false; + } + + Deflater deflater = new Deflater(); + deflater.setInput(out.toByteArray()); + deflater.finish(); + + out.reset(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); // returns the generated code... index + out.write(buffer, 0, count); + } + + writer.println("--encoded-view-dump-v0--"); + writer.println(Base64.encodeToString( + out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING)); + return true; + } + + public static void prepareToShowOverview(Launcher launcher) { + RecentsView overview = launcher.getOverviewPanel(); + if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) { + SCALE_PROPERTY.set(overview, 1.33f); + } + } + + private static class LauncherTaskViewController extends TaskViewTouchController { + + public LauncherTaskViewController(Launcher activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.isInState(OVERVIEW); + } + + @Override + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + mActivity.getStateManager().setCurrentUserControlledAnimation(animController); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..76f35f5253968b6878effa67212f2226c64afc68 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.annotation.TargetApi; +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import com.android.systemui.shared.system.TonalCompat; +import com.android.systemui.shared.system.TonalCompat.ExtractionInfo; + +import java.util.ArrayList; + +import static android.app.WallpaperManager.FLAG_SYSTEM; + +@TargetApi(Build.VERSION_CODES.P) +public class WallpaperColorInfo implements OnColorsChangedListener { + + private static final Object sInstanceLock = new Object(); + private static WallpaperColorInfo sInstance; + + public static WallpaperColorInfo getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new WallpaperColorInfo(context.getApplicationContext()); + } + return sInstance; + } + } + + private final ArrayList mListeners = new ArrayList<>(); + private final WallpaperManager mWallpaperManager; + private final TonalCompat mTonalCompat; + + private ExtractionInfo mExtractionInfo; + + private OnChangeListener[] mTempListeners = new OnChangeListener[0]; + + private WallpaperColorInfo(Context context) { + mWallpaperManager = context.getSystemService(WallpaperManager.class); + mTonalCompat = new TonalCompat(context); + + mWallpaperManager.addOnColorsChangedListener(this, new Handler(Looper.getMainLooper())); + update(mWallpaperManager.getWallpaperColors(FLAG_SYSTEM)); + } + + public int getMainColor() { + return mExtractionInfo.mainColor; + } + + public int getSecondaryColor() { + return mExtractionInfo.secondaryColor; + } + + public boolean isDark() { + return mExtractionInfo.supportsDarkTheme; + } + + public boolean supportsDarkText() { + return mExtractionInfo.supportsDarkText; + } + + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if ((which & FLAG_SYSTEM) != 0) { + update(colors); + notifyChange(); + } + } + + private void update(WallpaperColors wallpaperColors) { + mExtractionInfo = mTonalCompat.extractDarkColors(wallpaperColors); + } + + public void addOnChangeListener(OnChangeListener listener) { + mListeners.add(listener); + } + + public void removeOnChangeListener(OnChangeListener listener) { + mListeners.remove(listener); + } + + private void notifyChange() { + // Create a new array to avoid concurrent modification when the activity destroys itself. + mTempListeners = mListeners.toArray(mTempListeners); + for (OnChangeListener listener : mTempListeners) { + if (listener != null) { + listener.onExtractedColorsChanged(this); + } + } + } + + public interface OnChangeListener { + void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo); + } +} diff --git a/quickstep/src/main/res/values/strings.xml b/quickstep/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..32728d876405fc70023fb22b5e6af520453ce9ec --- /dev/null +++ b/quickstep/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Quickstep + diff --git a/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java b/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f4ecc5fa5f2ea24e1d70baaedd0ec2183ef0e932 --- /dev/null +++ b/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package foundation.e.blisslauncher.quickstep; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100755 index e70334ca2e10a41e1aa63b764f3f9d4b645171e5..0000000000000000000000000000000000000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -include ':app' -include ':data' -include ':domain' \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100755 index 0000000000000000000000000000000000000000..4657f28498efab39752f8c9cab5d28ddd747caad --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +include(":app", ":common", ":blisslauncherv2", ":data-bridge", ":data", ":domain", ":mvicore") \ No newline at end of file