diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index c6f1778dbcdc8335f6f9b690129be66983af3172..0000000000000000000000000000000000000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7c7b35133d522706e3e57af3ab5526871272cbc0..44fc010f9f8416713f82ea7a4df63255901b661b 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { versionName "${versionMajor}.${versionMinor}.${versionPatch}" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - renderscriptTargetApi 28 + renderscriptTargetApi 31 renderscriptSupportModeEnabled true } buildTypes { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0e1e0913179799e75858d0ea2a0b6e6ebeaf7aa..9ee0c994577dc7a5e639510c84406a4d8b74e84e 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,9 +64,9 @@ - + - HokoBlur.with(context) - .scheme(HokoBlur.SCHEME_NATIVE) - .mode(HokoBlur.MODE_STACK) + .scheme(HokoBlur.SCHEME_RENDER_SCRIPT) + .mode(HokoBlur.MODE_BOX) .radius(blurRadius) .sampleFactor(8f) .forceCopy(false) 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 000b9c933535c6bb3ed53803e51c7848531801e6..4e9425340490defa77df23b777d5ffa78651007f 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 @@ -27,6 +27,7 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: } private val blurBounds = RectF() + var canvasOffset: Float = 0f private val blurPath = Path() private var blurPathValid = false set(value) { @@ -44,6 +45,7 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: fun draw(canvas: Canvas, noRadius: Boolean = false) { if (blurAlpha == 0) return + if (blurBounds.right.toInt() - blurBounds.left.toInt() == 0) return blurBitmap = blurWallpaperProvider.wallpaper if (blurBitmap == null) { @@ -65,7 +67,11 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: // setupBlurPath() - // canvas.translate(0f, -1500f) + // We check the offset just to make sure we don't translate it incorrectly + // when moving back to home screen from widget page. + if (canvasOffset > 0) { + canvas.translate(canvasOffset, 0f) + } if (noRadius) { canvas.drawRect( 0f, 0f, @@ -75,12 +81,15 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: } else { canvas.drawPath(DeviceProfile.path, blurPaint) } - // canvas.translate(0f, 1500f) + + if (canvasOffset > 0) + canvas.translate(-canvasOffset, 0f) } override fun setAlpha(alpha: Int) { blurAlpha = alpha blurPaint.alpha = alpha + invalidateSelf() } override fun getAlpha(): Int { diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/AbstractFloatingView.java b/app/src/main/java/foundation/e/blisslauncher/core/customviews/AbstractFloatingView.java index 612a6f2340f098f14eef4163e02b895d0a8a18be..e0205171b089468293b9de99374047c746d4433d 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/AbstractFloatingView.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/AbstractFloatingView.java @@ -40,6 +40,7 @@ import foundation.e.blisslauncher.features.test.TouchController; public abstract class AbstractFloatingView extends LinearLayout implements TouchController { @IntDef(flag = true, value = { + TYPE_FOLDER, TYPE_TASK_MENU, TYPE_OPTIONS_POPUP, TYPE_LISTENER @@ -48,13 +49,15 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public @interface FloatingViewType { } + public static final int TYPE_FOLDER = 1 << 0; public static final int TYPE_LISTENER = 1 << 1; // Popups related to quickstep UI public static final int TYPE_TASK_MENU = 1 << 2; public static final int TYPE_OPTIONS_POPUP = 1 << 3; - public static final int TYPE_ALL = TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_LISTENER; + public static final int TYPE_ALL = + TYPE_FOLDER | TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_LISTENER; public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_LISTENER; diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/Folder.kt b/app/src/main/java/foundation/e/blisslauncher/core/customviews/Folder.kt new file mode 100644 index 0000000000000000000000000000000000000000..a616cb04dbd5ee0e1a60cc382f52cea40836e4a7 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/Folder.kt @@ -0,0 +1,795 @@ +package foundation.e.blisslauncher.core.customviews + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.text.InputType +import android.text.Selection +import android.util.AttributeSet +import android.util.Log +import android.view.FocusFinder +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.ViewDebug.ExportedProperty +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import androidx.viewpager.widget.ViewPager +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.core.database.model.FolderItem +import foundation.e.blisslauncher.core.database.model.LauncherItem +import foundation.e.blisslauncher.features.folder.FolderAnimationManager +import foundation.e.blisslauncher.features.folder.FolderIcon +import foundation.e.blisslauncher.features.folder.FolderPagerAdapter +import foundation.e.blisslauncher.features.folder.FolderViewPager +import foundation.e.blisslauncher.features.test.Alarm +import foundation.e.blisslauncher.features.test.BaseDragLayer +import foundation.e.blisslauncher.features.test.CellLayout +import foundation.e.blisslauncher.features.test.LauncherRootView +import foundation.e.blisslauncher.features.test.OnAlarmListener +import foundation.e.blisslauncher.features.test.TestActivity +import foundation.e.blisslauncher.features.test.VariantDeviceProfile +import foundation.e.blisslauncher.features.test.dragndrop.DragController +import foundation.e.blisslauncher.features.test.dragndrop.DragLayer +import foundation.e.blisslauncher.features.test.dragndrop.DragOptions +import foundation.e.blisslauncher.features.test.dragndrop.DragSource +import foundation.e.blisslauncher.features.test.dragndrop.DropTarget +import java.util.ArrayList +import kotlinx.android.synthetic.main.activity_test.* +import me.relex.circleindicator.CircleIndicator + +class Folder @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AbstractFloatingView(context, attrs), DragController.DragListener, + FolderTitleInput.OnBackKeyListener, FolderItem.FolderListener, View.OnFocusChangeListener, + OnEditorActionListener, DragSource, DropTarget { + + private var mScrollAreaOffset: Int = 0 + private val MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10 + + private val mOnExitAlarm: Alarm = Alarm() + val mItemsInReadingOrder = ArrayList() + + val launcher: TestActivity + var dragController: DragController? = null + lateinit var mInfo: FolderItem + private var mCurrentAnimator: AnimatorSet? = null + + var folderIcon: FolderIcon? = null + + lateinit var mContent: FolderViewPager + lateinit var mFolderTitleInput: FolderTitleInput + private lateinit var mPageIndicator: CircleIndicator + + var mPrevTargetRank = 0 + var mEmptyCellRank = 0 + + var mState: Int = STATE_NONE + + private var mRearrangeOnClose = false + var mItemsInvalidated = false + private var mCurrentDragView: View? = null + private var mIsExternalDrag = false + private var mDragInProgress = false + private var mDeleteFolderOnDropCompleted = false + private var mSuppressFolderDeletion = false + private var mItemAddedBackToSelfViaIcon = false + + var mFolderIconPivotX = 0f + + private var mIsEditingName = false + + @ExportedProperty(category = "launcher") + private var mDestroyed = false + + init { + setLocaleDependentFields(resources, false /* force */) + launcher = TestActivity.getLauncher(context) + isFocusableInTouchMode = true + } + + override fun onFinishInflate() { + super.onFinishInflate() + mContent = findViewById(R.id.folder_apps) + + mPageIndicator = findViewById(R.id.indicator) + mFolderTitleInput = findViewById(R.id.folder_title) + mFolderTitleInput.setOnBackKeyListener(this) + mFolderTitleInput.onFocusChangeListener = this + mFolderTitleInput.setOnEditorActionListener(this) + mFolderTitleInput.setSelectAllOnFocus(true) + mFolderTitleInput.inputType = mFolderTitleInput.inputType and + InputType.TYPE_TEXT_FLAG_AUTO_CORRECT.inv() and + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS.inv() or + InputType.TYPE_TEXT_FLAG_CAP_WORDS + mFolderTitleInput.forceDisableSuggestions(true) + mFolderTitleInput.clearFocus() + } + + fun startDrag(v: View, options: DragOptions): Boolean { + val tag = v.tag + if (tag is LauncherItem) { + val item: LauncherItem = tag + mEmptyCellRank = item.cell + mCurrentDragView = v + dragController!!.addDragListener(this) + launcher.getLauncherPagedView().beginDragShared(v, this, options) + } + return true + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { + /*ev?.let { + if (it.action == MotionEvent.ACTION_DOWN) { + val dl: DragLayer = launcher.dragLayer + if (isEditingName()) { + if (!dl.isEventOverView(mFolderTitleInput, ev)) { + mFolderTitleInput.dispatchBackKey() + return true + } + return false + } else if (!dl.isEventOverView(this, ev)) { + close(true) + return true + } + } + }*/ + return false + } + + override fun handleClose(animate: Boolean) { + mIsOpen = false + + if (!animate && mCurrentAnimator != null && mCurrentAnimator!!.isRunning) { + mCurrentAnimator?.cancel() + } + + if (isEditingName()) { + mFolderTitleInput.dispatchBackKey() + } + + if (folderIcon != null) { + // mFolderIcon.clearLeaveBehindIfExists() + } + + if (animate) { + animateClosed() + } else { + closeComplete(false) + } + } + + private fun animateClosed() { + val a = FolderAnimationManager(this, false /* isOpening */).animator + a.play(ObjectAnimator.ofFloat(launcher.getLauncherPagedView(), View.ALPHA, 1f)) + .with(ObjectAnimator.ofFloat(launcher.hotseat, View.ALPHA, 1f)) + .with( + ObjectAnimator.ofFloat( + launcher.getLauncherPagedView().pageIndicator, + View.ALPHA, + 1f + ) + ) + .with(ObjectAnimator.ofInt(launcher.rootView, LauncherRootView.BLUR_ALPHA, 0)) + a.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + closeComplete(true) + } + }) + startAnimation(a) + } + + private fun closeComplete(wasAnimated: Boolean) { + // TODO: Clear all active animations. + (this.parent as DragLayer?)?.removeView(this) + dragController?.removeDropTarget(this) + clearFocus() + folderIcon?.apply { + launcher.getLauncherPagedView().alpha = 1f + launcher.getLauncherPagedView().pageIndicator.alpha = 1f + launcher.hotseat.alpha = 1f + if (wasAnimated) { + if (this.hasDot()) { + this.animateDotScale(0f, 1f) + } + } + } + + if (mRearrangeOnClose) { + rearrangeChildren() + mRearrangeOnClose = false + } + if (getItemCount() <= 1) { + if (!mDragInProgress && !mSuppressFolderDeletion) { + replaceFolderWithFinalItem() + } else if (mDragInProgress) { + mDeleteFolderOnDropCompleted = true + } + } + mSuppressFolderDeletion = false + clearDragInfo() + mState = STATE_NONE + mContent.currentItem = 0 + } + + override fun isOfType(type: Int): Boolean = type and TYPE_FOLDER != 0 + + override fun onBackKey(): Boolean { + // Convert to a string here to ensure that no other state associated with the text field + // gets saved. + val newTitle: String = mFolderTitleInput.text.toString() + mInfo.setTitle(newTitle) + + // Update database + launcher.getLauncherPagedView().updateDatabase() + + // This ensures that focus is gained every time the field is clicked, which selects all + // the text and brings up the soft keyboard if necessary. + mFolderTitleInput.clearFocus() + + Selection.setSelection(mFolderTitleInput.text, 0, 0) + mIsEditingName = false + return true + } + + // This is used so the item doesn't immediately appear in the folder when added. In one case + // we need to create the illusion that the item isn't added back to the folder yet, to + // to correspond to the animation of the icon back into the folder. This is + fun hideItem(info: LauncherItem) { + getViewForInfo(info)?.apply { + this.clearAnimation() + this.visibility = INVISIBLE + } + } + + fun showItem(info: LauncherItem) { + getViewForInfo(info)?.apply { + this.clearAnimation() + this.visibility = VISIBLE + } + } + + override fun onAdd(item: LauncherItem) { + // mContent.adapter?.notifyDataSetChanged() + } + + override fun onTitleChanged(title: CharSequence?) {} + + override fun onRemove(item: LauncherItem) { + Log.d(TAG, "onRemove() called with: item = $item") + mItemsInvalidated = true + val v: View? = getViewForInfo(item) + mContent.adapter?.notifyDataSetChanged() + if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true + } else { + rearrangeChildren() + } + if (getItemCount() <= 1) { + if (mIsOpen) { + close(true) + } else { + replaceFolderWithFinalItem() + } + } + } + + private fun getViewForInfo(item: LauncherItem): View? { + return mContent.iterateOverItems { info, _, _ -> info === item } + } + + override fun onItemsChanged(animate: Boolean) { + mContent.adapter?.notifyDataSetChanged() + updateTextViewFocus() + invalidate() + } + + override fun onDragStart(dragObject: DropTarget.DragObject, options: DragOptions) { + if (dragObject.dragSource != this) { + return + } + mCurrentDragView?.clearAnimation() + hideItem(dragObject.dragInfo) + if (dragObject.dragInfo is LauncherItem) { + mItemsInvalidated = true + SuppressInfoChanges().use { _ -> + // mInfo?.remove(dragObject.dragInfo, true) + } + } + mDragInProgress = true + mItemAddedBackToSelfViaIcon = false + } + + override fun onDragEnd() { + if (mIsExternalDrag && mDragInProgress) { + completeDragExit() + } + mDragInProgress = false + dragController?.removeDragListener(this) + } + + fun isEditingName(): Boolean { + return mIsEditingName + } + + private fun startEditingFolderName() { + post { + mFolderTitleInput.hint = "" + mIsEditingName = true + } + } + + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mFolderTitleInput.dispatchBackKey() + return true + } + return false + } + + override fun onAttachedToWindow() { + // requestFocus() causes the focus onto the folder itself, which doesn't cause visual + // effect but the next arrow key can start the keyboard focus inside of the folder, not + // the folder itself. + requestFocus() + super.onAttachedToWindow() + } + + override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean = + // When the folder gets focus, we don't want to announce the list of items. + true + + override fun focusSearch(direction: Int): View? = + // When the folder is focused, further focus search should be within the folder contents. + FocusFinder.getInstance().findNextFocus(this, null, direction) + + /** + * @return the FolderInfo object associated with this folder + */ + fun getInfo(): FolderItem? { + return mInfo + } + + fun bind(info: FolderItem) { + mInfo = info + val children: MutableList = info.items.toMutableList() + children.sortWith { lhs, rhs -> + lhs.cell - rhs.cell + } + + mItemsInvalidated = true + updateTextViewFocus() + mInfo.addListener(this) + + mFolderTitleInput.setText(mInfo.title) + + val mDeviceProfile: VariantDeviceProfile = launcher.deviceProfile + mContent.adapter = + FolderPagerAdapter(context, mInfo.items, mDeviceProfile) + mContent.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int + ) { + } + + override fun onPageSelected(position: Int) { + updateTextViewFocus() + } + + override fun onPageScrollStateChanged(state: Int) { + } + }) + // We use same size for height and width as we want to look it like square + mContent.layoutParams?.width = + mDeviceProfile.cellHeightPx * 3 + resources.getDimensionPixelSize(R.dimen.folder_padding) * 2 + mContent.layoutParams?.height = + (mDeviceProfile.cellHeightPx + mDeviceProfile.iconDrawablePaddingPx * 2) * 3 + resources.getDimensionPixelSize( + R.dimen.folder_padding + ) * 2 + // In case any children didn't come across during loading, clean up the folder accordingly + folderIcon?.post { + if (getItemCount() <= 1) { + replaceFolderWithFinalItem() + } + } + } + + private fun startAnimation(a: AnimatorSet) { + if (mCurrentAnimator != null && mCurrentAnimator!!.isRunning) { + mCurrentAnimator?.cancel() + } + val workspace: LauncherPagedView = launcher.getLauncherPagedView() + val currentCellLayout: CellLayout = + workspace.getChildAt(workspace.currentPage) as CellLayout + val useHardware = shouldUseHardwareLayerForAnimation(currentCellLayout) + val wasHardwareAccelerated: Boolean = currentCellLayout.isHardwareLayerEnabled() + a.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + if (useHardware) { + currentCellLayout.enableHardwareLayer(true) + } + mState = STATE_ANIMATING + mCurrentAnimator = a + } + + override fun onAnimationEnd(animation: Animator) { + if (useHardware) { + currentCellLayout.enableHardwareLayer(wasHardwareAccelerated) + } + mCurrentAnimator = null + } + }) + a.start() + } + + private fun shouldUseHardwareLayerForAnimation(currentCellLayout: CellLayout): Boolean { + var folderCount = 0 + for (i in currentCellLayout.childCount - 1 downTo 0) { + val child: View = currentCellLayout.getChildAt(i) + if (child is FolderIcon) ++folderCount + } + return folderCount >= MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION + } + + /** + * Opens the user folder described by the specified tag. The opening of the folder + * is animated relative to the specified View. If the View is null, no animation + * is played. + */ + fun animateOpen() { + val openFolder = getOpen(launcher) + if (openFolder != null && openFolder !== this) { + // Close any open folder before opening a folder. + openFolder.close(true) + } + mIsOpen = true + val dragLayer = launcher.dragLayer + // Just verify that the folder hasn't already been added to the DragLayer. + // There was a one-off crash where the folder had a parent already. + if (parent == null) { + mContent.adapter = + FolderPagerAdapter(context, mInfo.items, launcher.deviceProfile) + mPageIndicator.setViewPager(mContent) + + dragLayer.addView( + this, + BaseDragLayer.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + dragController?.addDropTarget(this) + } else { + Log.e( + TAG, + "Opening folder (" + this + ") which already has a parent:" + + parent + ) + } + // mContent.completePendingPageChanges() + if (!mDragInProgress) { + // Open on the first page. + mContent.currentItem = 0 + } + + // This is set to true in close(), but isn't reset to false until onDropCompleted(). This + // leads to an inconsistent state if you drag out of the folder and drag back in without + // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. + mDeleteFolderOnDropCompleted = false + // centerAboutIcon() + + val anim: AnimatorSet = FolderAnimationManager(this, true /* isOpening */).animator + anim.play(ObjectAnimator.ofFloat(launcher.getLauncherPagedView(), View.ALPHA, 0f)) + .with(ObjectAnimator.ofFloat(launcher.hotseat, View.ALPHA, 0f)) + .with( + ObjectAnimator.ofFloat( + launcher.getLauncherPagedView().pageIndicator, + View.ALPHA, + 0f + ) + ) + .with(ObjectAnimator.ofInt(launcher.rootView, LauncherRootView.BLUR_ALPHA, 0, 255)) + anim.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + } + + override fun onAnimationEnd(animation: Animator) { + mState = STATE_OPEN + launcher.getLauncherPagedView().alpha = 0f + launcher.hotseat.alpha = 0f + launcher.getLauncherPagedView().pageIndicator.alpha = 0f + launcher.rootView.blurAlpha = 255 + mContent.setFocusOnFirstChild() + } + + override fun onAnimationCancel(animation: Animator?) { + launcher.getLauncherPagedView().alpha = 1f + launcher.hotseat.alpha = 1f + launcher.rootView.blurAlpha = 0 + launcher.getLauncherPagedView().pageIndicator.alpha = 1f + } + }) + startAnimation(anim) + + // Make sure the folder picks up the last drag move even if the finger doesn't move. + if (dragController!!.isDragging) { + dragController!!.forceTouchMove() + } + } + + fun completeDragExit() { + when { + mIsOpen -> { + close(true) + mRearrangeOnClose = true + } + mState == STATE_ANIMATING -> { + mRearrangeOnClose = true + } + else -> { + rearrangeChildren() + clearDragInfo() + } + } + } + + private fun clearDragInfo() { + mCurrentDragView = null + mIsExternalDrag = false + } + + /** + * Rearranges the children based on their rank. + */ + fun rearrangeChildren() { + rearrangeChildren(-1) + } + + /** + * Rearranges the children based on their rank. + * @param itemCount if greater than the total children count, empty spaces are left at the end, + * otherwise it is ignored. + */ + private fun rearrangeChildren(itemCount: Int) { + mContent.adapter?.notifyDataSetChanged() + mItemsInvalidated = true + } + + fun isDestroyed(): Boolean { + return mDestroyed + } + + private fun replaceFolderWithFinalItem() { + // Add the last remaining child to the workspace in place of the folder + val onCompleteRunnable = Runnable { + val itemCount: Int = mInfo.items.size + if (itemCount <= 1) { + var finalItem: LauncherItem? = null + if (itemCount == 1) { + // Move the item from the folder to the workspace, in the position of the + // folder + finalItem = mInfo.items.removeAt(0) + finalItem?.apply { + cell = mInfo.cell + screenId = mInfo.screenId + container = mInfo.container + } + } + + // Remove the folder + launcher.getLauncherPagedView().removeItem(folderIcon, mInfo /* deleteFromDb */) + if (finalItem != null) { + // We add the child after removing the folder to prevent both from existing + // at the same time in the CellLayout. We need to add the new item with + // addInScreenFromBind() to ensure that hotseat items are placed correctly. + launcher.getLauncherPagedView().bindItems(listOf(finalItem), true) + } + launcher.getLauncherPagedView().updateDatabase() + } + } + onCompleteRunnable.run() + mDestroyed = true + } + + fun getItemCount(): Int { + Log.i(TAG, "getItemCount: " + mContent.getItemCount() + " " + mInfo.items.size) + return mInfo.items.size + } + + // This method keeps track of the first and last item in the folder for the purposes + // of keyboard focus + fun updateTextViewFocus() { + val firstChild: View? = mContent.getFirstItem() + val lastChild: View? = mContent.getLastItem() + if (firstChild != null && lastChild != null) { + mFolderTitleInput.nextFocusDownId = lastChild.id + mFolderTitleInput.nextFocusRightId = lastChild.id + mFolderTitleInput.nextFocusLeftId = lastChild.id + mFolderTitleInput.nextFocusUpId = lastChild.id + // Hitting TAB from the folder name wraps around to the first item on the current + // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name. + mFolderTitleInput.nextFocusForwardId = firstChild.id + // When clicking off the folder when editing the name, this Folder gains focus. When + // pressing an arrow key from that state, give the focus to the first item. + this.nextFocusDownId = firstChild.id + this.nextFocusRightId = firstChild.id + this.nextFocusLeftId = firstChild.id + this.nextFocusUpId = firstChild.id + // When pressing shift+tab in the above state, give the focus to the last item. + setOnKeyListener { _, keyCode, event -> + val isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB && + event.hasModifiers(KeyEvent.META_SHIFT_ON) + if (isShiftPlusTab && this@Folder.isFocused) { + lastChild.requestFocus() + } else false + } + } + } + + var mOnExitAlarmListener: OnAlarmListener = OnAlarmListener { completeDragExit() } + + override fun onDropCompleted(target: View?, d: DropTarget.DragObject?, success: Boolean) { + if (success) { + if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target !== this) { + replaceFolderWithFinalItem() + } + } else { + // The drag failed, we need to return the item to the folder + mContent.adapter = + FolderPagerAdapter(context, mInfo.items, launcher.deviceProfile) + launcher.dragLayer.removeView(d?.dragView) + d?.dragView = null + invalidate() + launcher.getLauncherPagedView().wobbleLayouts() + } + + mDeleteFolderOnDropCompleted = false + mDragInProgress = false + mItemAddedBackToSelfViaIcon = false + mCurrentDragView = null + + // Reordering may have occurred, and we need to save the new item locations. We do this once + // at the end to prevent unnecessary database operations. + launcher.getLauncherPagedView().updateDatabase() + } + + override fun onBackPressed(): Boolean { + if (isEditingName()) { + mFolderTitleInput.dispatchBackKey() + } else { + super.onBackPressed() + } + return true + } + + override fun onFocusChange(v: View?, hasFocus: Boolean) { + if (v === mFolderTitleInput) { + if (hasFocus) { + startEditingFolderName() + } else { + mFolderTitleInput.dispatchBackKey() + } + } + } + + fun getItemsInReadingOrder(): ArrayList { + if (mItemsInvalidated) { + mItemsInReadingOrder.clear() + mContent.iterateOverItems { _, view, _ -> + mItemsInReadingOrder.add(view) + false + } + mItemsInvalidated = false + } + return mItemsInReadingOrder + } + + companion object { + const val STATE_NONE = -1 + const val STATE_ANIMATING = 1 + const val STATE_OPEN = 2 + + private const val ON_EXIT_CLOSE_DELAY = 400L + + const val TAG = "Folder" + + private var sDefaultFolderName: String? = null + + /** + * Creates a new UserFolder, inflated from R.layout.user_folder. + * + * @param launcher The main activity. + * + * @return A new UserFolder. + */ + @SuppressLint("InflateParams") + fun fromXml(launcher: TestActivity): Folder { + return launcher.layoutInflater.inflate(R.layout.layout_folder, null) as Folder + } + + /** + * Returns a folder which is already open or null + */ + fun getOpen(launcher: TestActivity?): Folder? { + return getOpenView(launcher, TYPE_FOLDER) + } + + fun setLocaleDependentFields(res: Resources, force: Boolean) { + if (sDefaultFolderName == null || force) { + sDefaultFolderName = res.getString(R.string.untitled) + } + } + } + + /** + * Temporary resource held while we don't want to handle info changes + */ + inner class SuppressInfoChanges internal constructor() : AutoCloseable { + override fun close() { + mInfo.addListener(this@Folder) + updateTextViewFocus() + } + + init { + mInfo.removeListener(this@Folder) + } + } + + override fun isDropEnabled(): Boolean = mState != STATE_ANIMATING + + override fun onDrop(dragObject: DropTarget.DragObject?, options: DragOptions?) { + // Do nothing here as we don't allow to drop icon in folder. + } + + override fun onDragEnter(d: DropTarget.DragObject) { + mPrevTargetRank = -1 + mOnExitAlarm.cancelAlarm() + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + mScrollAreaOffset = d.dragView.dragRegionWidth / 2 - d.xOffset + } + + override fun onDragOver(dragObject: DropTarget.DragObject?) { + // Do Nothing here, we don't allow drop. + Log.d(TAG, "onDragOver() called with: dragObject = $dragObject") + } + + override fun onDragExit(d: DropTarget.DragObject) { + // We only close the folder if this is a true drag exit, ie. not because + // a drop has occurred above the folder. + if (!d.dragComplete) { + mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener) + mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY) + } + } + + override fun acceptDrop(dragObject: DropTarget.DragObject?): Boolean = false + + override fun prepareAccessibilityDrop() { + } + + override fun getHitRectRelativeToDragLayer(outRect: Rect?) { + launcher.dragLayer.getDescendantRectRelativeToSelf(mContent, outRect) + // mContent.getHitRect(outRect) + /*outRect!!.left -= mScrollAreaOffset + outRect!!.right += mScrollAreaOffset*/ + Log.i(TAG, "getHitRectRelativeToDragLayer: " + outRect) + } + + fun getContent(): ViewGroup { + return mContent + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/FolderTitleInput.kt b/app/src/main/java/foundation/e/blisslauncher/core/customviews/FolderTitleInput.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d388d58ed29fff2ad93f57b5bdef45fc2d25916 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/FolderTitleInput.kt @@ -0,0 +1,90 @@ +package foundation.e.blisslauncher.core.customviews + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.inputmethod.InputMethodManager +import foundation.e.blisslauncher.features.test.UiThreadHelper + +class FolderTitleInput @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : BlissInput(context, attrs) { + + private var mShowImeAfterFirstLayout = false + private var mForceDisableSuggestions = false + + /** + * Implemented by listeners of the back key. + */ + interface OnBackKeyListener { + fun onBackKey(): Boolean + } + + private var mBackKeyListener: OnBackKeyListener? = null + + fun setOnBackKeyListener(listener: OnBackKeyListener) { + mBackKeyListener = listener + } + + override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { + // If this is a back key, propagate the key back to the listener + return if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { + mBackKeyListener?.onBackKey() ?: false + } else super.onKeyPreIme(keyCode, event) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + if (mShowImeAfterFirstLayout) { + // soft input only shows one frame after the layout of the EditText happens, + post { + showSoftInput() + mShowImeAfterFirstLayout = false + } + } + } + + fun showKeyboard() { + mShowImeAfterFirstLayout = !showSoftInput() + } + + fun hideKeyboard() { + UiThreadHelper.hideKeyboardAsync(context, windowToken) + } + + private fun showSoftInput(): Boolean { + return requestFocus() && + (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + } + + fun dispatchBackKey() { + hideKeyboard() + mBackKeyListener?.onBackKey() + } + + /** + * Set to true when you want isSuggestionsEnabled to return false. + * Use this to disable the red underlines that appear under typos when suggestions is enabled. + */ + fun forceDisableSuggestions(forceDisableSuggestions: Boolean) { + mForceDisableSuggestions = forceDisableSuggestions + } + + override fun isSuggestionsEnabled(): Boolean { + return !mForceDisableSuggestions && super.isSuggestionsEnabled() + } + + fun reset() { + if (!TextUtils.isEmpty(text)) { + setText("") + } + if (isFocused) { + val nextFocus = focusSearch(FOCUS_DOWN) + nextFocus?.requestFocus() + } + hideKeyboard() + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/LauncherPagedView.java b/app/src/main/java/foundation/e/blisslauncher/core/customviews/LauncherPagedView.java index a23ed23b6816b0afe1f97e0257288626ed0ae7ee..a4006ee42684160825056ed4389d8f23ebf17015 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/LauncherPagedView.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/LauncherPagedView.java @@ -3,8 +3,8 @@ package foundation.e.blisslauncher.core.customviews; import static foundation.e.blisslauncher.core.utils.Constants.ITEM_TYPE_APPLICATION; import static foundation.e.blisslauncher.features.test.LauncherState.NORMAL; import static foundation.e.blisslauncher.features.test.anim.LauncherAnimUtils.SPRING_LOADED_TRANSITION_MS; -import static foundation.e.blisslauncher.features.test.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -13,16 +13,27 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.AlertDialog; import android.app.WallpaperManager; +import android.app.usage.UsageStats; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.location.LocationManager; +import android.os.Bundle; import android.os.UserHandle; +import android.provider.Settings; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.util.MutableInt; @@ -31,14 +42,31 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; +import android.view.inputmethod.EditorInfo; import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.jakewharton.rxbinding3.widget.RxTextView; + import foundation.e.blisslauncher.BuildConfig; import foundation.e.blisslauncher.R; +import foundation.e.blisslauncher.core.Preferences; import foundation.e.blisslauncher.core.Utilities; import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicatorDots; import foundation.e.blisslauncher.core.database.DatabaseManager; @@ -46,21 +74,33 @@ import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.FolderItem; import foundation.e.blisslauncher.core.database.model.LauncherItem; import foundation.e.blisslauncher.core.database.model.ShortcutItem; +import foundation.e.blisslauncher.core.executors.AppExecutors; import foundation.e.blisslauncher.core.touch.ItemClickHandler; import foundation.e.blisslauncher.core.touch.ItemLongClickListener; +import foundation.e.blisslauncher.core.touch.WorkspaceTouchListener; +import foundation.e.blisslauncher.core.utils.AppUtils; import foundation.e.blisslauncher.core.utils.Constants; import foundation.e.blisslauncher.core.utils.GraphicsUtil; import foundation.e.blisslauncher.core.utils.IntSparseArrayMap; import foundation.e.blisslauncher.core.utils.IntegerArray; +import foundation.e.blisslauncher.core.utils.ListUtil; import foundation.e.blisslauncher.core.utils.PackageUserKey; +import foundation.e.blisslauncher.features.folder.FolderIcon; import foundation.e.blisslauncher.features.launcher.Hotseat; +import foundation.e.blisslauncher.features.launcher.SearchInputDisposableObserver; import foundation.e.blisslauncher.features.notification.FolderDotInfo; import foundation.e.blisslauncher.features.shortcuts.DeepShortcutManager; import foundation.e.blisslauncher.features.shortcuts.InstallShortcutReceiver; import foundation.e.blisslauncher.features.shortcuts.ShortcutKey; +import foundation.e.blisslauncher.features.suggestions.AutoCompleteAdapter; +import foundation.e.blisslauncher.features.suggestions.SearchSuggestionUtil; +import foundation.e.blisslauncher.features.suggestions.SuggestionProvider; +import foundation.e.blisslauncher.features.suggestions.SuggestionsResult; import foundation.e.blisslauncher.features.test.Alarm; import foundation.e.blisslauncher.features.test.CellLayout; import foundation.e.blisslauncher.features.test.IconTextView; +import foundation.e.blisslauncher.features.test.LauncherItemMatcher; +import foundation.e.blisslauncher.features.test.LauncherRootView; import foundation.e.blisslauncher.features.test.LauncherState; import foundation.e.blisslauncher.features.test.LauncherStateManager; import foundation.e.blisslauncher.features.test.OnAlarmListener; @@ -78,22 +118,45 @@ import foundation.e.blisslauncher.features.test.dragndrop.DropTarget; import foundation.e.blisslauncher.features.test.dragndrop.SpringLoadedDragController; import foundation.e.blisslauncher.features.test.graphics.DragPreviewProvider; import foundation.e.blisslauncher.features.test.uninstall.UninstallHelper; +import foundation.e.blisslauncher.features.usagestats.AppUsageStats; +import foundation.e.blisslauncher.features.weather.DeviceStatusService; +import foundation.e.blisslauncher.features.weather.ForecastBuilder; +import foundation.e.blisslauncher.features.weather.WeatherPreferences; +import foundation.e.blisslauncher.features.weather.WeatherSourceListenerService; +import foundation.e.blisslauncher.features.weather.WeatherUpdateService; +import foundation.e.blisslauncher.features.weather.WeatherUtils; +import foundation.e.blisslauncher.features.widgets.WidgetManager; +import foundation.e.blisslauncher.features.widgets.WidgetPageLayer; +import foundation.e.blisslauncher.features.widgets.WidgetViewBuilder; +import foundation.e.blisslauncher.features.widgets.WidgetsActivity; +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; + import org.jetbrains.annotations.NotNull; public class LauncherPagedView extends PagedView implements View.OnTouchListener, Insettable, DropTarget, DragSource, DragController.DragListener, - LauncherStateManager.StateHandler, OnAlarmListener { + LauncherStateManager.StateHandler, OnAlarmListener, + AutoCompleteAdapter.OnSuggestionClickListener { private static final String TAG = "LauncherPagedView"; - private static final int DEFAULT_PAGE = 0; + private static final int DEFAULT_PAGE = 1; private static final int SNAP_OFF_EMPTY_SCREEN_DURATION = 400; private static final int FADE_EMPTY_SCREEN_DURATION = 150; @@ -158,7 +221,7 @@ public class LauncherPagedView extends PagedView implements V private int mDragOverY = -1; private static final int FOLDER_CREATION_TIMEOUT = 100; - public static final int REORDER_TIMEOUT = 650; + public static final int REORDER_TIMEOUT = 350; private final Alarm mFolderCreationAlarm = new Alarm(); private final Alarm mReorderAlarm = new Alarm(); //private FolderIcon mDragOverFolderIcon = null; @@ -198,6 +261,20 @@ public class LauncherPagedView extends PagedView implements V */ public final Map pinnedShortcutCounts = new HashMap<>(); + /** + * The value that {@link #mTransitionProgress} must be greater than for + * {@link #transitionStateShouldAllowDrop()} to return true. + */ + private static final float ALLOW_DROP_TRANSITION_PROGRESS = 0.25f; + private WidgetPageLayer widgetPage; + private LinearLayout widgetContainer; + private BlissInput mSearchInput; + private View mWeatherPanel; + private View mWeatherSetupTextView; + public RoundedWidgetView activeRoundedWidgetView; + private List mUsageStats; + private AnimatorSet currentAnimator; + public LauncherPagedView(Context context, AttributeSet attributeSet) { this(context, attributeSet, 0); } @@ -212,13 +289,13 @@ public class LauncherPagedView extends PagedView implements V initWorkspace(); setMotionEventSplittingEnabled(true); - setOnTouchListener((v, event) -> false); + setOnTouchListener(new WorkspaceTouchListener(mLauncher, this)); wobbleExpireAlarm.setOnAlarmListener(this); } private void initWorkspace() { - mCurrentPage = DEFAULT_PAGE; + mCurrentPage = 0; setClipToPadding(false); setupLayoutTransition(); //setWallpaperDimension(); @@ -244,10 +321,7 @@ public class LauncherPagedView extends PagedView implements V @Override public void onViewAdded(View child) { - if (!(child instanceof GridLayout)) { - throw new IllegalArgumentException("A Workspace can only have GridLayout children."); - } - GridLayout grid = (GridLayout) child; + // GridLayout grid = (GridLayout) child; //grid.setOnInterceptOnTouchListener(this); super.onViewAdded(child); } @@ -261,14 +335,14 @@ public class LauncherPagedView extends PagedView implements V mInsets.set(insets); VariantDeviceProfile grid = mLauncher.getDeviceProfile(); - mMaxDistanceForFolderCreation = (0.55f * grid.getIconSizePx()); + mMaxDistanceForFolderCreation = (0.35f * grid.getIconSizePx()); Rect padding = grid.getWorkspacePadding(); - setPadding(padding.left, padding.top, padding.right, padding.bottom); + setPadding(padding.left, padding.top, padding.right, 0); int paddingLeftRight = grid.getCellLayoutPaddingLeftRightPx(); int paddingBottom = grid.getCellLayoutBottomPaddingPx(); for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) { mWorkspaceScreens.valueAt(i) - .setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom); + .setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom + padding.bottom); } } @@ -277,7 +351,7 @@ public class LauncherPagedView extends PagedView implements V } public void bindAndInitFirstScreen(View view) { - // Do nothing here. + setupWidgetPage(); } public void removeAllWorkspaceScreens() { @@ -286,13 +360,13 @@ public class LauncherPagedView extends PagedView implements V disableLayoutTransitions(); // Remove the pages and clear the screen models - //removeFolderListeners(); + removeFolderListeners(); removeAllViews(); mScreenOrder.clear(); mWorkspaceScreens.clear(); // Ensure that the first page is always present - //bindAndInitFirstScreen(qsb); + bindAndInitFirstScreen(null); // Re-enable the layout transitions enableLayoutTransitions(); @@ -305,7 +379,7 @@ public class LauncherPagedView extends PagedView implements V if (insertIndex < 0) { insertIndex = mScreenOrder.size(); } - insertNewWorkspaceScreen(screenId, insertIndex); + insertNewWorkspaceScreen(screenId, insertIndex + 1); } public void bindScreens(@NotNull IntegerArray orderedScreenIds) { @@ -325,17 +399,32 @@ public class LauncherPagedView extends PagedView implements V ) { final Collection bounceAnims = new ArrayList<>(); int newItemsScreenId = -1; + List unhandledItems = new ArrayList<>(); for (int i = 0; i < launcherItems.size(); i++) { LauncherItem launcherItem = launcherItems.get(i); - IconTextView appView = (IconTextView) LayoutInflater.from(getContext()) - .inflate(R.layout.app_icon, null, false); - appView.applyFromShortcutItem(launcherItem); + View appView; + if (launcherItem.itemType == Constants.ITEM_TYPE_FOLDER) { + FolderIcon folderIcon = FolderIcon.Companion.fromXml( + R.layout.folder_icon, + getScreenWithId(launcherItem.screenId), + (FolderItem) launcherItem + ); + folderIcon.applyFromFolderItem((FolderItem) launcherItem); + appView = folderIcon; + } else { + IconTextView appIcon = (IconTextView) LayoutInflater.from(getContext()) + .inflate(R.layout.app_icon, null, false); + appIcon.applyFromShortcutItem(launcherItem); + appView = appIcon; + } appView.setOnClickListener(ItemClickHandler.INSTANCE); appView.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); if (launcherItem.container == Constants.CONTAINER_DESKTOP) { CellLayout cl = getScreenWithId(launcherItem.screenId); - if (cl != null && cl.isOccupied(launcherItem.cell)) { - // TODO: Add item to the end of the list + if ((cl != null && cl.isOccupied(launcherItem.cell))) { + // We add this item to unhandled and handle them later. + unhandledItems.add(launcherItem); + continue; } GridLayout.Spec rowSpec = GridLayout.spec(GridLayout.UNDEFINED); GridLayout.Spec colSpec = GridLayout.spec(GridLayout.UNDEFINED); @@ -344,7 +433,6 @@ public class LauncherPagedView extends PagedView implements V iconLayoutParams.height = mLauncher.getDeviceProfile().getCellHeightPx(); iconLayoutParams.width = mLauncher.getDeviceProfile().getCellWidthPx(); appView.setLayoutParams(iconLayoutParams); - appView.setTextVisibility(true); } else if (launcherItem.container == Constants.CONTAINER_HOTSEAT) { GridLayout.Spec rowSpec = GridLayout.spec(GridLayout.UNDEFINED); GridLayout.Spec colSpec = GridLayout.spec(GridLayout.UNDEFINED); @@ -365,6 +453,13 @@ public class LauncherPagedView extends PagedView implements V } } + // Handle unhandled items + if (!unhandledItems.isEmpty()) { + List tempItems = new ArrayList<>(unhandledItems); + bindItemsAdded(tempItems); + unhandledItems.clear(); + } + // Animate to the correct page if (animateIcons && newItemsScreenId > -1) { AnimatorSet anim = new AnimatorSet(); @@ -376,7 +471,7 @@ public class LauncherPagedView extends PagedView implements V if (newItemsScreenId != currentScreenId) { // We post the animation slightly delayed to prevent slowdowns // when we are loading right after we return to launcher. - this.postDelayed((Runnable) () -> { + this.postDelayed(() -> { AbstractFloatingView.closeAllOpenViews(mLauncher, false); snapToPage(newScreenIndex); @@ -426,7 +521,7 @@ public class LauncherPagedView extends PagedView implements V throw new RuntimeException("Unexpected info type"); } - if(item.itemType == Constants.ITEM_TYPE_SHORTCUT) { + if (item.itemType == Constants.ITEM_TYPE_SHORTCUT) { // Increment the count for the given shortcut ShortcutKey pinnedShortcut = ShortcutKey.fromItem((ShortcutItem) item); MutableInt count = pinnedShortcutCounts.get(pinnedShortcut); @@ -441,7 +536,6 @@ public class LauncherPagedView extends PagedView implements V if (count.value == 1) { DeepShortcutManager.getInstance(getContext()).pinShortcut(pinnedShortcut); } - } // Save the WorkspaceItemInfo for binding in the workspace addedItemsFinal.add(itemInfo); @@ -481,6 +575,526 @@ public class LauncherPagedView extends PagedView implements V } } + public void updateDatabase() { + updateDatabase(getWorkspaceAndHotseatCellLayouts()); + } + + private void setupWidgetPage() { + widgetPage = + (WidgetPageLayer) LayoutInflater.from(getContext()) + .inflate(R.layout.widgets_page, this, false); + this.addView(widgetPage, 0); + InsettableScrollLayout scrollView = widgetPage.findViewById(R.id.widgets_scroll_container); + scrollView.setOnTouchListener((v, event) -> { + if (widgetPage.findViewById(R.id.widget_resizer_container).getVisibility() + == VISIBLE) { + hideWidgetResizeContainer(); + } + return false; + }); + + widgetContainer = widgetPage.findViewById(R.id.widget_container); + + widgetPage.setVisibility(View.VISIBLE); +// widgetPage.post { +// widgetPage.translationX = -(widgetPage.measuredWidth * 1.00f) +// } + widgetPage.findViewById(R.id.used_apps_layout).setClipToOutline(true); + widgetPage.setTag("Widget page"); + + // TODO: replace with app predictions + // Prepare app suggestions view + // [[BEGIN]] + widgetPage.findViewById(R.id.openUsageAccessSettings) + .setOnClickListener(v -> mLauncher.startActivity( + new Intent( + Settings.ACTION_USAGE_ACCESS_SETTINGS + ) + )); + + // divided by 2 because of left and right padding. + final float emptySpace = mLauncher.getDeviceProfile().getAvailableWidthPx() - 4 * + mLauncher.getDeviceProfile().getCellWidthPx(); + int padding = (int) (emptySpace / 10); + widgetPage.findViewById(R.id.suggestedAppGrid) + .setPadding(padding, 0, padding, 0); + // [[END]] + + // Prepare search suggestion view + // [[BEGIN]] + mSearchInput = widgetPage.findViewById(R.id.search_input); + ImageView clearSuggestions = + widgetPage.findViewById(R.id.clearSuggestionImageView); + clearSuggestions.setOnClickListener(v -> { + mSearchInput.setText(""); + mSearchInput.clearFocus(); + }); + + mSearchInput.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (s.toString().trim().length() == 0) { + clearSuggestions.setVisibility(GONE); + } else { + clearSuggestions.setVisibility(VISIBLE); + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + RecyclerView suggestionRecyclerView = widgetPage.findViewById(R.id.suggestionRecyclerView); + AutoCompleteAdapter suggestionAdapter = new AutoCompleteAdapter(getContext(), this); + suggestionRecyclerView.setHasFixedSize(true); + suggestionRecyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); + suggestionRecyclerView.setAdapter(suggestionAdapter); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration( + getContext(), + DividerItemDecoration.VERTICAL + ); + suggestionRecyclerView.addItemDecoration(dividerItemDecoration); + mLauncher.getCompositeDisposable().add( + RxTextView.textChanges(mSearchInput) + .debounce(300, TimeUnit.MILLISECONDS) + .map(CharSequence::toString) + .distinctUntilChanged() + .switchMap(charSequence -> { + if (charSequence != null && charSequence.length() > 0) { + return searchForQuery(charSequence); + } else { + return Observable.just( + new SuggestionsResult(charSequence)); + } + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith( + new SearchInputDisposableObserver(mLauncher, suggestionAdapter, widgetPage) + ) + ); + + mSearchInput.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + mLauncher.hideKeyboard(v); + } + }); + + mSearchInput.setOnEditorActionListener((textView, action, keyEvent) -> { + if (action == EditorInfo.IME_ACTION_SEARCH) { + mLauncher.hideKeyboard(mSearchInput); + mLauncher.runSearch(mSearchInput.getText().toString()); + mSearchInput.setText(""); + mSearchInput.clearFocus(); + return true; + } + return false; + }); + // [[END]] + + // Prepare edit widgets button + findViewById(R.id.edit_widgets_button).setOnClickListener(v -> mLauncher.startActivity( + new Intent( + mLauncher, + WidgetsActivity.class + ) + )); + + // Prepare weather widget view + // [[BEGIN]] + findViewById(R.id.weather_setting_imageview) + .setOnClickListener(v -> mLauncher.startActivity( + new Intent( + mLauncher, + WeatherPreferences.class + ) + )); + + mWeatherSetupTextView = findViewById(R.id.weather_setup_textview); + mWeatherPanel = findViewById(R.id.weather_panel); + mWeatherPanel.setOnClickListener(v -> { + Intent launchIntent = mLauncher.getPackageManager().getLaunchIntentForPackage( + "foundation.e.weather"); + if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mLauncher.startActivity(launchIntent); + } + }); + updateWeatherPanel(); + + if (WeatherUtils.isWeatherServiceAvailable(mLauncher)) { + mLauncher.startService(new Intent(mLauncher, WeatherSourceListenerService.class)); + mLauncher.startService(new Intent(mLauncher, DeviceStatusService.class)); + } + + LocalBroadcastManager.getInstance(mLauncher) + .registerReceiver(mWeatherReceiver, new IntentFilter( + WeatherUpdateService.ACTION_UPDATE_FINISHED)); + + if (!Preferences.useCustomWeatherLocation(mLauncher)) { + if (!WeatherPreferences.hasLocationPermission(mLauncher)) { + String[] permissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION}; + mLauncher.requestPermissions( + permissions, + WeatherPreferences.LOCATION_PERMISSION_REQUEST_CODE + ); + } else { + LocationManager lm = + (LocationManager) mLauncher.getSystemService(Context.LOCATION_SERVICE); + if (!lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + && Preferences.getEnableLocation(mLauncher)) { + showLocationEnableDialog(); + Preferences.setEnableLocation(mLauncher); + } else { + mLauncher.startService(new Intent(mLauncher, WeatherUpdateService.class) + .setAction(WeatherUpdateService.ACTION_FORCE_UPDATE)); + } + } + } else { + mLauncher.startService(new Intent(mLauncher, WeatherUpdateService.class) + .setAction(WeatherUpdateService.ACTION_FORCE_UPDATE)); + } + // [[END]] + + int[] widgetIds = mLauncher.mAppWidgetHost.getAppWidgetIds(); + Arrays.sort(widgetIds); + for (int id : widgetIds) { + AppWidgetProviderInfo appWidgetInfo = mLauncher.mAppWidgetManager.getAppWidgetInfo(id); + if (appWidgetInfo != null) { + mLauncher.getCompositeDisposable() + .add(DatabaseManager.getManager(mLauncher).getHeightOfWidget(id) + .subscribeOn(Schedulers.from(AppExecutors.getInstance().diskIO())) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(height -> { + RoundedWidgetView hostView = + (RoundedWidgetView) mLauncher.getMAppWidgetHost().createView( + mLauncher.getApplicationContext(), id, + appWidgetInfo + ); + hostView.setAppWidget(id, appWidgetInfo); + addWidgetToContainer(hostView); + if (height != 0) { + int minHeight = hostView.getAppWidgetInfo().minResizeHeight; + int maxHeight = + mLauncher.getDeviceProfile().getAvailableHeightPx() * 3 / 4; + int normalisedDifference = (maxHeight - minHeight) / 100; + hostView.getLayoutParams().height = + minHeight + (normalisedDifference * height); + } + }, Throwable::printStackTrace)); + } + } + } + + private void addWidgetToContainer(RoundedWidgetView widgetView) { + int padding = getResources().getDimensionPixelSize(R.dimen.dp_8); + widgetView.setPadding(0, padding, 0, padding); + widgetContainer.addView(widgetView); + } + + private ObservableSource searchForQuery( + CharSequence charSequence + ) { + Observable launcherItems = searchForLauncherItems( + charSequence.toString() + ).subscribeOn(Schedulers.io()); + Observable networkItems = searchForNetworkItems( + charSequence + ).subscribeOn(Schedulers.io()); + return launcherItems.mergeWith(networkItems); + } + + private Observable searchForLauncherItems( + CharSequence charSequence + ) { + String query = charSequence.toString().toLowerCase(); + SuggestionsResult suggestionsResult = new SuggestionsResult( + query + ); + List launcherItems = new ArrayList(); + mapOverItems(true, new ItemOperator() { + @Override + public boolean evaluate(LauncherItem item, View view, int index) { + Log.i(TAG, "searchForLauncherItems: ${item.title}"); + if (item.title.toString().toLowerCase(Locale.getDefault()).contains(query)) { + launcherItems.add(item); + } + return false; + } + }); + launcherItems.sort(Comparator.comparing(launcherItem -> + launcherItem.title.toString().toLowerCase().indexOf(query) + )); + + if (launcherItems.size() > 4) { + suggestionsResult.setLauncherItems(launcherItems.subList(0, 4)); + } else { + suggestionsResult.setLauncherItems(launcherItems); + } + return Observable.just(suggestionsResult) + .onErrorReturn(throwable -> { + suggestionsResult.setLauncherItems(new ArrayList<>()); + return suggestionsResult; + }); + } + + private Observable searchForNetworkItems(CharSequence charSequence) { + String query = charSequence.toString().toLowerCase(Locale.getDefault()).trim(); + SuggestionProvider suggestionProvider = new SearchSuggestionUtil().getSuggestionProvider( + mLauncher + ); + return suggestionProvider.query(query).toObservable(); + } + + @Override + public void onClick(String suggestion) { + mSearchInput.setText(suggestion); + mLauncher.runSearch(suggestion); + mSearchInput.clearFocus(); + mSearchInput.setText(""); + } + + private BroadcastReceiver mWeatherReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getBooleanExtra(WeatherUpdateService.EXTRA_UPDATE_CANCELLED, false)) { + updateWeatherPanel(); + } + } + }; + + private static final int REQUEST_LOCATION_SOURCE_SETTING = 267; + private AlertDialog enableLocationDialog; + + public void updateWeatherPanel() { + if (mWeatherPanel != null) { + if (Preferences.getCachedWeatherInfo(mLauncher) == null) { + mWeatherSetupTextView.setVisibility(VISIBLE); + mWeatherPanel.setVisibility(GONE); + mWeatherSetupTextView.setOnClickListener( + v -> mLauncher.startActivity( + new Intent(mLauncher, WeatherPreferences.class))); + return; + } + mWeatherSetupTextView.setVisibility(GONE); + mWeatherPanel.setVisibility(VISIBLE); + ForecastBuilder.buildLargePanel(mLauncher, mWeatherPanel, + Preferences.getCachedWeatherInfo(mLauncher) + ); + } + } + + public void showLocationEnableDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(mLauncher); + // Build and show the dialog + builder.setTitle(R.string.weather_retrieve_location_dialog_title); + builder.setMessage(R.string.weather_retrieve_location_dialog_message); + builder.setCancelable(false); + builder.setPositiveButton( + R.string.weather_retrieve_location_dialog_enable_button, + (dialog1, whichButton) -> { + Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + mLauncher.startActivityForResult(intent, REQUEST_LOCATION_SOURCE_SETTING); + } + ); + builder.setNegativeButton(R.string.cancel, null); + enableLocationDialog = builder.create(); + enableLocationDialog.show(); + } + + public void refreshSuggestedApps(boolean forceRefresh) { + if (widgetPage != null) { + refreshSuggestedApps(widgetPage, forceRefresh); + } + } + + public void refreshSuggestedApps(ViewGroup viewGroup, boolean forceRefresh) { + TextView openUsageAccessSettingsTv = viewGroup.findViewById(R.id.openUsageAccessSettings); + GridLayout suggestedAppsGridLayout = viewGroup.findViewById(R.id.suggestedAppGrid); + AppUsageStats appUsageStats = new AppUsageStats(getContext()); + List usageStats = appUsageStats.getUsageStats(); + if (usageStats.size() > 0) { + openUsageAccessSettingsTv.setVisibility(GONE); + suggestedAppsGridLayout.setVisibility(VISIBLE); + + // Check if usage stats have been changed or not to avoid unnecessary flickering + if (forceRefresh || mUsageStats == null || mUsageStats.size() != usageStats.size() + || !ListUtil.areEqualLists(mUsageStats, usageStats)) { + mUsageStats = usageStats; + if (suggestedAppsGridLayout.getChildCount() > 0) { + suggestedAppsGridLayout.removeAllViews(); + } + int i = 0; + while (suggestedAppsGridLayout.getChildCount() < 4 && i < mUsageStats.size()) { + ApplicationItem appItem = AppUtils.createAppItem( + getContext(), + mUsageStats.get(i).getPackageName(), + new foundation.e.blisslauncher.core.utils.UserHandle() + ); + if (appItem != null) { + BlissFrameLayout view = mLauncher.prepareSuggestedApp(appItem); + mLauncher.addAppToGrid(suggestedAppsGridLayout, view); + } + i++; + } + } + } else { + openUsageAccessSettingsTv.setVisibility(VISIBLE); + suggestedAppsGridLayout.setVisibility(GONE); + } + } + + public void showWidgetResizeContainer(RoundedWidgetView roundedWidgetView) { + RelativeLayout widgetResizeContainer = widgetPage.findViewById( + R.id.widget_resizer_container); + if (widgetResizeContainer.getVisibility() != VISIBLE) { + activeRoundedWidgetView = roundedWidgetView; + + SeekBar seekBar = widgetResizeContainer.findViewById(R.id.widget_resizer_seekbar); + if (currentAnimator != null) { + currentAnimator.cancel(); + } + + seekBar.setOnTouchListener((v, event) -> { + seekBar.getParent().requestDisallowInterceptTouchEvent(true); + return false; + }); + + AnimatorSet set = new AnimatorSet(); + set.play(ObjectAnimator.ofFloat(widgetResizeContainer, View.Y, + mLauncher.getDeviceProfile().getAvailableHeightPx(), + mLauncher.getDeviceProfile().getAvailableHeightPx() - Utilities + .pxFromDp(48, getContext()) + )); + set.setDuration(200); + set.setInterpolator(new LinearInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + widgetResizeContainer.setVisibility(VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + currentAnimator = null; + widgetResizeContainer.setVisibility(GONE); + roundedWidgetView.removeBorder(); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + currentAnimator = null; + prepareWidgetResizeSeekBar(seekBar); + roundedWidgetView.addBorder(); + } + } + ); + set.start(); + currentAnimator = set; + } + } + + private void prepareWidgetResizeSeekBar(SeekBar seekBar) { + int minHeight = activeRoundedWidgetView.getAppWidgetInfo().minResizeHeight; + int maxHeight = mLauncher.getDeviceProfile().getAvailableHeightPx() * 3 / 4; + int normalisedDifference = (maxHeight - minHeight) / 100; + int defaultHeight = activeRoundedWidgetView.getHeight(); + int currentProgress = (defaultHeight - minHeight) * 100 / (maxHeight - minHeight); + + seekBar.setMax(100); + seekBar.setProgress(currentProgress); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int newHeight = minHeight + (normalisedDifference * progress); + LinearLayout.LayoutParams layoutParams = + (LinearLayout.LayoutParams) activeRoundedWidgetView.getLayoutParams(); + layoutParams.height = newHeight; + activeRoundedWidgetView.setLayoutParams(layoutParams); + + Bundle newOps = new Bundle(); + newOps.putInt( + AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + mLauncher.getDeviceProfile().getMaxWidgetWidth() + ); + newOps.putInt( + AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + mLauncher.getDeviceProfile().getMaxWidgetWidth() + ); + newOps.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newHeight); + newOps.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newHeight); + activeRoundedWidgetView.updateAppWidgetOptions(newOps); + activeRoundedWidgetView.requestLayout(); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + DatabaseManager.getManager(getContext()).saveWidget( + activeRoundedWidgetView.getAppWidgetId(), seekBar.getProgress()); + } + }); + } + + public void hideWidgetResizeContainer() { + if (widgetPage != null) { + RelativeLayout widgetResizeContainer = widgetPage.findViewById( + R.id.widget_resizer_container); + if (widgetResizeContainer.getVisibility() == VISIBLE) { + if (currentAnimator != null) { + currentAnimator.cancel(); + } + AnimatorSet set = new AnimatorSet(); + set.play(ObjectAnimator.ofFloat(widgetResizeContainer, View.Y, + mLauncher.getDeviceProfile().getAvailableHeightPx() + )); + set.setDuration(200); + set.setInterpolator(new LinearInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + ((SeekBar) widgetPage.findViewById( + R.id.widget_resizer_seekbar)).setOnSeekBarChangeListener(null); + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + currentAnimator = null; + widgetResizeContainer.setVisibility(VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + currentAnimator = null; + widgetResizeContainer.setVisibility(GONE); + activeRoundedWidgetView.removeBorder(); + } + } + ); + set.start(); + currentAnimator = set; + } + } + } + private int[] findSpaceForItem(IntegerArray addedWorkspaceScreensFinal) { // Find appropriate space for the item. int screenId = 0; @@ -488,7 +1102,7 @@ public class LauncherPagedView extends PagedView implements V boolean found = false; int screenCount = getChildCount(); - for (int screen = 0; screen < screenCount; screen++) { + for (int screen = 1; screen < screenCount; screen++) { View child = getChildAt(screen); if (child instanceof CellLayout) { CellLayout cellLayout = (CellLayout) child; @@ -510,6 +1124,18 @@ public class LauncherPagedView extends PagedView implements V return new int[]{screenId, cell}; } + /** + * Removes all folder listeners + */ + public void removeFolderListeners() { + mapOverItems(false, (info, view, index) -> { + if (view instanceof FolderIcon) { + ((FolderIcon) view).removeListeners(); + } + return false; + }); + } + /** * Returns true if the shortcuts already exists on the workspace. This must be called after * the workspace has been loaded. We identify a shortcut by its intent. @@ -615,13 +1241,14 @@ public class LauncherPagedView extends PagedView implements V CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate( R.layout.workspace_screen, this, false /* attachToRoot */); int paddingLeftRight = mLauncher.getDeviceProfile().getCellLayoutPaddingLeftRightPx(); - int paddingBottom = mLauncher.getDeviceProfile().getCellLayoutBottomPaddingPx(); + int paddingBottom = mLauncher.getDeviceProfile().getCellLayoutBottomPaddingPx() + mLauncher + .getDeviceProfile().getWorkspacePadding().bottom; newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom); newScreen.setRowCount(mLauncher.getDeviceProfile().getInv().getNumRows()); newScreen.setColumnCount(mLauncher.getDeviceProfile().getInv().getNumColumns()); mWorkspaceScreens.put(screenId, newScreen); - mScreenOrder.add(insertIndex, screenId); + mScreenOrder.add(insertIndex - 1, screenId); addView(newScreen, insertIndex); return newScreen; @@ -755,6 +1382,7 @@ public class LauncherPagedView extends PagedView implements V } // Update the page indicator to reflect the removed page. showPageIndicatorAtCurrentScroll(); + handleWidgetPageTransition(); } }; @@ -969,6 +1597,7 @@ public class LauncherPagedView extends PagedView implements V if (!removeScreens.isEmpty()) { // Update the model if we have changed any screens //TODO: LauncherModel.updateWorkspaceScreenOrder(mLauncher, mScreenOrder); + updateDatabase(); } if (pageShift >= 0) { @@ -1010,6 +1639,24 @@ public class LauncherPagedView extends PagedView implements V child.startAnimation(AnimationUtils .loadAnimation(getContext(), R.anim.wobble_reverse)); } + + LauncherItem info = (LauncherItem) child.getTag(); + if (child instanceof IconTextView && (info instanceof ApplicationItem || info instanceof ShortcutItem) && !UninstallHelper.INSTANCE + .isUninstallDisabled(info.user.getRealHandle(), getContext())) { + // Return early if this app is system app + if (info instanceof ApplicationItem) { + ApplicationItem applicationItem = (ApplicationItem) info; + if (applicationItem.isSystemApp != ApplicationItem.FLAG_SYSTEM_UNKNOWN) { + if ((applicationItem.isSystemApp & ApplicationItem.FLAG_SYSTEM_NO) != 0) { + ((IconTextView) child).applyUninstallIconState(true); + } + } else { + ((IconTextView) child).applyUninstallIconState(true); + } + } else if (info instanceof ShortcutItem) { + ((IconTextView) child).applyUninstallIconState(true); + } + } } }); } @@ -1212,6 +1859,12 @@ public class LauncherPagedView extends PagedView implements V } } + @Override + protected void notifyPageSwitchListener(int prevPage) { + super.notifyPageSwitchListener(prevPage); + handleWidgetPageTransition(); + } + @Override public int getExpectedHeight() { return getMeasuredHeight() <= 0 || !mIsLayoutValid @@ -1233,7 +1886,7 @@ public class LauncherPagedView extends PagedView implements V enableHwLayersOnVisiblePages(); } else { for (int i = 0; i < getPageCount(); i++) { - final GridLayout grid = (GridLayout) getChildAt(i); + final View grid = getChildAt(i); grid.setLayerType(LAYER_TYPE_NONE, sPaint); } } @@ -1266,7 +1919,7 @@ public class LauncherPagedView extends PagedView implements V } for (int i = 0; i < screenCount; i++) { - final GridLayout layout = (GridLayout) getPageAt(i); + final View layout = getPageAt(i); // enable layers between left and right screen inclusive. boolean enableLayer = leftScreen <= i && i <= rightScreen; layout.setLayerType(enableLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint); @@ -1274,6 +1927,22 @@ public class LauncherPagedView extends PagedView implements V } } + public void onWallpaperTap(MotionEvent ev) { + setWobbleExpirationAlarm(0); // Dismiss any animation if running. + final int[] position = mTempXY; + getLocationOnScreen(position); + + int pointerIndex = ev.getActionIndex(); + position[0] += (int) ev.getX(pointerIndex); + position[1] += (int) ev.getY(pointerIndex); + + mWallpaperManager.sendWallpaperCommand(getWindowToken(), + ev.getAction() == MotionEvent.ACTION_UP + ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP, + position[0], position[1], 0, null + ); + } + public void setup(@NotNull DragController dragController) { mSpringLoadedDragController = new SpringLoadedDragController(mLauncher); mDragController = dragController; @@ -1332,11 +2001,10 @@ public class LauncherPagedView extends PagedView implements V if (!workspaceInModalState() && !mIsSwitchingState) { result = super.scrollLeft(); } - // TODO: Fix this asap - /*Folder openFolder = Folder.getOpen(mLauncher); + Folder openFolder = Folder.Companion.getOpen(mLauncher); if (openFolder != null) { openFolder.completeDragExit(); - }*/ + } return result; } @@ -1346,30 +2014,67 @@ public class LauncherPagedView extends PagedView implements V if (!workspaceInModalState() && !mIsSwitchingState) { result = super.scrollRight(); } - // TODO: Fix this asap - /*Folder openFolder = Folder.getOpen(mLauncher); + Folder openFolder = Folder.Companion.getOpen(mLauncher); if (openFolder != null) { openFolder.completeDragExit(); - }*/ + } return result; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); - // Update the page indicator progress. boolean isTransitioning = mIsSwitchingState || (getLayoutTransition() != null && getLayoutTransition() .isRunning()); if (!isTransitioning) { showPageIndicatorAtCurrentScroll(); + int currentScrollX = getScrollX(); + int firstPageScroll = getScrollForPage(1); + if (currentScrollX <= firstPageScroll) { + handleWidgetPageTransition(currentScrollX, firstPageScroll); + } } //updatePageAlphaValues(); enableHwLayersOnVisiblePages(); } + private void handleWidgetPageTransition() { + handleWidgetPageTransition(getScrollX(), getScrollForPage(1)); + } + + private void handleWidgetPageTransition(int currentScrollX, int firstPageScroll) { + if (currentScrollX >= firstPageScroll) { + currentScrollX = firstPageScroll; + } + boolean reset = currentScrollX == firstPageScroll; + float scroll = 1 - (float) currentScrollX / firstPageScroll; + float offset = 0f; + + scroll = Math.max(scroll - offset, 0); + scroll = Math.min(1, scroll / (1 - offset)); + + float transX = getHotseat().getMeasuredWidth() * scroll; + + if (mIsRtl) { + transX = -transX; + } + // TODO(adamcohen): figure out a final effect here. We may need to recommend + // different effects based on device performance. On at least one relatively high-end + // device I've tried, translating the launcher causes things to get quite laggy. + float finalTransX = transX; + float finalScroll = scroll; + boolean finalReset = reset; + post(() -> { + ((LauncherRootView) getParent().getParent()).setBlurAlpha((int) (finalScroll * 255)); + getHotseat().setTranslationX(finalTransX); + getHotseat().changeBlurBounds(finalScroll, finalReset); + getPageIndicator().setTranslationX(finalTransX); + }); + } + public void showPageIndicatorAtCurrentScroll() { if (mPageIndicator != null) { mPageIndicator.setScroll(getScrollX(), computeMaxScrollX()); @@ -1412,9 +2117,11 @@ public class LauncherPagedView extends PagedView implements V public void onDragStart( DragObject dragObject, DragOptions options ) { - if (mDragInfo != null && mDragInfo.getCell() != null) { - CellLayout layout = (CellLayout) mDragInfo.getCell().getParent(); - layout.markCellsAsUnoccupiedForView(mDragInfo.getCell()); + if (mDragInfo != null) { + ViewParent parent = mDragInfo.getCell().getParent(); + if (parent instanceof CellLayout) { + ((CellLayout) parent).markCellsAsUnoccupiedForView(mDragInfo.getCell()); + } } if (mOutlineProvider != null) { @@ -1478,6 +2185,38 @@ public class LauncherPagedView extends PagedView implements V mDragInfo = null; } + /** + * Unbinds the view for the specified item, and removes the item and all its children. + * + * @param v the view being removed. + * @param itemInfo the {@link LauncherItem} for this view. + */ + public boolean removeItem(View v, final LauncherItem itemInfo) { + if (itemInfo instanceof ApplicationItem || itemInfo instanceof ShortcutItem) { + // Remove the shortcut from the folder before removing it from launcher + View folderIcon = getHomescreenIconByItemId(String.valueOf(itemInfo.container)); + if (folderIcon instanceof FolderIcon) { + ((FolderItem) folderIcon.getTag()).remove(itemInfo, true); + } else { + removeWorkspaceItem(v); + } + updateDatabase(); + } else if (itemInfo instanceof FolderItem) { + if (v instanceof FolderIcon) { + ((FolderIcon) v).removeListeners(); + } + removeWorkspaceItem(v); + updateDatabase(); + } else { + return false; + } + return true; + } + + public View getHomescreenIconByItemId(final String id) { + return getFirstMatch((info, v, idx) -> info != null && info.id == id); + } + /** * For opposite operation. See {@link #addInScreen}. */ @@ -1516,7 +2255,7 @@ public class LauncherPagedView extends PagedView implements V ArrayList getWorkspaceAndHotseatCellLayouts() { ArrayList layouts = new ArrayList<>(); int screenCount = getChildCount(); - for (int screen = 0; screen < screenCount; screen++) { + for (int screen = 1; screen < screenCount; screen++) { layouts.add(((CellLayout) getChildAt(screen))); } if (mLauncher.getHotseat() != null) { @@ -1605,7 +2344,7 @@ public class LauncherPagedView extends PagedView implements V if (d.dragSource != this || mDragInfo == null) { final int[] touchXY = new int[]{(int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1]}; - //onDropExternal(touchXY, dropTargetLayout, d); + // onDropExternal(touchXY, dropTargetLayout, d); } else { final View cell = mDragInfo.getCell(); boolean droppedOnOriginalCellDuringTransition = false; @@ -1821,50 +2560,51 @@ public class LauncherPagedView extends PagedView implements V LauncherItem sourceItem = (LauncherItem) newView.getTag(); LauncherItem destItem = (LauncherItem) targetView.getTag(); - Rect folderLocation = new Rect(); + // if the drag started here, we need to remove it from the workspace + if (!external) { + getParentCellLayoutForView(mDragInfo.getCell()).removeView(mDragInfo.getCell()); + } targetView.clearAnimation(); target.removeView(targetView); - FolderItem fi = new FolderItem(); + + final FolderItem fi = new FolderItem(); fi.title = getResources().getString(R.string.untitled); fi.id = String.valueOf(System.currentTimeMillis()); fi.items = new ArrayList<>(); + fi.container = destItem.container; + fi.screenId = destItem.screenId; + fi.cell = destItem.cell; + + // Create the view + FolderIcon newFolder = FolderIcon.Companion.fromXml(R.layout.folder_icon, target, fi); + addInScreen(newFolder, fi); + // Force measure the new folder icon + CellLayout parent = getParentCellLayoutForView(newFolder); + parent.measureChild(newFolder); sourceItem.container = Long.parseLong(fi.id); destItem.container = Long.parseLong(fi.id); sourceItem.screenId = -1; destItem.screenId = -1; - sourceItem.cell = fi.items.size(); - fi.items.add(sourceItem); - destItem.cell = fi.items.size(); - fi.items.add(destItem); - Drawable folderIcon = new GraphicsUtil(getContext()).generateFolderIcon(getContext(), - sourceItem.icon, destItem.icon - ); - fi.icon = folderIcon; - fi.container = container; - fi.screenId = screenId; - fi.cell = targetCell[1] * mLauncher.getDeviceProfile().getInv() - .getNumColumns() + targetCell[0]; - IconTextView folderView = (IconTextView) LayoutInflater.from(getContext()) - .inflate(R.layout.app_icon, null, false); - folderView.applyFromShortcutItem(fi); - folderView.setOnClickListener(ItemClickHandler.INSTANCE); - folderView.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); - addInScreen(folderView, fi); - // if the drag started here, we need to remove it from the workspace - if (!external) { - getParentCellLayoutForView(mDragInfo.getCell()).removeView(mDragInfo.getCell()); - } - //Add animation here. - dragView.remove(); + destItem.cell = 0; + sourceItem.cell = 1; + newFolder.addItem(destItem); + newFolder.addItem(sourceItem); + updateDatabase(getWorkspaceAndHotseatCellLayouts()); + + // Clear drag view + dragView.remove(); + dragView = null; + invalidate(); post(() -> { if (fi.cell % 2 == 0) { - folderView + newFolder .startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.wobble)); } else { - folderView.startAnimation(AnimationUtils + newFolder.startAnimation(AnimationUtils .loadAnimation(getContext(), R.anim.wobble_reverse)); } + newFolder.applyUninstallIconState(false); }); return true; } @@ -1885,16 +2625,12 @@ public class LauncherPagedView extends PagedView implements V if (!mAddToExistingFolderOnDrop) return false; mAddToExistingFolderOnDrop = false; - if ((dropOverView instanceof IconTextView) && (dropOverView - .getTag() instanceof FolderItem)) { - FolderItem fi = (FolderItem) dropOverView.getTag(); - LauncherItem sourceItem = (LauncherItem) newView.getTag(); - sourceItem.container = Long.parseLong(fi.id); - sourceItem.screenId = -1; - sourceItem.cell = fi.items.size(); - fi.items.add(sourceItem); - fi.icon = new GraphicsUtil(getContext()).generateFolderIcon(getContext(), fi); - ((IconTextView) dropOverView).applyFromShortcutItem(fi); + if (dropOverView instanceof FolderIcon) { + FolderIcon folderIcon = (FolderIcon) dropOverView; + if (folderIcon.acceptDrop()) { + LauncherItem item = d.dragInfo; + folderIcon.addItem(item); + } // if the drag started here, we need to remove it from the workspace if (!external) { getParentCellLayoutForView(mDragInfo.getCell()).removeView(mDragInfo.getCell()); @@ -1967,7 +2703,7 @@ public class LauncherPagedView extends PagedView implements V } // Always pick the current page. - if (layout == null && nextPage >= 0 && nextPage < getPageCount()) { + if (layout == null && nextPage >= 1 && nextPage < getPageCount()) { layout = (CellLayout) getChildAt(nextPage); } if (layout != mDragTargetLayout) { @@ -2070,7 +2806,7 @@ public class LauncherPagedView extends PagedView implements V * Returns the child CellLayout if the point is inside the page coordinates, null otherwise. */ private CellLayout verifyInsidePage(int pageNo, float[] touchXy) { - if (pageNo >= 0 && pageNo < getPageCount()) { + if (pageNo >= 1 && pageNo < getPageCount()) { CellLayout cl = (CellLayout) getChildAt(pageNo); mapPointFromSelfToChild(cl, touchXy); if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() && @@ -2133,7 +2869,7 @@ public class LauncherPagedView extends PagedView implements V } // Handle the drag over - if (mDragTargetLayout != null) { + if (mDragTargetLayout != null && child != null) { // We want the point to be mapped to the dragTarget. if (mLauncher.isHotseatLayout(mDragTargetLayout)) { mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); @@ -2218,9 +2954,56 @@ public class LauncherPagedView extends PagedView implements V return alarmPending; } + public boolean isWobbling() { + return wobbleExpireAlarm.alarmPending(); + } + @Override - public boolean acceptDrop(DragObject dragObject) { + public boolean acceptDrop(DragObject d) { CellLayout dropTargetLayout = mDropToLayout; + if (d.dragSource != this) { + if (dropTargetLayout == null) { + return false; + } + if (!transitionStateShouldAllowDrop()) return false; + + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); + + // We want the point to be mapped to the dragTarget. + mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter); + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], dropTargetLayout, + mTargetCell + ); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell + ); + if (mCreateUserFolderOnDrop && willCreateUserFolder(d.dragInfo, + dropTargetLayout, mTargetCell, distance, true + )) { + return true; + } + + if (mAddToExistingFolderOnDrop && willAddToExistingUserFolder(d.dragInfo, + dropTargetLayout, mTargetCell, distance + )) { + return true; + } + + int[] resultSpan = new int[2]; + mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], 1, 1, 1, 1, + null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP + ); + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + + // Don't accept the drop if there's no room for the item + if (!foundCell) { + onNoCellFound(dropTargetLayout); + return false; + } + } long screenId = getIdForScreen(dropTargetLayout); if (screenId == EXTRA_EMPTY_SCREEN_ID) { commitExtraEmptyScreen(); @@ -2228,6 +3011,26 @@ public class LauncherPagedView extends PagedView implements V return true; } + /** + * Updates the point in {@param xy} to point to the co-ordinate space of {@param layout} + * + * @param layout either hotseat of a page in workspace + * @param xy the point location in workspace co-ordinate space + */ + private void mapPointFromDropLayout(CellLayout layout, float[] xy) { + if (mLauncher.isHotseatLayout(layout)) { + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true); + mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy); + } else { + mapPointFromSelfToChild(layout, xy); + } + } + + private boolean transitionStateShouldAllowDrop() { + return (!isSwitchingState() || mTransitionProgress > ALLOW_DROP_TRANSITION_PROGRESS) && + workspaceIconsCanBeDragged(); + } + @Override public void prepareAccessibilityDrop() { Log.d(TAG, "prepareAccessibilityDrop() called"); @@ -2511,6 +3314,18 @@ public class LauncherPagedView extends PagedView implements V ); } + public View getFirstMatch(final ItemOperator operator) { + final View[] value = new View[1]; + mapOverItems(MAP_NO_RECURSE, (info, v, index) -> { + if (operator.evaluate(info, v, index)) { + value[0] = v; + return true; + } + return false; + }); + return value[0]; + } + /** * @param cellLayouts List of CellLayouts to scan, in order of preference. * @param operators List of operators, in order starting from best matching operator. @@ -2557,9 +3372,18 @@ public class LauncherPagedView extends PagedView implements V return; } } + Folder folder = Folder.Companion.getOpen(mLauncher); + if (folder != null && !folder.isDestroyed()) { + for (int i = 0; i < folder.getContent().getChildCount(); i++) { + GridLayout grid = (GridLayout) folder.getContent().getChildAt(i); + if (mapOverCellLayout(recurse, grid, op)) { + return; + } + } + } } - private boolean mapOverCellLayout(boolean recurse, CellLayout layout, ItemOperator op) { + private boolean mapOverCellLayout(boolean recurse, GridLayout layout, ItemOperator op) { // TODO(b/128460496) Potential race condition where layout is not yet loaded if (layout == null) { return false; @@ -2607,12 +3431,12 @@ public class LauncherPagedView extends PagedView implements V // Update folder icons mapOverItems(MAP_NO_RECURSE, (info, v, itemIdx) -> { if (info instanceof FolderItem && folderIds.contains(info.id) - && v instanceof IconTextView) { + && v instanceof FolderIcon) { FolderDotInfo folderDotInfo = new FolderDotInfo(); for (LauncherItem si : ((FolderItem) info).items) { folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si)); } - ((IconTextView) v).setDotInfo((FolderItem) info, folderDotInfo); + ((FolderIcon) v).setDotInfo(folderDotInfo); } // process all the shortcuts return false; @@ -2656,8 +3480,8 @@ public class LauncherPagedView extends PagedView implements V // TODO(adamcohen): figure out a final effect here. We may need to recommend // different effects based on device performance. On at least one relatively high-end // device I've tried, translating the launcher causes things to get quite laggy. - mLauncher.getDragLayer().setTranslationX(transX); - mLauncher.getDragLayer().getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha); + getHotseat().setTranslationX(transX); + getHotseat().getAlphaProperty(0).setValue(alpha); } protected void announcePageForAccessibility() { @@ -2736,6 +3560,120 @@ public class LauncherPagedView extends PagedView implements V computeScrollHelper(false); } + /** + * Removes items that match the {@param matcher}. When applications are removed + * as a part of an update, this is called to ensure that other widgets and application + * shortcuts are not removed. + */ + public void removeItemsByMatcher(@NotNull LauncherItemMatcher matcher) { + for (final CellLayout layout : getWorkspaceAndHotseatCellLayouts()) { + + HashMap idToViewMap = new HashMap<>(); + ArrayList items = new ArrayList<>(); + for (int j = 0; j < layout.getChildCount(); j++) { + final View view = layout.getChildAt(j); + if (view.getTag() instanceof LauncherItem) { + LauncherItem item = (LauncherItem) view.getTag(); + items.add(item); + idToViewMap.put(item.id, view); + } + } + + for (LauncherItem itemToRemove : matcher.filterItemInfos(items)) { + View child = idToViewMap.get(itemToRemove.id); + + if (child != null) { + // Note: We can not remove the view directly from CellLayoutChildren as this + // does not re-mark the spaces as unoccupied. + child.clearAnimation(); + layout.removeViewInLayout(child); + if (child instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) child); + } + // Remove item from database + DatabaseManager.getManager(getContext()).removeItem(itemToRemove.id); + } else if (itemToRemove.container >= 0) { + // The item may belong to a folder. + View parent = idToViewMap.get(String.valueOf(itemToRemove.container)); + if (parent != null) { + FolderItem folder = (FolderItem) parent.getTag(); + parent.clearAnimation(); + // Close folder before making any changes + // mLauncher.closeFolder(); + folder.items.remove(itemToRemove); + DatabaseManager.getManager(getContext()).removeItem(itemToRemove.id); + if (folder.items.size() == 0) { + layout.removeView(parent); + if (parent instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) child); + } + } else if (folder.items.size() == 1) { + LauncherItem lastFolderItem = folder.items.get(0); + layout.removeViewInLayout(parent); + if (parent instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) parent); + } + lastFolderItem.container = folder.container; + lastFolderItem.cell = folder.cell; + lastFolderItem.screenId = folder.screenId; + bindItems(Collections.singletonList(lastFolderItem), true); + } else { + folder.icon = new GraphicsUtil(getContext()) + .generateFolderIcon(getContext(), folder); + layout.removeViewInLayout(parent); + if (parent instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) parent); + } + bindItems(Collections.singletonList(folder), true); + } + } + } + } + } + + // Strip all the empty screens + stripEmptyScreens(); + updateDatabase(getWorkspaceAndHotseatCellLayouts()); + } + + public void clearWidgetState() { + if (activeRoundedWidgetView != null && activeRoundedWidgetView.isWidgetActivated()) { + hideWidgetResizeContainer(); + } + + if (mSearchInput != null) { + mSearchInput.setText(""); + } + } + + public void updateWidgets() { + if (widgetContainer != null) { + WidgetManager widgetManager = WidgetManager.getInstance(); + Integer id = widgetManager.dequeRemoveId(); + while (id != null) { + for (int i = 0; i < widgetContainer.getChildCount(); i++) { + if (widgetContainer.getChildAt(i) instanceof RoundedWidgetView) { + RoundedWidgetView appWidgetHostView = + (RoundedWidgetView) widgetContainer.getChildAt(i); + if (appWidgetHostView.getAppWidgetId() == id) { + widgetContainer.removeViewAt(i); + DatabaseManager.getManager(mLauncher).removeWidget(id); + break; + } + } + } + id = widgetManager.dequeRemoveId(); + } + + RoundedWidgetView widgetView = widgetManager.dequeAddWidgetView(); + while (widgetView != null) { + widgetView = WidgetViewBuilder.create(mLauncher, widgetView); + addWidgetToContainer(widgetView); + widgetView = widgetManager.dequeAddWidgetView(); + } + } + } + public interface ItemOperator { /** * Process the next itemInfo, possibly with side-effect on the next item. 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 index 11b883306da0212de1957745fb6985349cd051f4..7831c278c1469fa089071c1667a18ef919b9dd34 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java @@ -497,6 +497,7 @@ public abstract class PagedView extends ViewGrou int myHeightSpec = MeasureSpec.makeMeasureSpec( heightSize - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY); + Log.i("WidgetPageLayer", "sending heigh: "+(heightSize - mInsets.top - mInsets.bottom)); // measureChildren takes accounts for content padding, we only need to care about extra // space due to insets. measureChildren(myWidthSpec, myHeightSpec); diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt b/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt index 83eb20f3cbe5fb3d182b1e8604827ba7df620d94..11e239caa7c35358260f4fc783597cd6ce5b6c53 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt +++ b/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt @@ -192,7 +192,7 @@ class PageIndicatorDots(context: Context, attrs: AttributeSet?, defStyleAttr: In circleGap = -circleGap } for (i in mEntryAnimationRadiusFactors!!.indices) { - mCirclePaint.setColor(if (i == mActivePage) mActiveColor else mInActiveColor) + mCirclePaint.color = if (i == mActivePage) mActiveColor else mInActiveColor canvas.drawCircle( x, y, @@ -207,7 +207,7 @@ class PageIndicatorDots(context: Context, attrs: AttributeSet?, defStyleAttr: In canvas.drawCircle(x, y, mDotRadius, mCirclePaint) x += circleGap } - mCirclePaint.setColor(mActiveColor) + mCirclePaint.color = mActiveColor canvas.drawRoundRect(activeRect, mDotRadius, mDotRadius, mCirclePaint) } } // Dot is leaving the left circle. diff --git a/app/src/main/java/foundation/e/blisslauncher/core/database/DatabaseManager.java b/app/src/main/java/foundation/e/blisslauncher/core/database/DatabaseManager.java index d8d96f4fea84eaf1c51eb59de616a567fa1971a7..5317a38779fcf7a6d6c2b7fb3341ab1b058e742f 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/database/DatabaseManager.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/database/DatabaseManager.java @@ -134,4 +134,9 @@ public class DatabaseManager { public void removeWidget(int id) { mAppExecutors.diskIO().execute(() -> LauncherDB.getDatabase(mContext).widgetDao().delete(id)); } + + public void removeItem(String id) { + mAppExecutors.diskIO().execute( + () -> LauncherDB.getDatabase(mContext).launcherDao().delete(id)); + } } diff --git a/app/src/main/java/foundation/e/blisslauncher/core/database/model/FolderItem.java b/app/src/main/java/foundation/e/blisslauncher/core/database/model/FolderItem.java index c96d7304c5bf57408088ee14171a9ea41a8ac0f4..b751c4bfc530017d7eae880445bf26c19ba6ece0 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/database/model/FolderItem.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/database/model/FolderItem.java @@ -1,7 +1,13 @@ package foundation.e.blisslauncher.core.database.model; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import foundation.e.blisslauncher.core.Utilities; import foundation.e.blisslauncher.core.utils.Constants; + +import java.util.ArrayList; import java.util.List; public class FolderItem extends LauncherItem { @@ -15,4 +21,62 @@ public class FolderItem extends LauncherItem { itemType = Constants.ITEM_TYPE_FOLDER; } + ArrayList listeners = new ArrayList<>(); + + public void setTitle(CharSequence title) { + this.title = title; + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onTitleChanged(title); + } + } + + public void addListener(FolderListener listener) { + listeners.add(listener); + } + + public void removeListener(FolderListener listener) { + listeners.remove(listener); + } + + public void remove(@NotNull LauncherItem launcherItem, boolean animate) { + items.remove(launcherItem); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onRemove(launcherItem); + } + itemsChanged(animate); + } + + public void itemsChanged(boolean animate) { + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onItemsChanged(animate); + } + } + + /** + * Add an app or shortcut + * + * @param item + */ + public void add(LauncherItem item, boolean animate) { + add(item, items.size(), animate); + } + + /** + * Add an app or shortcut for a specified rank. + */ + public void add(LauncherItem item, int rank, boolean animate) { + rank = Utilities.boundToRange(rank, 0, items.size()); + items.add(rank, item); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onAdd(item); + } + itemsChanged(animate); + } + + public interface FolderListener { + void onAdd(LauncherItem item); + void onTitleChanged(CharSequence title); + void onRemove(LauncherItem item); + void onItemsChanged(boolean animate); + } } diff --git a/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemClickHandler.java b/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemClickHandler.java index 4724e07d6afbcbb88681f1060228deb782dcc658..0e4ca929467bea02193afe7d3243c23fc47c8f93 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemClickHandler.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemClickHandler.java @@ -21,10 +21,12 @@ import android.util.Log; import android.view.View; import android.view.View.OnClickListener; +import foundation.e.blisslauncher.core.customviews.Folder; import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.FolderItem; import foundation.e.blisslauncher.core.database.model.LauncherItem; import foundation.e.blisslauncher.core.database.model.ShortcutItem; +import foundation.e.blisslauncher.features.folder.FolderIcon; import foundation.e.blisslauncher.features.test.IconTextView; import foundation.e.blisslauncher.features.test.TestActivity; @@ -51,6 +53,10 @@ public class ItemClickHandler { return; }*/ + if (launcher.getLauncherPagedView().isWobbling()) { + return; + } + if(v instanceof IconTextView) { boolean result = ((IconTextView) v).tryToHandleUninstallClick(launcher); if(result) return; @@ -60,7 +66,7 @@ public class ItemClickHandler { if (tag instanceof ShortcutItem) { onClickAppShortcut(v, (ShortcutItem) tag, launcher); } else if (tag instanceof FolderItem) { - onClickFolderIcon(v, launcher); + onClickFolderIcon(v); } else if (tag instanceof ApplicationItem) { startAppShortcutOrInfoActivity(v, (ApplicationItem) tag, launcher); } @@ -70,14 +76,13 @@ public class ItemClickHandler { * Event handler for a folder icon click. * * @param v The view that was clicked. Must be an instance of {@link IconTextView}. - * @param launcher Launcher activity to pass actions. */ - private static void onClickFolderIcon( - View v, - TestActivity launcher - ) { - Log.d("ItemClick", "onClickFolderIcon() called with: v = [" + v + "]"); - launcher.openFolder(v); + private static void onClickFolderIcon(View v) { + Folder folder = ((FolderIcon) v).getFolder(); + if (!folder.isOpen() && !folder.isDestroyed()) { + // Open the requested folder + folder.animateOpen(); + } } /** diff --git a/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemLongClickListener.java b/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemLongClickListener.java index b828ba89aeff98aa99fa0b5afe2b3ac98ccf753d..a491968c36e0fdbb60888f87d2809d6320f0e42a 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemLongClickListener.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/touch/ItemLongClickListener.java @@ -20,6 +20,7 @@ import static foundation.e.blisslauncher.features.test.LauncherState.OVERVIEW; import android.view.View; import android.view.View.OnLongClickListener; +import foundation.e.blisslauncher.core.customviews.Folder; import foundation.e.blisslauncher.core.database.model.LauncherItem; import foundation.e.blisslauncher.features.test.CellLayout; import foundation.e.blisslauncher.features.test.TestActivity; @@ -57,9 +58,8 @@ public class ItemLongClickListener { View v, TestActivity launcher, LauncherItem info, DragOptions dragOptions ) { - //TODO: Enable when supporting folders - /* if (info.container >= 0) { - Folder folder = Folder.getOpen(launcher); + if (info.container >= 0) { + Folder folder = Folder.Companion.getOpen(launcher); if (folder != null) { if (!folder.getItemsInReadingOrder().contains(v)) { folder.close(true); @@ -68,7 +68,7 @@ public class ItemLongClickListener { return; } } - }*/ + } CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info); launcher.getLauncherPagedView().startDrag(longClickCellInfo, dragOptions); diff --git a/app/src/main/java/foundation/e/blisslauncher/core/touch/WorkspaceTouchListener.java b/app/src/main/java/foundation/e/blisslauncher/core/touch/WorkspaceTouchListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ec49a2d4d5111acd246f154ceab1e4d8e0d52695 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/core/touch/WorkspaceTouchListener.java @@ -0,0 +1,90 @@ +/* + * 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.core.touch; + +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; + +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import foundation.e.blisslauncher.core.customviews.LauncherPagedView; +import foundation.e.blisslauncher.features.test.TestActivity; + +/** + * Helper class to handle touch on empty space in workspace and show options popup on long press + */ +public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListener + implements OnTouchListener { + + /** + * STATE_PENDING_PARENT_INFORM is the state between longPress performed & the next motionEvent. + * This next event is used to send an ACTION_CANCEL to Workspace, to that it clears any + * temporary scroll state. After that, the state is set to COMPLETED, and we just eat up all + * subsequent motion events. + */ + private static final int STATE_CANCELLED = 0; + private static final int STATE_REQUESTED = 1; + private static final int STATE_PENDING_PARENT_INFORM = 2; + private static final int STATE_COMPLETED = 3; + + private final Rect mTempRect = new Rect(); + private final TestActivity mLauncher; + private final LauncherPagedView mWorkspace; + private final PointF mTouchDownPoint = new PointF(); + private final float mTouchSlop; + + private final GestureDetector mGestureDetector; + + public WorkspaceTouchListener(TestActivity launcher, LauncherPagedView workspace) { + mLauncher = launcher; + mWorkspace = workspace; + // Use twice the touch slop as we are looking for long press which is more + // likely to cause movement. + mTouchSlop = 2 * ViewConfiguration.get(launcher).getScaledTouchSlop(); + mGestureDetector = new GestureDetector(workspace.getContext(), this); + } + + @Override + public boolean onTouch(View view, MotionEvent ev) { + mGestureDetector.onTouchEvent(ev); + + int action = ev.getActionMasked(); + if (action == ACTION_DOWN) { + mWorkspace.onTouchEvent(ev); + // Return true to keep receiving touch events + return true; + } + + final boolean result = false; + + if (action == ACTION_UP || action == ACTION_POINTER_UP) { + if (!mWorkspace.isHandlingTouch()) { + final View currentPage = mWorkspace.getChildAt(mWorkspace.getCurrentPage()); + if (currentPage != null) { + mWorkspace.onWallpaperTap(ev); + } + } + } + return result; + } + +} diff --git a/app/src/main/java/foundation/e/blisslauncher/core/utils/AppUtils.java b/app/src/main/java/foundation/e/blisslauncher/core/utils/AppUtils.java index b0aebbef93fe1da0864d40b0f6dbdf324dc15377..946eb29746432ef9127e1317ff4d58362b44c04d 100755 --- a/app/src/main/java/foundation/e/blisslauncher/core/utils/AppUtils.java +++ b/app/src/main/java/foundation/e/blisslauncher/core/utils/AppUtils.java @@ -32,6 +32,9 @@ import foundation.e.blisslauncher.BlissLauncher; import foundation.e.blisslauncher.core.IconsHandler; import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.features.launcher.AppProvider; + +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -138,4 +141,48 @@ public class AppUtils { return null; } + + public static List createAppItems(Context context, String packageName, UserHandle userHandle) { + List items = new ArrayList<>(); + if (AppProvider.DISABLED_PACKAGES.contains(packageName)) { + return items; + } + if (sLauncherApps == null) { + sLauncherApps = (LauncherApps) context.getSystemService( + Context.LAUNCHER_APPS_SERVICE); + } + + IconsHandler iconsHandler = BlissLauncher.getApplication(context).getIconsHandler(); + + List matches = sLauncherApps.getActivityList( + packageName, + userHandle.getRealHandle()); + if (matches == null || matches.size() == 0) { + return items; + } + + for (LauncherActivityInfo info : matches) { + if (info != null) { + ApplicationItem applicationItem = new ApplicationItem(info, + userHandle); + ApplicationInfo appInfo = info.getApplicationInfo(); + applicationItem.icon = iconsHandler.getDrawableIconForPackage( + info, userHandle); + String componentName = info.getComponentName().toString(); + applicationItem.appType = iconsHandler.isClock(componentName) + ? ApplicationItem.TYPE_CLOCK : (iconsHandler.isCalendar( + componentName) + ? ApplicationItem.TYPE_CALENDAR : ApplicationItem.TYPE_DEFAULT); + applicationItem.title = info.getLabel().toString(); + applicationItem.container = Constants.CONTAINER_DESKTOP; + if (appInfo.packageName.equalsIgnoreCase("com.generalmagic.magicearth")) { + applicationItem.title = "Maps"; + } + applicationItem.packageName = appInfo.packageName; + items.add(applicationItem); + } + } + + return items; + } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/blisslauncher/core/utils/FlagOp.java b/app/src/main/java/foundation/e/blisslauncher/core/utils/FlagOp.java new file mode 100644 index 0000000000000000000000000000000000000000..7f8073f84cd4ba82a5a37a2179a13127a6146fc6 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/core/utils/FlagOp.java @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.core.utils; + +public interface FlagOp { + + FlagOp NO_OP = i -> i; + + int apply(int flags); + + static FlagOp addFlag(int flag) { + return i -> i | flag; + } + + static FlagOp removeFlag(int flag) { + return i -> i & ~flag; + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/core/utils/ItemInfoMatcher.java b/app/src/main/java/foundation/e/blisslauncher/core/utils/ItemInfoMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..1f2cffc0b9d5f782eabf395cbdfdb33da1102256 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/core/utils/ItemInfoMatcher.java @@ -0,0 +1,110 @@ +/* + * 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.core.utils; + +import android.content.ComponentName; +import android.os.UserHandle; + +import java.util.HashSet; + +import foundation.e.blisslauncher.core.database.model.ApplicationItem; +import foundation.e.blisslauncher.core.database.model.FolderItem; +import foundation.e.blisslauncher.core.database.model.LauncherItem; +import foundation.e.blisslauncher.core.database.model.ShortcutItem; +import foundation.e.blisslauncher.features.shortcuts.ShortcutKey; + +/** + * A utility class to check for {@link foundation.e.blisslauncher.core.database.model.LauncherItem} + */ +public interface ItemInfoMatcher { + + boolean matches(LauncherItem info, ComponentName cn); + + /** + * Filters {@param infos} to those satisfying the {@link #matches(LauncherItem, ComponentName)}. + */ + default HashSet filterItemInfos(Iterable infos) { + HashSet filtered = new HashSet<>(); + for (LauncherItem i : infos) { + if (i instanceof ApplicationItem) { + ApplicationItem info = (ApplicationItem) i; + ComponentName cn = info.getTargetComponent(); + if (cn != null && matches(info, cn)) { + filtered.add(info); + } + } else if (i instanceof ShortcutItem) { + ShortcutItem info = (ShortcutItem) i; + ComponentName cn = info.getTargetComponent(); + if (cn != null && matches(info, cn)) { + filtered.add(info); + } + } else if (i instanceof FolderItem) { + FolderItem info = (FolderItem) i; + for (LauncherItem s : info.items) { + ComponentName cn = s.getTargetComponent(); + if (cn != null && matches(s, cn)) { + filtered.add(s); + } + } + } + } + return filtered; + } + + /** + * Returns a new matcher with returns true if either this or {@param matcher} returns true. + */ + default ItemInfoMatcher or(ItemInfoMatcher matcher) { + return (info, cn) -> matches(info, cn) || matcher.matches(info, cn); + } + + /** + * Returns a new matcher with returns true if both this and {@param matcher} returns true. + */ + default ItemInfoMatcher and(ItemInfoMatcher matcher) { + return (info, cn) -> matches(info, cn) && matcher.matches(info, cn); + } + + /** + * Returns a new matcher which returns the opposite boolean value of the provided + * {@param matcher}. + */ + static ItemInfoMatcher not(ItemInfoMatcher matcher) { + return (info, cn) -> !matcher.matches(info, cn); + } + + static ItemInfoMatcher ofUser(UserHandle user) { + return (info, cn) -> info.user.equals(user); + } + + static ItemInfoMatcher ofComponents(HashSet components, UserHandle user) { + return (info, cn) -> components.contains(cn) && info.user.equals(user); + } + + static ItemInfoMatcher ofPackages(HashSet packageNames, UserHandle user) { + return (info, cn) -> packageNames.contains(cn.getPackageName()) && info.user.equals(user); + } + + static ItemInfoMatcher ofShortcutKeys(HashSet keys) { + return (info, cn) -> info.itemType == Constants.ITEM_TYPE_SHORTCUT && + keys.contains(ShortcutKey.fromItem((ShortcutItem) info)); + } + + static ItemInfoMatcher ofItemIds(IntSparseArrayMap ids, Boolean matchDefault) { + return (info, cn) -> ids.get(Integer.parseInt(info.id), matchDefault); + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderAnimationManager.java b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderAnimationManager.java new file mode 100644 index 0000000000000000000000000000000000000000..1ebe57f1034ed94845b716a3d80b8fe1d39413e0 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderAnimationManager.java @@ -0,0 +1,194 @@ +/* + * 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.features.folder; + +import static foundation.e.blisslauncher.features.test.anim.LauncherAnimUtils.SCALE_PROPERTY; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.util.Property; +import android.view.View; +import android.view.animation.AnimationUtils; +import androidx.viewpager.widget.ViewPager; +import foundation.e.blisslauncher.R; +import foundation.e.blisslauncher.core.customviews.Folder; +import foundation.e.blisslauncher.features.test.TestActivity; +import foundation.e.blisslauncher.features.test.dragndrop.DragLayer; + +/** + * Manages the opening and closing animations for a {@link Folder}. + *

+ * All of the animations are done in the Folder. + * ie. When the user taps on the FolderIcon, we immediately hide the FolderIcon and show the Folder + * in its place before starting the animation. + */ +public class FolderAnimationManager { + + private Folder mFolder; + private ViewPager mContent; + private GradientDrawable mFolderBackground; + + private FolderIcon mFolderIcon; + + private Context mContext; + private TestActivity mLauncher; + + private final boolean mIsOpening; + + private final int mDuration; + private final int mDelay; + + private final TimeInterpolator mFolderInterpolator; + + public FolderAnimationManager(Folder folder, boolean isOpening) { + mFolder = folder; + mContent = folder.mContent; + + mFolderIcon = folder.getFolderIcon(); + + mContext = folder.getContext(); + mLauncher = folder.getLauncher(); + + mIsOpening = isOpening; + + Resources res = mContent.getResources(); + mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); + mDelay = res.getInteger(R.integer.config_folderDelay); + + mFolderInterpolator = AnimationUtils.loadInterpolator( + mContext, + R.anim.folder_interpolator + ); + } + + /** + * Prepares the Folder for animating between open / closed states. + */ + public AnimatorSet getAnimator() { + final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams(); + + // Match position of the FolderIcon + final Rect folderIconPos = new Rect(); + float scaleRelativeToDragLayer = mLauncher.getDragLayer() + .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); + // Calculate the starting and ending bounds for the zoomed-in image. + // This step involves lots of math. Yay, math. + Rect dragLayerBounds = new Rect(); + Point globalOffset = new Point(); + + mLauncher.getDragLayer() + .getGlobalVisibleRect(dragLayerBounds, globalOffset); + float startScale = ((float) folderIconPos.width() / dragLayerBounds.width()); + float startHeight = startScale * dragLayerBounds.height(); + float deltaHeight = (startHeight - folderIconPos.height()) / 2; + folderIconPos.top -= deltaHeight; + folderIconPos.bottom += deltaHeight; + float initialScale = (float) folderIconPos.height() / dragLayerBounds.height(); + final float finalScale = 1f; + float scale = mIsOpening ? initialScale : finalScale; + float initialAlpha = 0f; + float finalAlpha = 1f; + mFolder.setScaleX(scale); + mFolder.setScaleY(scale); + mFolder.setPivotX(0); + mFolder.setPivotY(0); + + // Create the animators. + AnimatorSet a = new AnimatorSet(); + + play( + a, + getAnimator(mFolder, + View.X, + folderIconPos.left, + dragLayerBounds.left + ) + ); + play(a, getAnimator(mFolder, View.Y, folderIconPos.top, dragLayerBounds.top)); + play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale)); + play(a, getAnimator(mFolder, View.ALPHA, initialAlpha, finalAlpha)); + // play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor)); + + // Animate the elevation midway so that the shadow is not noticeable in the background. + int midDuration = mDuration / 2; + Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0); + play(a, z, mIsOpening ? midDuration : 0, midDuration); + + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mFolder.setVisibility(View.VISIBLE); + mFolder.setPivotX(0f); + mFolder.setPivotY(0f); + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + if (mIsOpening) { + mFolder.setVisibility(View.GONE); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mFolder.setScaleX(1f); + mFolder.setScaleY(1f); + if (!mIsOpening) { + mFolder.setVisibility(View.GONE); + } + } + }); + return a; + } + + private void play(AnimatorSet as, Animator a) { + play(as, a, a.getStartDelay(), mDuration); + } + + private void play(AnimatorSet as, Animator a, long startDelay, int duration) { + a.setStartDelay(startDelay); + a.setDuration(duration); + as.play(a); + } + + private TimeInterpolator getPreviewItemInterpolator() { + return mFolderInterpolator; + } + + private Animator getAnimator(View view, Property property, float v1, float v2) { + return mIsOpening + ? ObjectAnimator.ofFloat(view, property, v1, v2) + : ObjectAnimator.ofFloat(view, property, v2, v1); + } + + private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) { + return mIsOpening + ? ObjectAnimator.ofArgb(drawable, property, v1, v2) + : ObjectAnimator.ofArgb(drawable, property, v2, v1); + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderIcon.kt b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderIcon.kt new file mode 100644 index 0000000000000000000000000000000000000000..f6d3e4d87e99925b56c885c7dd87eb82632ff989 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderIcon.kt @@ -0,0 +1,122 @@ +package foundation.e.blisslauncher.features.folder + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import foundation.e.blisslauncher.core.customviews.Folder +import foundation.e.blisslauncher.core.database.model.FolderItem +import foundation.e.blisslauncher.core.database.model.LauncherItem +import foundation.e.blisslauncher.core.touch.ItemClickHandler +import foundation.e.blisslauncher.core.utils.GraphicsUtil +import foundation.e.blisslauncher.features.notification.FolderDotInfo +import foundation.e.blisslauncher.features.test.IconTextView + +/** + * A text view which displays an icon on top side of it. + */ +@SuppressLint("AppCompatCustomView") +class FolderIcon @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : IconTextView(context, attrs, defStyle), FolderItem.FolderListener { + + private lateinit var folderItem: FolderItem + var folder: Folder? = null + + companion object { + fun fromXml( + resId: Int, + group: ViewGroup, + folderInfo: FolderItem + ): FolderIcon { + val icon = LayoutInflater.from(group.context) + .inflate(resId, group, false) as FolderIcon + icon.tag = folderInfo + icon.setOnClickListener(ItemClickHandler.INSTANCE) + icon.folderItem = folderInfo + val folder = Folder.fromXml(icon.launcher).apply { + this.dragController = launcher.dragController + this.folderIcon = icon + } + folder.bind(folderInfo) + icon.folder = folder + folderInfo.addListener(icon) + return icon + } + } + + override fun onAdd(item: LauncherItem?) { + if (mDotInfo is FolderDotInfo) { + (mDotInfo as FolderDotInfo).let { + val wasDotted: Boolean = it.hasDot() + it.addDotInfo(launcher.getDotInfoForItem(item)) + val isDotted: Boolean = it.hasDot() + updateDotScale(wasDotted, isDotted, true) + invalidate() + requestLayout() + } + } + } + + override fun onTitleChanged(title: CharSequence?) { + text = title + } + + override fun onRemove(item: LauncherItem?) { + if (mDotInfo is FolderDotInfo) { + (mDotInfo as FolderDotInfo).let { + val wasDotted: Boolean = it.hasDot() + it.subtractDotInfo(launcher.getDotInfoForItem(item)) + val isDotted: Boolean = it.hasDot() + updateDotScale(wasDotted, isDotted, true) + invalidate() + requestLayout() + } + } + } + + override fun onItemsChanged(animate: Boolean) { + updateFolderIcon() + invalidate() + requestLayout() + } + + private fun updateFolderIcon() { + folderItem.icon = GraphicsUtil(context).generateFolderIcon(context, folderItem) + applyFromFolderItem(folderItem) + } + + fun applyFromFolderItem(item: FolderItem) { + applyFromShortcutItem(item) + folderItem = item + } + + fun setDotInfo(dotInfo: FolderDotInfo) { + val wasDotted = mDotInfo is FolderDotInfo && (mDotInfo as FolderDotInfo).hasDot() + updateDotScale(wasDotted, dotInfo.hasDot(), true) + mDotInfo = dotInfo + } + + override fun hasDot(): Boolean { + return mDotInfo != null && (mDotInfo as FolderDotInfo).hasDot() + } + + fun removeListeners() { + folderItem.removeListener(this) + } + + fun acceptDrop(): Boolean { + return !(folder?.isDestroyed() ?: true) + } + + fun addItem(item: LauncherItem) { + addItem(item, true) + } + + fun addItem(item: LauncherItem?, animate: Boolean) { + folderItem.add(item, animate) + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderPagerAdapter.java b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderPagerAdapter.java index e636db0beab635ad987198f66a306a0109338a14..8c1c4380854974adeecca44bb32b707775e9ab29 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderPagerAdapter.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderPagerAdapter.java @@ -1,6 +1,7 @@ package foundation.e.blisslauncher.features.folder; import android.content.Context; +import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -12,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; +import java.util.ArrayList; import java.util.List; import foundation.e.blisslauncher.R; @@ -28,6 +30,7 @@ public class FolderPagerAdapter extends PagerAdapter { private Context mContext; private List mFolderAppItems; private VariantDeviceProfile mDeviceProfile; + private List grids = new ArrayList<>(); public FolderPagerAdapter( Context context, @@ -72,6 +75,7 @@ public class FolderPagerAdapter extends PagerAdapter { viewGroup.addView(appView); i++; } + grids.add(viewGroup); container.addView(viewGroup); return viewGroup; } @@ -93,4 +97,9 @@ public class FolderPagerAdapter extends PagerAdapter { ) { container.removeView((View) object); } + + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderViewPager.kt b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderViewPager.kt new file mode 100644 index 0000000000000000000000000000000000000000..673226c945755dccdf97843efe76658a2d8bea07 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/features/folder/FolderViewPager.kt @@ -0,0 +1,78 @@ +package foundation.e.blisslauncher.features.folder + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import androidx.core.view.get +import androidx.viewpager.widget.ViewPager +import foundation.e.blisslauncher.core.customviews.LauncherPagedView +import foundation.e.blisslauncher.core.database.model.LauncherItem + +class FolderViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + + fun getFirstItem(): View? { + if (childCount < 1) { + return null + } + val currContainer: GridLayout = getChildAt(currentItem) as GridLayout + return if (currContainer.childCount > 0) { + currContainer[0] + } else { + null + } + } + + fun getLastItem(): View? { + if (childCount < 1) { + return null + } + val currContainer: GridLayout = getChildAt(currentItem) as GridLayout + val lastRank = currContainer.childCount - 1 + return if (currContainer.childCount > 0) { + currContainer[lastRank] + } else { + null + } + } + + fun iterateOverItems(op: LauncherPagedView.ItemOperator): View? { + for (k in 0 until childCount) { + val page: GridLayout = getChildAt(k) as GridLayout + for (j in 0 until page.childCount) { + val v: View? = page.getChildAt(j) + if (v != null && op.evaluate(v.tag as LauncherItem, v, j)) { + return v + } + } + } + return null + } + + fun removeItem(v: View?) { + for (i in childCount - 1 downTo 0) { + val page: GridLayout = getChildAt(i) as GridLayout + page.removeView(v) + } + } + + fun getItemCount(): Int { + val lastPageIndex = childCount - 1 + return if (lastPageIndex < 0) { + 0 + } else { + (getChildAt(lastPageIndex) as GridLayout).childCount + lastPageIndex * 9 // maxItems per page + } + } + + /** + * Sets the focus on the first visible child. + */ + fun setFocusOnFirstChild() { + (getChildAt(currentItem) as ViewGroup?)?.getChildAt(0)?.requestFocus() + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppProvider.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppProvider.java index 2c7afc100158b8547a2d04edc40cf8ba6b25c5a9..e1b6685a29c60ac488fa6d9754f379ae1d071417 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppProvider.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppProvider.java @@ -1,6 +1,5 @@ package foundation.e.blisslauncher.features.launcher; - import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -18,10 +17,12 @@ import android.os.UserManager; import android.provider.MediaStore; import android.util.Log; import android.util.LongSparseArray; + +import androidx.core.content.ContextCompat; + import foundation.e.blisslauncher.BlissLauncher; import foundation.e.blisslauncher.R; import foundation.e.blisslauncher.core.Utilities; -import foundation.e.blisslauncher.core.broadcast.PackageAddedRemovedHandler; import foundation.e.blisslauncher.core.database.DatabaseManager; import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.FolderItem; @@ -38,6 +39,7 @@ import foundation.e.blisslauncher.features.launcher.tasks.LoadDatabaseTask; import foundation.e.blisslauncher.features.launcher.tasks.LoadShortcutTask; import foundation.e.blisslauncher.features.shortcuts.DeepShortcutManager; import foundation.e.blisslauncher.features.shortcuts.ShortcutInfoCompat; + import java.text.Collator; import java.util.ArrayList; import java.util.Collection; @@ -46,7 +48,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; - // TODO: Find better solution instead of excessively using volatile and synchronized. // - and use RxJava instead of bad async tasks. public class AppProvider { @@ -56,6 +57,11 @@ public class AppProvider { */ List mLauncherItems; + /** + * Represents all items including the ones which are added when launcher is not running. + */ + AppsRepository.AllItems allItems; + /** * Represents networkItems stored in database. */ @@ -80,6 +86,7 @@ public class AppProvider { private static final String MICROG_PACKAGE = "com.google.android.gms"; private static final String MUPDF_PACKAGE = "com.artifex.mupdf.mini.app"; private static final String PDF_VIEWER_PACKAGE = "com.gsnathan.pdfviewer"; + private static final String PDF_VIEWER_E_PACKAGE = "foundation.e.pdfviewer"; private static final String OPENKEYCHAIN_PACKAGE = "org.sufficientlysecure.keychain"; private static final String LIBREOFFICE_PACKAGE = "org.documentfoundation.libreoffice"; @@ -91,6 +98,7 @@ public class AppProvider { DISABLED_PACKAGES.add(MICROG_PACKAGE); DISABLED_PACKAGES.add(MUPDF_PACKAGE); DISABLED_PACKAGES.add(PDF_VIEWER_PACKAGE); + DISABLED_PACKAGES.add(PDF_VIEWER_E_PACKAGE); DISABLED_PACKAGES.add(OPENKEYCHAIN_PACKAGE); DISABLED_PACKAGES.add(LIBREOFFICE_PACKAGE); } @@ -116,86 +124,8 @@ public class AppProvider { assert manager != null; final LauncherApps launcher = (LauncherApps) mContext.getSystemService( - Context.LAUNCHER_APPS_SERVICE); + Context.LAUNCHER_APPS_SERVICE); assert launcher != null; - - launcher.registerCallback(new LauncherApps.Callback() { - @Override - public void onPackageRemoved(String packageName, android.os.UserHandle user) { - if (packageName.equalsIgnoreCase(MICROG_PACKAGE) || packageName.equalsIgnoreCase( - MUPDF_PACKAGE)) { - return; - } - - PackageAddedRemovedHandler.handleEvent(mContext, - "android.intent.action.PACKAGE_REMOVED", - packageName, new UserHandle(manager.getSerialNumberForUser(user), user), - false - ); - } - - @Override - public void onPackageAdded(String packageName, android.os.UserHandle user) { - if (packageName.equalsIgnoreCase(MICROG_PACKAGE) || packageName.equalsIgnoreCase( - MUPDF_PACKAGE)) { - return; - } - - PackageAddedRemovedHandler.handleEvent(mContext, - "android.intent.action.PACKAGE_ADDED", - packageName, new UserHandle(manager.getSerialNumberForUser(user), user), - false - ); - } - - @Override - public void onPackageChanged(String packageName, android.os.UserHandle user) { - if (packageName.equalsIgnoreCase(MICROG_PACKAGE) || packageName.equalsIgnoreCase( - MUPDF_PACKAGE)) { - return; - } - - PackageAddedRemovedHandler.handleEvent(mContext, - "android.intent.action.PACKAGE_CHANGED", - packageName, new UserHandle(manager.getSerialNumberForUser(user), user), - true - ); - } - - @Override - public void onPackagesAvailable(String[] packageNames, android.os.UserHandle user, - boolean replacing) { - Log.d(TAG, "onPackagesAvailable() called with: packageNames = [" + packageNames + "], user = [" + user + "], replacing = [" + replacing + "]"); - for (String packageName : packageNames) { - PackageAddedRemovedHandler.handleEvent(mContext, - "android.intent.action.MEDIA_MOUNTED", - packageName, new UserHandle(manager.getSerialNumberForUser(user), user), false - ); - } - } - - @Override - public void onPackagesUnavailable(String[] packageNames, android.os.UserHandle user, - boolean replacing) { - Log.d(TAG, "onPackagesUnavailable() called with: packageNames = [" + packageNames + "], user = [" + user + "], replacing = [" + replacing + "]"); - PackageAddedRemovedHandler.handleEvent(mContext, - "android.intent.action.MEDIA_UNMOUNTED", - null, new UserHandle(manager.getSerialNumberForUser(user), user), false - ); - } - - @Override - public void onPackagesSuspended(String[] packageNames, android.os.UserHandle user) { - Log.d(TAG, "onPackagesSuspended() called with: packageNames = [" + packageNames + "], user = [" + user + "]"); - } - - @Override - public void onPackagesUnsuspended(String[] packageNames, android.os.UserHandle user) { - super.onPackagesUnsuspended(packageNames, user); - Log.d(TAG, "onPackagesUnsuspended() called with: packageNames = [" + packageNames + "], user = [" + user + "]"); - } - }); - mAppsRepository = AppsRepository.getAppsRepository(); } @@ -269,7 +199,11 @@ public class AppProvider { } public synchronized void loadDatabaseOver(List databaseItems) { - Log.d(TAG, "loadDatabaseOver() called with: databaseItems = [" + Thread.currentThread().getName() + "]" + mStopped); + Log.d( + TAG, + "loadDatabaseOver() called with: databaseItems = [" + Thread.currentThread() + .getName() + "]" + mStopped + ); this.mDatabaseItems = databaseItems; databaseLoaded = true; handleAllProviderLoaded(); @@ -278,19 +212,19 @@ public class AppProvider { private synchronized void handleAllProviderLoaded() { if (appsLoaded && shortcutsLoaded && databaseLoaded) { if (mDatabaseItems == null || mDatabaseItems.size() <= 0) { - mLauncherItems = prepareDefaultLauncherItems(); + allItems = prepareDefaultLauncherItems(); } else { - mLauncherItems = prepareLauncherItems(); + allItems = prepareLauncherItems(); } - mAppsRepository.updateAppsRelay(mLauncherItems); + mAppsRepository.updateAllAppsRelay(allItems); } } - private List prepareLauncherItems() { + private AppsRepository.AllItems prepareLauncherItems() { Log.d(TAG, "prepareLauncherItems() called"); - /** - * Indices of folder in {@link #mLauncherItems}. + /* + Indices of folder in {@link #mLauncherItems}. */ LongSparseArray foldersIndex = new LongSparseArray<>(); List mLauncherItems = new ArrayList<>(); @@ -306,7 +240,13 @@ public class AppProvider { if ((isAppOnSdcard(databaseItem.packageName, userHandle) || !isSdCardReady ) && !DISABLED_PACKAGES.contains(databaseItem.packageName)) { Log.d(TAG, "Missing package: " + databaseItem.packageName); - Log.d(TAG, "Is App on Sdcard " + isAppOnSdcard(databaseItem.packageName, userHandle)); + Log.d( + TAG, + "Is App on Sdcard " + isAppOnSdcard( + databaseItem.packageName, + userHandle + ) + ); Log.d(TAG, "Is Sdcard ready " + isSdCardReady); pendingPackages.addToList(userHandle, databaseItem.packageName); @@ -316,7 +256,8 @@ public class AppProvider { applicationItem.user = userHandle; applicationItem.componentName = databaseItem.getTargetComponent(); applicationItem.packageName = databaseItem.packageName; - applicationItem.icon = getContext().getDrawable(R.drawable.default_icon); + applicationItem.icon = + ContextCompat.getDrawable(getContext(), R.drawable.default_icon); applicationItem.isDisabled = true; } else { DatabaseManager.getManager(mContext).removeLauncherItem(databaseItem.id); @@ -329,7 +270,7 @@ public class AppProvider { applicationItem.cell = databaseItem.cell; applicationItem.keyId = databaseItem.keyId; if (applicationItem.container == Constants.CONTAINER_DESKTOP - || applicationItem.container == Constants.CONTAINER_HOTSEAT) { + || applicationItem.container == Constants.CONTAINER_HOTSEAT) { mLauncherItems.add(applicationItem); } else { Integer index = foldersIndex.get(applicationItem.container); @@ -352,12 +293,12 @@ public class AppProvider { } if (shortcutItem.container == Constants.CONTAINER_DESKTOP - || shortcutItem.container == Constants.CONTAINER_HOTSEAT) { + || shortcutItem.container == Constants.CONTAINER_HOTSEAT) { mLauncherItems.add(shortcutItem); } else { FolderItem folderItem = - (FolderItem) mLauncherItems.get( - foldersIndex.get(shortcutItem.container)); + (FolderItem) mLauncherItems.get( + foldersIndex.get(shortcutItem.container)); if (folderItem.items == null) { folderItem.items = new ArrayList<>(); } @@ -379,43 +320,49 @@ public class AppProvider { if (foldersIndex.size() > 0) { for (int i = 0; i < foldersIndex.size(); i++) { FolderItem folderItem = - (FolderItem) mLauncherItems.get(foldersIndex.get(foldersIndex.keyAt(i))); + (FolderItem) mLauncherItems.get(foldersIndex.get(foldersIndex.keyAt(i))); if (folderItem.items == null || folderItem.items.size() == 0) { DatabaseManager.getManager(mContext).removeLauncherItem(folderItem.id); mLauncherItems.remove((int) foldersIndex.get(foldersIndex.keyAt(i))); } else { - folderItem.icon = new GraphicsUtil(mContext).generateFolderIcon(mContext, - folderItem); + folderItem.icon = new GraphicsUtil(mContext).generateFolderIcon( + mContext, + folderItem + ); } } } for (LauncherItem mLauncherItem : mLauncherItems) { - Log.i(TAG, "prepareLauncherItems: "+mLauncherItem); + Log.i(TAG, "prepareLauncherItems: " + mLauncherItem); } + applicationItems.removeAll(mDatabaseItems); - List mutableList = new ArrayList<>(applicationItems); - Collections.sort(mutableList, (app1, app2) -> { + List mutableList = new ArrayList<>(applicationItems); + mutableList.sort((app1, app2) -> { Collator collator = Collator.getInstance(); return collator.compare(app1.title.toString(), app2.title.toString()); }); - mLauncherItems.addAll(mutableList); - return mLauncherItems; + + return new AppsRepository.AllItems(mLauncherItems, mutableList); } private boolean isAppOnSdcard(String packageName, UserHandle userHandle) { - ApplicationInfo info = null; + ApplicationInfo info; try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { info = ((LauncherApps) mContext.getSystemService( - Context.LAUNCHER_APPS_SERVICE)).getApplicationInfo( - packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userHandle.getRealHandle()); + Context.LAUNCHER_APPS_SERVICE)).getApplicationInfo( + packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, + userHandle.getRealHandle() + ); return info != null && (info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } else { info = getContext().getPackageManager() - .getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES); - return info != null && info.enabled; + .getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES); + return info.enabled; } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); @@ -430,7 +377,8 @@ public class AppProvider { shortcutItem.title = databaseItem.title.toString(); shortcutItem.icon_blob = databaseItem.icon_blob; Bitmap bitmap = BitmapFactory.decodeByteArray(databaseItem.icon_blob, 0, - databaseItem.icon_blob.length); + databaseItem.icon_blob.length + ); shortcutItem.icon = new BitmapDrawable(mContext.getResources(), bitmap); shortcutItem.launchIntent = databaseItem.getIntent(); shortcutItem.launchIntentUri = databaseItem.launchIntentUri; @@ -444,8 +392,10 @@ public class AppProvider { private ShortcutItem prepareShortcutForOreo(LauncherItem databaseItem) { ShortcutInfoCompat info = mShortcutInfoCompats.get(databaseItem.id); if (info == null) { - Log.d(TAG, - "prepareShortcutForOreo() called with: databaseItem = [" + databaseItem + "]"); + Log.d( + TAG, + "prepareShortcutForOreo() called with: databaseItem = [" + databaseItem + "]" + ); return null; } @@ -453,10 +403,12 @@ public class AppProvider { shortcutItem.id = info.getId(); shortcutItem.packageName = info.getPackage(); shortcutItem.title = info.getShortLabel().toString(); - Drawable icon = DeepShortcutManager.getInstance(mContext).getShortcutIconDrawable(info, - mContext.getResources().getDisplayMetrics().densityDpi); + Drawable icon = DeepShortcutManager.getInstance(mContext).getShortcutIconDrawable( + info, + mContext.getResources().getDisplayMetrics().densityDpi + ); shortcutItem.icon = BlissLauncher.getApplication( - mContext).getIconsHandler().convertIcon(icon); + mContext).getIconsHandler().convertIcon(icon); shortcutItem.launchIntent = info.makeIntent(); shortcutItem.container = databaseItem.container; shortcutItem.screenId = databaseItem.screenId; @@ -465,25 +417,27 @@ public class AppProvider { return shortcutItem; } - private List prepareDefaultLauncherItems() { + private AppsRepository.AllItems prepareDefaultLauncherItems() { List mLauncherItems = new ArrayList<>(); List pinnedItems = new ArrayList<>(); PackageManager pm = mContext.getPackageManager(); Intent[] intents = { - new Intent(Intent.ACTION_DIAL), - new Intent(Intent.ACTION_VIEW, Uri.parse("sms:")), - new Intent(Intent.ACTION_VIEW, Uri.parse("http:")), - new Intent(MediaStore.ACTION_IMAGE_CAPTURE) + new Intent(Intent.ACTION_DIAL), + new Intent(Intent.ACTION_VIEW, Uri.parse("sms:")), + new Intent(Intent.ACTION_VIEW, Uri.parse("http:")), + new Intent(MediaStore.ACTION_IMAGE_CAPTURE) }; for (int i = 0; i < intents.length; i++) { String packageName = AppUtils.getPackageNameForIntent(intents[i], pm); LauncherApps launcherApps = (LauncherApps) mContext.getSystemService( - Context.LAUNCHER_APPS_SERVICE); - List list = launcherApps.getActivityList(packageName, - Process.myUserHandle()); + Context.LAUNCHER_APPS_SERVICE); + List list = launcherApps.getActivityList( + packageName, + Process.myUserHandle() + ); for (LauncherActivityInfo launcherActivityInfo : list) { ApplicationItem applicationItem = mApplicationItems.get( - launcherActivityInfo.getComponentName().flattenToString()); + launcherActivityInfo.getComponentName().flattenToString()); if (applicationItem != null) { applicationItem.container = Constants.CONTAINER_HOTSEAT; applicationItem.cell = i; @@ -494,19 +448,19 @@ public class AppProvider { } for (Map.Entry stringApplicationItemEntry : mApplicationItems - .entrySet()) { + .entrySet()) { if (!pinnedItems.contains(stringApplicationItemEntry.getValue())) { mLauncherItems.add(stringApplicationItemEntry.getValue()); } } - Collections.sort(mLauncherItems, (app1, app2) -> { + mLauncherItems.sort((app1, app2) -> { Collator collator = Collator.getInstance(); return collator.compare(app1.title.toString(), app2.title.toString()); }); mLauncherItems.addAll(pinnedItems); - return mLauncherItems; + return new AppsRepository.AllItems(mLauncherItems, Collections.emptyList()); } public AppsRepository getAppsRepository() { @@ -514,7 +468,7 @@ public class AppProvider { } public void clear() { - this.sInstance = null; + sInstance = null; mLauncherItems = new ArrayList<>(); mAppsRepository.updateAppsRelay(Collections.emptyList()); } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppsRepository.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppsRepository.java index cf4e5cbc7471522da15f3fe92c9a7784c7f209ff..8e0f05a4456616585a4f1091b2a46b4e41e24cdb 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppsRepository.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/AppsRepository.java @@ -3,17 +3,22 @@ package foundation.e.blisslauncher.features.launcher; import com.jakewharton.rxrelay2.BehaviorRelay; import foundation.e.blisslauncher.core.database.model.LauncherItem; + import java.util.List; public class AppsRepository { private static final String TAG = "AppsRepository"; + + @Deprecated private BehaviorRelay> appsRelay; + private BehaviorRelay allItems; private static AppsRepository sAppsRepository; private AppsRepository() { appsRelay = BehaviorRelay.create(); + allItems = BehaviorRelay.create(); } public static AppsRepository getAppsRepository() { @@ -25,13 +30,43 @@ public class AppsRepository { public void clearAll(){ appsRelay = BehaviorRelay.create(); + allItems = BehaviorRelay.create(); } public void updateAppsRelay(List launcherItems) { this.appsRelay.accept(launcherItems); } + public void updateAllAppsRelay(AllItems allItems) { + this.allItems.accept(allItems); + } + public BehaviorRelay> getAppsRelay() { return appsRelay; } -} + + public BehaviorRelay getAllItemsRelay() { + return allItems; + } + + public static class AllItems { + private final List items; + private final List newAddedItems; + + public AllItems( + List items, + List newAddedItems + ) { + this.items = items; + this.newAddedItems = newAddedItems; + } + + public List getItems() { + return items; + } + + public List getNewAddedItems() { + return newAddedItems; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.java index a3d7389542bb68eacbfcc472b6e82526cb040553..df84dd3ae64522440e07033049dd615201c46fe1 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.java @@ -17,7 +17,9 @@ package foundation.e.blisslauncher.features.launcher; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; @@ -27,16 +29,26 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.logging.Handler; + import foundation.e.blisslauncher.R; +import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider; +import foundation.e.blisslauncher.core.blur.ShaderBlurDrawable; import foundation.e.blisslauncher.core.customviews.Insettable; import foundation.e.blisslauncher.core.customviews.InsettableFrameLayout; +import foundation.e.blisslauncher.core.executors.MainThreadExecutor; +import foundation.e.blisslauncher.core.utils.MultiValueAlpha; import foundation.e.blisslauncher.features.test.CellLayout; import foundation.e.blisslauncher.features.test.TestActivity; import foundation.e.blisslauncher.features.test.VariantDeviceProfile; -public class Hotseat extends CellLayout implements Insettable { +public class Hotseat extends CellLayout implements Insettable, BlurWallpaperProvider.Listener { private final TestActivity mLauncher; + private final MultiValueAlpha mMultiValueAlpha; private CellLayout mContent; private static final String TAG = "Hotseat"; @@ -44,6 +56,30 @@ public class Hotseat extends CellLayout implements Insettable { @ViewDebug.ExportedProperty(category = "launcher") private boolean mHasVerticalHotseat; + private final BlurWallpaperProvider blurWallpaperProvider; + private ShaderBlurDrawable fullBlurDrawable = null; + private int blurAlpha = 255; + private final Drawable.Callback blurDrawableCallback = new Drawable.Callback() { + @Override + public void invalidateDrawable(@NonNull Drawable who) { + new MainThreadExecutor().execute(() -> invalidate()); + } + + @Override + public void scheduleDrawable( + @NonNull Drawable who, @NonNull Runnable what, long when + ) { + + } + + @Override + public void unscheduleDrawable( + @NonNull Drawable who, @NonNull Runnable what + ) { + + } + }; + public Hotseat(Context context) { this(context, null); } @@ -55,7 +91,20 @@ public class Hotseat extends CellLayout implements Insettable { public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLauncher = TestActivity.Companion.getLauncher(context); - setBackgroundColor(0x33000000); + setWillNotDraw(false); + blurWallpaperProvider = BlurWallpaperProvider.Companion.getInstance(getContext()); + createBlurDrawable(); + mMultiValueAlpha = new MultiValueAlpha(this, 1); + } + + private void createBlurDrawable() { + if (isAttachedToWindow() && fullBlurDrawable != null) { + fullBlurDrawable.stopListening(); + } + fullBlurDrawable = blurWallpaperProvider.createDrawable(); + fullBlurDrawable.setCallback(blurDrawableCallback); + fullBlurDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom()); + if (isAttachedToWindow()) fullBlurDrawable.startListening(); } // TODO: Remove this later. @@ -70,6 +119,44 @@ public class Hotseat extends CellLayout implements Insettable { setGridSize(idp.getInv().getNumHotseatIcons(), 1); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + BlurWallpaperProvider.Companion.getInstance(getContext()).addListener(this); + if (fullBlurDrawable != null) { + fullBlurDrawable.startListening(); + } + } + + public MultiValueAlpha.AlphaProperty getAlphaProperty(int index) { + return mMultiValueAlpha.getProperty(index); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + BlurWallpaperProvider.Companion.getInstance(getContext()).removeListener(this); + if (fullBlurDrawable != null) { + fullBlurDrawable.stopListening(); + } + } + + @Override + protected void onDraw(@Nullable Canvas canvas) { + if (fullBlurDrawable != null) { + fullBlurDrawable.draw(canvas); + } + super.onDraw(canvas); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (changed && fullBlurDrawable != null) { + fullBlurDrawable.setBounds(left, top, right, bottom); + } + super.onLayout(changed, left, top, right, bottom); + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // We don't want any clicks to go through to the hotseat unless the workspace is in @@ -96,4 +183,27 @@ public class Hotseat extends CellLayout implements Insettable { // Don't let if follow through to workspace return true; } + + @Override + public void onWallpaperChanged() { + + } + + @Override + public void onEnabledChanged() { + createBlurDrawable(); + } + + /** + * We only need to change left bound for hotseat blur layer. + */ + public void changeBlurBounds(float factor, boolean reset) { + if(fullBlurDrawable != null) { + if(reset) { + factor = 0f; + } + fullBlurDrawable.setBounds((int) ((getRight() - getLeft()) * factor), getTop(), getRight(), getBottom()); + fullBlurDrawable.invalidateSelf(); + } + } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/SearchInputDisposableObserver.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/SearchInputDisposableObserver.java index fd0ad5010ffc7d662d82db89e32e58848b155a19..1aa6a0ff2607e2380aeb3e54d3108ea72c524998 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/SearchInputDisposableObserver.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/SearchInputDisposableObserver.java @@ -43,7 +43,7 @@ public class SearchInputDisposableObserver extends DisposableObserver(), suggestionsResults.queryText); } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/suggestions/AutoCompleteAdapter.java b/app/src/main/java/foundation/e/blisslauncher/features/suggestions/AutoCompleteAdapter.java index 65943ddcfd3d4902e21a9ee772ee222499b7496a..dbf4e95730aac2f61f949f915cfcd044d272e929 100755 --- a/app/src/main/java/foundation/e/blisslauncher/features/suggestions/AutoCompleteAdapter.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/suggestions/AutoCompleteAdapter.java @@ -16,6 +16,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import foundation.e.blisslauncher.R; +import foundation.e.blisslauncher.core.customviews.LauncherPagedView; import foundation.e.blisslauncher.features.launcher.LauncherActivity; import foundation.e.blisslauncher.features.test.TestActivity; @@ -32,7 +33,13 @@ public class AutoCompleteAdapter extends public AutoCompleteAdapter(Context context) { super(); - mOnSuggestionClickListener = (TestActivity) context; + mOnSuggestionClickListener = null; + mInflater = LayoutInflater.from(context); + } + + public AutoCompleteAdapter(Context context, LauncherPagedView workspace) { + super(); + mOnSuggestionClickListener = workspace; mInflater = LayoutInflater.from(context); } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/BaseActivity.java b/app/src/main/java/foundation/e/blisslauncher/features/test/BaseActivity.java index a84799e2e6904848ee498885df92eb192e5fce73..ec246472ed2acd2af47c05487af93c93bf4c02e9 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/BaseActivity.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/BaseActivity.java @@ -24,6 +24,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.view.View.AccessibilityDelegate; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import foundation.e.blisslauncher.core.utils.ViewCache; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -87,6 +88,7 @@ public abstract class BaseActivity extends Activity { return mViewCache; } + @NonNull public VariantDeviceProfile getDeviceProfile() { return mDeviceProfile; } @@ -137,6 +139,10 @@ public abstract class BaseActivity extends Activity { mActivityFlags &= ~ACTIVITY_STATE_STARTED & ~ACTIVITY_STATE_USER_ACTIVE; mForceInvisible = 0; super.onStop(); + + // Reset the overridden sysui flags used for the task-swipe launch animation, this is a + // catch all for if we do not get resumed (and therefore not paused below) + getSystemUiController().updateUiState(SystemUiController.UI_STATE_OVERVIEW, 0); } @Override @@ -148,7 +154,7 @@ public abstract class BaseActivity extends Activity { // here instead of at the end of the animation because the start of the new activity does // not happen immediately, which would cause us to reset to launcher's sysui flags and then // back to the new app (causing a flash) - getSystemUiController().updateUiState(SystemUiController.UI_STATE_NORMAL, 0); + getSystemUiController().updateUiState(SystemUiController.UI_STATE_OVERVIEW, 0); } public boolean isStarted() { diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/CellLayout.kt b/app/src/main/java/foundation/e/blisslauncher/features/test/CellLayout.kt index 7931a580827c153dc1dda58d6562dfb7bb6df00a..94dbf46bdcd9a1d9e9ef7d5dedcb35c0598b2909 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/CellLayout.kt +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/CellLayout.kt @@ -15,6 +15,8 @@ import android.os.Handler import android.os.Looper import android.util.ArrayMap import android.util.AttributeSet +import android.util.Log +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils @@ -22,12 +24,14 @@ import android.widget.GridLayout import androidx.annotation.IntDef import foundation.e.blisslauncher.R import foundation.e.blisslauncher.core.Utilities +import foundation.e.blisslauncher.core.database.model.ApplicationItem import foundation.e.blisslauncher.core.database.model.LauncherItem +import foundation.e.blisslauncher.core.database.model.ShortcutItem import foundation.e.blisslauncher.core.utils.Constants -import foundation.e.blisslauncher.features.launcher.Hotseat import foundation.e.blisslauncher.features.test.anim.Interpolators import foundation.e.blisslauncher.features.test.dragndrop.DropTarget import foundation.e.blisslauncher.features.test.graphics.DragPreviewProvider +import foundation.e.blisslauncher.features.test.uninstall.UninstallHelper.isUninstallDisabled import java.lang.Double.MAX_VALUE import java.util.ArrayList import java.util.Arrays @@ -109,6 +113,8 @@ open class CellLayout @JvmOverloads constructor( const val WORKSPACE = 0 const val HOTSEAT = 1 + + private val paint = Paint() } init { @@ -190,13 +196,21 @@ open class CellLayout @JvmOverloads constructor( } } + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + Log.d( + TAG, + "onLayout() called with: changed = $changed, left = $left, top = $top, right = $right, bottom = $bottom, ${Utilities.dpiFromPx(60, context.resources.displayMetrics)}" + ) + super.onLayout(changed, left, top, right, bottom) + } + fun getContainerType() = mContainerType open fun getCellContentHeight(): Int { return Math.min( measuredHeight, launcher.deviceProfile - .getCellHeight(if (parent is Hotseat) Constants.CONTAINER_HOTSEAT else Constants.CONTAINER_DESKTOP) + .getCellHeight(mContainerType) ) } @@ -211,10 +225,6 @@ open class CellLayout @JvmOverloads constructor( requestLayout() } - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - super.onLayout(changed, left, top, right, bottom) - } - fun measureChild(child: View) { val lp = child.layoutParams as LayoutParams lp.rowSpec = spec(UNDEFINED) @@ -254,6 +264,17 @@ open class CellLayout @JvmOverloads constructor( return false } + open fun enableHardwareLayer(hasLayer: Boolean) { + setLayerType( + if (hasLayer) LAYER_TYPE_HARDWARE else LAYER_TYPE_NONE, + paint + ) + } + + open fun isHardwareLayerEnabled(): Boolean { + return layerType == LAYER_TYPE_HARDWARE + } + override fun cancelLongPress() { super.cancelLongPress() // Cancel long press for all children @@ -811,19 +832,26 @@ open class CellLayout @JvmOverloads constructor( result[1] = index / mCountX } - // appView.findViewById(R.id.app_label).setVisibility(GONE); + val genericLp: ViewGroup.LayoutParams = dragView.layoutParams val rowSpec = spec(UNDEFINED) val colSpec = spec(UNDEFINED) - val iconLayoutParams = LayoutParams(rowSpec, colSpec) - // iconLayoutParams.setGravity(Gravity.CENTER) - iconLayoutParams.height = if (mContainerType == HOTSEAT) - dp.hotseatCellHeightPx else dp.cellHeightPx - iconLayoutParams.width = dp.cellWidthPx - dragView.also { - if (it is IconTextView) it.setTextVisibility(mContainerType != HOTSEAT) + val lp: LayoutParams + if (genericLp !is LayoutParams) { + lp = LayoutParams(rowSpec, colSpec) + } else { + lp = genericLp + lp.rowSpec = rowSpec + lp.columnSpec = colSpec + } + lp.setGravity(Gravity.CENTER) + + // Get the canonical child id to uniquely represent this view in this screen + + val childId: Int = launcher.getViewIdForItem(dragView.tag as LauncherItem) + dragView.apply { + if (this is IconTextView) setTextVisibility(mContainerType != HOTSEAT) } - dragView.layoutParams = iconLayoutParams - addView(dragView, index) + addViewToCellLayout(dragView, index, childId, lp, true) // Update item info after reordering so that we always save correct state in database. // TODO: May optimize this @@ -867,6 +895,26 @@ open class CellLayout @JvmOverloads constructor( ) ) } + + val info: LauncherItem = dragView.getTag() as LauncherItem + if ((info is ApplicationItem || info is ShortcutItem) && !isUninstallDisabled( + info.user.realHandle, + context + ) + ) { + // Return early if this app is system app + if (info is ApplicationItem) { + if (info.isSystemApp != ApplicationItem.FLAG_SYSTEM_UNKNOWN) { + if (info.isSystemApp and ApplicationItem.FLAG_SYSTEM_NO != 0) { + dragView.applyUninstallIconState(true) + } + } else { + dragView.applyUninstallIconState(true) + } + } else if (info is ShortcutItem) { + dragView.applyUninstallIconState(true) + } + } } } } @@ -926,7 +974,7 @@ open class CellLayout @JvmOverloads constructor( return if (cellIdx < mOccupied.cells.size) { mOccupied.cells[cellIdx] } else { - throw RuntimeException("Position exceeds the bound of this CellLayout") + true } } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/IconTextView.kt b/app/src/main/java/foundation/e/blisslauncher/features/test/IconTextView.kt index dae87c1d82e8d938f5a4f0bdde62643250d0f645..87c4096dc79c7c2d79f11a2d2e4c264f9a770c0e 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/IconTextView.kt +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/IconTextView.kt @@ -27,7 +27,6 @@ import foundation.e.blisslauncher.core.database.model.ShortcutItem import foundation.e.blisslauncher.core.utils.Constants import foundation.e.blisslauncher.features.notification.DotInfo import foundation.e.blisslauncher.features.notification.DotRenderer -import foundation.e.blisslauncher.features.notification.FolderDotInfo import foundation.e.blisslauncher.features.test.uninstall.UninstallButtonRenderer import kotlin.math.roundToInt @@ -35,7 +34,7 @@ import kotlin.math.roundToInt * A text view which displays an icon on top side of it. */ @SuppressLint("AppCompatCustomView") -class IconTextView @JvmOverloads constructor( +open class IconTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet?, defStyle: Int @@ -43,7 +42,6 @@ class IconTextView @JvmOverloads constructor( companion object { private const val DISPLAY_WORKSPACE = 1 - private const val DISPLAY_FOLDER = 2 private val STATE_PRESSED = intArrayOf(android.R.attr.state_pressed) private const val TAG = "IconTextView" @@ -98,7 +96,7 @@ class IconTextView @JvmOverloads constructor( val bottom = top + defaultIconSize return Rect(left, top, right, bottom) } - private val launcher: TestActivity = TestActivity.getLauncher(context) + val launcher: TestActivity = TestActivity.getLauncher(context) private val dp = launcher.deviceProfile private val defaultIconSize = dp.iconSizePx @@ -115,7 +113,7 @@ class IconTextView @JvmOverloads constructor( private var longPressHelper: CheckLongPressHelper - private var mDotInfo: DotInfo? = null + protected var mDotInfo: DotInfo? = null private var mDotScaleAnim: Animator? = null private var mForceHideDot = false @@ -134,6 +132,7 @@ class IconTextView @JvmOverloads constructor( compoundDrawablePadding = dp.iconDrawablePaddingPx ellipsize = TruncateAt.END longPressHelper = CheckLongPressHelper(this) + setTextAlpha(1f) } override fun onFocusChanged( @@ -153,7 +152,7 @@ class IconTextView @JvmOverloads constructor( override fun setTextColor(colors: ColorStateList) { mTextColor = colors.defaultColor - if (java.lang.Float.compare(mTextAlpha, 1f) == 0) { + if (mTextAlpha.compareTo(1f) == 0) { super.setTextColor(colors) } else { super.setTextColor(getModifiedColor()) @@ -198,13 +197,7 @@ class IconTextView @JvmOverloads constructor( } } - fun setDotInfo(item: FolderItem, dotInfo: FolderDotInfo) { - val wasDotted = mDotInfo is FolderDotInfo && (mDotInfo as FolderDotInfo).hasDot() - updateDotScale(wasDotted, dotInfo.hasDot(), true) - mDotInfo = dotInfo - } - - private fun updateDotScale(wasDotted: Boolean, isDotted: Boolean, animate: Boolean) { + protected fun updateDotScale(wasDotted: Boolean, isDotted: Boolean, animate: Boolean) { val newDotScale: Float = if (isDotted) 1f else 0f mDotRenderer = mActivity.deviceProfile.mDotRenderer if (wasDotted || isDotted) { @@ -241,12 +234,12 @@ class IconTextView @JvmOverloads constructor( mDotScaleAnim?.cancel() } - private fun animateDotScale(dotScales: Float) { + fun animateDotScale(vararg dotScales: Float) { cancelDotScaleAnim() mDotScaleAnim = ObjectAnimator.ofFloat( this, DOT_SCALE_PROPERTY, - dotScales + *dotScales ).apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { @@ -354,10 +347,7 @@ class IconTextView @JvmOverloads constructor( } } - private fun hasDot(): Boolean { - if (mDotInfo != null && mDotInfo is FolderDotInfo) { - return (mDotInfo as FolderDotInfo).hasDot() - } + open fun hasDot(): Boolean { return mDotInfo != null } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherAppState.java b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherAppState.java index 829cba995b4ea989934976563e074a5b6e65a9e4..e40c638ca353afc148e87834f777e064efdc6741 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherAppState.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherAppState.java @@ -22,14 +22,20 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.os.Looper; +import android.os.UserHandle; +import android.util.ArrayMap; import android.util.Log; +import androidx.annotation.NonNull; import foundation.e.blisslauncher.core.ConfigMonitor; import foundation.e.blisslauncher.core.UserManagerCompat; import foundation.e.blisslauncher.core.executors.MainThreadExecutor; import foundation.e.blisslauncher.core.utils.Preconditions; import foundation.e.blisslauncher.core.utils.SecureSettingsObserver; import foundation.e.blisslauncher.features.notification.NotificationListener; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -45,6 +51,8 @@ public class LauncherAppState { // We do not need any synchronization for this variable as its only written on UI thread. private static LauncherAppState INSTANCE; + private LauncherApps launcherApps; + private final Context mContext; private final InvariantDeviceProfile mInvariantDeviceProfile; @@ -52,6 +60,9 @@ public class LauncherAppState { private TestActivity launcher; private LauncherModel mModel; + private final ArrayMap mCallbacks = + new ArrayMap<>(); + public static LauncherAppState getInstance(final Context context) { if (INSTANCE == null) { if (Looper.myLooper() == Looper.getMainLooper()) { @@ -82,6 +93,7 @@ public class LauncherAppState { private LauncherAppState(Context context) { + launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); Log.v(TestActivity.TAG, "LauncherAppState initiated"); Preconditions.assertUIThread(); mContext = context; @@ -89,6 +101,11 @@ public class LauncherAppState { mInvariantDeviceProfile = new InvariantDeviceProfile(mContext); mModel = new LauncherModel(this); + WrappedCallback wrappedCallback = new WrappedCallback(mModel); + synchronized (mCallbacks) { + mCallbacks.put(mModel, wrappedCallback); + } + launcherApps.registerCallback(wrappedCallback); // Register intent receivers IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); @@ -129,6 +146,13 @@ public class LauncherAppState { if (mNotificationDotsObserver != null) { mNotificationDotsObserver.unregister(); } + final WrappedCallback wrappedCallback; + synchronized (mCallbacks) { + wrappedCallback = mCallbacks.remove(mModel); + } + if (wrappedCallback != null) { + launcherApps.unregisterCallback(wrappedCallback); + } } LauncherModel setLauncher(TestActivity launcher) { @@ -153,4 +177,54 @@ public class LauncherAppState { } + private static class WrappedCallback extends LauncherApps.Callback { + private final OnAppsChangedCallback mCallback; + + public WrappedCallback(OnAppsChangedCallback callback) { + mCallback = callback; + } + + @Override + public void onPackageRemoved(String packageName, UserHandle user) { + mCallback.onPackageRemoved(packageName, user); + } + + @Override + public void onPackageAdded(String packageName, UserHandle user) { + mCallback.onPackageAdded(packageName, user); + } + + @Override + public void onPackageChanged(String packageName, UserHandle user) { + mCallback.onPackageChanged(packageName, user); + } + + @Override + public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { + mCallback.onPackagesAvailable(packageNames, user, replacing); + } + + @Override + public void onPackagesUnavailable(String[] packageNames, UserHandle user, + boolean replacing) { + mCallback.onPackagesUnavailable(packageNames, user, replacing); + } + + @Override + public void onPackagesSuspended(String[] packageNames, UserHandle user) { + mCallback.onPackagesSuspended(packageNames, user); + } + + @Override + public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { + mCallback.onPackagesUnsuspended(packageNames, user); + } + + @Override + public void onShortcutsChanged(@NonNull String packageName, + @NonNull List shortcuts, + @NonNull UserHandle user) { + mCallback.onShortcutsChanged(packageName, shortcuts, user); + } + } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherItemMatcher.java b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherItemMatcher.java index a40b0dd9ca32e5795a3a8a3a38a3d60b336c4415..88f43c8c80fb4eb30d7dd9f4aa620426f184be4d 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherItemMatcher.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherItemMatcher.java @@ -5,6 +5,7 @@ import android.os.UserHandle; import java.util.HashSet; +import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.FolderItem; import foundation.e.blisslauncher.core.database.model.LauncherItem; import foundation.e.blisslauncher.core.database.model.ShortcutItem; @@ -22,7 +23,13 @@ public abstract class LauncherItemMatcher { public final HashSet filterItemInfos(Iterable infos) { HashSet filtered = new HashSet<>(); for (LauncherItem i : infos) { - if (i instanceof ShortcutItem) { + if (i instanceof ApplicationItem) { + ApplicationItem info = (ApplicationItem) i; + ComponentName cn = info.getTargetComponent(); + if (cn != null && matches(info, cn)) { + filtered.add(info); + } + } else if (i instanceof ShortcutItem) { ShortcutItem info = (ShortcutItem) i; ComponentName cn = info.getTargetComponent(); if (cn != null && matches(info, cn)) { @@ -106,7 +113,7 @@ public abstract class LauncherItemMatcher { return new LauncherItemMatcher() { @Override public boolean matches(LauncherItem info, ComponentName cn) { - return packageNames.contains(cn.getPackageName()) && info.user.equals(user); + return packageNames.contains(cn.getPackageName()) && info.user.getRealHandle().equals(user); } }; } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherModel.java b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherModel.java index 46557bc4cfddd340c7d935c3eaa67f8dfc861076..5d051c3b06ed5527712dd10b139b76767056a108 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherModel.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherModel.java @@ -1,48 +1,152 @@ +/* + * 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.features.test; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; -import android.telecom.Call; +import android.os.UserHandle; import android.util.Log; import android.util.Pair; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import foundation.e.blisslauncher.core.UserManagerCompat; +import foundation.e.blisslauncher.core.Utilities; +import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.LauncherItem; import foundation.e.blisslauncher.core.executors.MainThreadExecutor; +import foundation.e.blisslauncher.core.utils.AppUtils; import foundation.e.blisslauncher.core.utils.Constants; +import foundation.e.blisslauncher.core.utils.FlagOp; +import foundation.e.blisslauncher.core.utils.ItemInfoMatcher; import foundation.e.blisslauncher.core.utils.Preconditions; -import foundation.e.blisslauncher.features.shortcuts.DeepShortcutManager; +import foundation.e.blisslauncher.features.shortcuts.InstallShortcutReceiver; -public class LauncherModel extends BroadcastReceiver { +public class LauncherModel extends BroadcastReceiver implements + OnAppsChangedCallback { private static final boolean DEBUG_RECEIVER = false; static final String TAG = "Launcher.Model"; private final MainThreadExecutor mUiExecutor = new MainThreadExecutor(); - final LauncherAppState mApp; + final LauncherAppState mApp; final Object mLock = new Object(); WeakReference mCallbacks; static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); private static final Looper mWorkerLooper; + static { sWorkerThread.start(); mWorkerLooper = sWorkerThread.getLooper(); } + static final Handler sWorker = new Handler(mWorkerLooper); + @Override + public void onPackageRemoved(String packageName, UserHandle user) { + // TODO: Handle package removed here. + onPackagesRemoved(user, packageName); + } + + public void onPackagesRemoved(UserHandle user, String... packages) { + final HashSet removedPackages = new HashSet<>(); + Collections.addAll(removedPackages, packages); + if (!removedPackages.isEmpty()) { + LauncherItemMatcher removeMatch = LauncherItemMatcher.ofPackages(removedPackages, user); + deleteAndBindComponentsRemoved(removeMatch); + + // Remove any queued items from the install queue + if (sWorkerThread.getThreadId() == Process.myTid()) { + } else { + // If we are not on the worker thread, then post to the worker handler + sWorker.post(() -> InstallShortcutReceiver + .removeFromInstallQueue(mApp.getContext(), removedPackages, user)); + } + } + } + + private void deleteAndBindComponentsRemoved(LauncherItemMatcher removeMatch) { + mCallbacks.get().bindWorkspaceComponentsRemoved(removeMatch); + } + + @Override + public void onPackageAdded(String packageName, UserHandle user) { + final Context context = mApp.getContext(); + FlagOp flagOp = FlagOp.NO_OP; + final HashSet packageSet = new HashSet<>(Arrays.asList(packageName)); + ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, user); + + final List added = new ArrayList<>(); + added.addAll(AppUtils.createAppItems(context, + packageName, + new foundation.e.blisslauncher.core.utils.UserHandle(UserManagerCompat + .getInstance(context).getSerialNumberForUser(user), user) + )); + mUiExecutor.execute(() -> mCallbacks.get().bindAppsAdded(added)); + } + + @Override + public void onPackageChanged(String packageName, UserHandle user) { + + } + + @Override + public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { + + } + + @Override + public void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing) { + + } + + @Override + public void onPackagesSuspended(String[] packageNames, UserHandle user) { + + } + + @Override + public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { + + } + + @Override + public void onShortcutsChanged( + String packageName, List shortcuts, UserHandle user + ) { + + } + // Runnable to check if the shortcuts permission has changed. /*private final Runnable mShortcutPermissionCheckRunnable = new Runnable() { @Override @@ -59,6 +163,8 @@ public class LauncherModel extends BroadcastReceiver { public interface Callbacks { void bindAppsAdded(List items); + + void bindWorkspaceComponentsRemoved(LauncherItemMatcher matcher); } LauncherModel(LauncherAppState app) { @@ -87,13 +193,12 @@ public class LauncherModel extends BroadcastReceiver { Callbacks callbacks = getCallback(); if (callbacks != null) { //callbacks.preAddApps(); - List items = new ArrayList<>(); + List items = new ArrayList<>(); for (Pair entry : itemList) { items.add(entry.first); } mUiExecutor.execute(() -> callbacks.bindAppsAdded(items)); } - } public Callbacks getCallback() { diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherRootView.java b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherRootView.java index e763b81f45d7ebbfb454c0e6efde9576fc465735..69e9f33d0eea6363c1423d6d137abe9012f08b37 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherRootView.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/LauncherRootView.java @@ -10,18 +10,28 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; +import android.util.IntProperty; +import android.util.Log; +import android.util.Property; import android.view.View; import android.view.ViewDebug; import android.view.WindowInsets; +import androidx.annotation.NonNull; import foundation.e.blisslauncher.core.Utilities; +import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider; +import foundation.e.blisslauncher.core.blur.ShaderBlurDrawable; import foundation.e.blisslauncher.core.customviews.InsettableFrameLayout; +import foundation.e.blisslauncher.core.executors.MainThreadExecutor; +import foundation.e.blisslauncher.features.test.anim.Interpolators; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; -public class LauncherRootView extends InsettableFrameLayout { +public class LauncherRootView extends InsettableFrameLayout implements + BlurWallpaperProvider.Listener { private final Rect mTempRect = new Rect(); @@ -39,6 +49,53 @@ public class LauncherRootView extends InsettableFrameLayout { private WindowStateListener mWindowStateListener; private boolean mDisallowBackGesture; + private BlurWallpaperProvider blurWallpaperProvider; + private ShaderBlurDrawable fullBlurDrawable; + MainThreadExecutor mainThreadExecutor = new MainThreadExecutor(); + private Drawable.Callback blurDrawableCallback = new Drawable.Callback() { + @Override + public void invalidateDrawable(@NonNull Drawable who) { + mainThreadExecutor.execute(() -> invalidate()); + } + + @Override + public void scheduleDrawable( + @NonNull Drawable who, @NonNull Runnable what, long when + ) { + + } + + @Override + public void unscheduleDrawable( + @NonNull Drawable who, @NonNull Runnable what + ) { + + } + }; + + /** + * A Property wrapper around the blur alpha functionality. + * + * Note: Blur alpha values varies between 0 to 255. + */ + public static final Property BLUR_ALPHA = + new IntProperty("blur_alpha") { + @Override + public Integer get(LauncherRootView object) { + Log.d("LauncherRootView", "get() called with: object = [" + object + "]"); + return object.getBlurAlpha(); + } + + @Override + public void setValue(LauncherRootView object, int value) { + Log.d( + "LauncherRootView", + "setValue() called with: object = [" + object + "], value = [" + value + "]" + ); + object.setBlurAlpha(value); + } + }; + public LauncherRootView(Context context, AttributeSet attrs) { super(context, attrs); @@ -46,7 +103,44 @@ public class LauncherRootView extends InsettableFrameLayout { mOpaquePaint.setColor(Color.BLACK); mOpaquePaint.setStyle(Paint.Style.FILL); + setWillNotDraw(false); mLauncher = TestActivity.Companion.getLauncher(context); + blurWallpaperProvider = BlurWallpaperProvider.Companion.getInstance(context); + createBlurDrawable(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + blurWallpaperProvider.addListener(this); + if (fullBlurDrawable != null) { + fullBlurDrawable.startListening(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + blurWallpaperProvider.removeListener(this); + if (fullBlurDrawable != null) { + fullBlurDrawable.stopListening(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (fullBlurDrawable != null) { + fullBlurDrawable.draw(canvas); + } + super.onDraw(canvas); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed && fullBlurDrawable != null) { + fullBlurDrawable.setBounds(left, top, right, bottom); + } } @Override @@ -69,7 +163,7 @@ public class LauncherRootView extends InsettableFrameLayout { mConsumedInsets.bottom = insets.bottom; insets.set(0, insets.top, 0, 0); drawInsetBar = true; - } else if ((insets.right > 0 || insets.left > 0) && + } else if ((insets.right > 0 || insets.left > 0) && getContext().getSystemService(ActivityManager.class).isLowRamDevice()) { mConsumedInsets.left = insets.left; mConsumedInsets.right = insets.right; @@ -106,12 +200,14 @@ public class LauncherRootView extends InsettableFrameLayout { @Override public WindowInsets onApplyWindowInsets(@Nullable WindowInsets insets) { mTempRect.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), - insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); + insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom() + ); handleSystemWindowInsets(mTempRect); if (Utilities.ATLEAST_Q) { return insets.inset(mConsumedInsets.left, mConsumedInsets.top, - mConsumedInsets.right, mConsumedInsets.bottom); + mConsumedInsets.right, mConsumedInsets.bottom + ); } else { return insets.replaceSystemWindowInsets(mTempRect); } @@ -180,6 +276,54 @@ public class LauncherRootView extends InsettableFrameLayout { : Collections.emptyList()); } + @Override + public void onEnabledChanged() { + createBlurDrawable(); + } + + @Override + public void onWallpaperChanged() { + + } + + private void createBlurDrawable() { + if (isAttachedToWindow() && fullBlurDrawable != null) { + fullBlurDrawable.stopListening(); + } + fullBlurDrawable = blurWallpaperProvider.createDrawable(); + fullBlurDrawable.setCallback(blurDrawableCallback); + fullBlurDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom()); + if (isAttachedToWindow() && fullBlurDrawable != null) + fullBlurDrawable.startListening(); + } + + /** + * We only need to change left bound for hotseat blur layer. + */ + public void changeBlurBounds(float factor, boolean isReset) { + if (fullBlurDrawable != null) { + float alpha = Interpolators.DEACCEL_1_5.getInterpolation(factor); + if(isReset) { + factor = 1; + } + fullBlurDrawable.setBounds(getLeft(), + getTop(), + (int) ((getRight() - getLeft()) * factor), + getBottom() + ); + setBlurAlpha((int) (255 * alpha)); + } + } + + public void setBlurAlpha(int alpha) { + Log.d("LauncherRootView", "setBlurAlpha() called with: alpha = [" + alpha + "]"); + fullBlurDrawable.setAlpha(alpha); + } + + public int getBlurAlpha() { + return fullBlurDrawable.getAlpha(); + } + public interface WindowStateListener { void onWindowFocusChanged(boolean hasFocus); diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/OnAppsChangedCallback.java b/app/src/main/java/foundation/e/blisslauncher/features/test/OnAppsChangedCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..9c5433320f938895e921372c26633a3280ddb549 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/OnAppsChangedCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 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.features.test; + +import android.content.pm.ShortcutInfo; +import android.os.UserHandle; + +import java.util.List; + +public interface OnAppsChangedCallback { + void onPackageRemoved(String packageName, UserHandle user); + void onPackageAdded(String packageName, UserHandle user); + void onPackageChanged(String packageName, UserHandle user); + void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing); + void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing); + void onPackagesSuspended(String[] packageNames, UserHandle user); + void onPackagesUnsuspended(String[] packageNames, UserHandle user); + void onShortcutsChanged(String packageName, List shortcuts, + UserHandle user); +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/TestActivity.kt b/app/src/main/java/foundation/e/blisslauncher/features/test/TestActivity.kt index e946064eee1000bd5107ec7d302ca9b44bcafd1c..3d885c576cd2b37b1d2f1d5a7da007e98e7e4247 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/TestActivity.kt +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/TestActivity.kt @@ -1,29 +1,21 @@ package foundation.e.blisslauncher.features.test import android.Manifest -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.animation.ValueAnimator import android.app.ActivityOptions import android.app.AlertDialog -import android.app.usage.UsageStats +import android.app.WallpaperManager import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo -import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.ContextWrapper import android.content.Intent -import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps +import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Point -import android.graphics.Rect import android.location.LocationManager import android.net.Uri import android.os.Bundle @@ -33,107 +25,62 @@ import android.os.Process import android.os.StrictMode import android.os.StrictMode.VmPolicy import android.provider.Settings -import android.text.Editable -import android.text.TextWatcher import android.util.Log import android.view.ContextThemeWrapper -import android.view.KeyEvent import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View -import android.view.ViewGroup -import android.view.animation.AccelerateDecelerateInterpolator -import android.view.animation.LinearInterpolator -import android.view.inputmethod.EditorInfo +import android.view.View.OnAttachStateChangeListener import android.view.inputmethod.InputMethodManager import android.widget.GridLayout -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.RelativeLayout -import android.widget.SeekBar -import android.widget.TextView import android.widget.Toast -import androidx.core.view.isVisible -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager.widget.ViewPager -import com.jakewharton.rxbinding3.widget.textChanges +import androidx.core.app.ActivityCompat import foundation.e.blisslauncher.BlissLauncher import foundation.e.blisslauncher.R import foundation.e.blisslauncher.core.Preferences import foundation.e.blisslauncher.core.Utilities +import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider +import foundation.e.blisslauncher.core.broadcast.WallpaperChangeReceiver import foundation.e.blisslauncher.core.customviews.AbstractFloatingView import foundation.e.blisslauncher.core.customviews.BlissFrameLayout -import foundation.e.blisslauncher.core.customviews.BlissInput -import foundation.e.blisslauncher.core.customviews.InsettableFrameLayout import foundation.e.blisslauncher.core.customviews.LauncherPagedView import foundation.e.blisslauncher.core.customviews.RoundedWidgetView import foundation.e.blisslauncher.core.customviews.SquareFrameLayout import foundation.e.blisslauncher.core.customviews.WidgetHost -import foundation.e.blisslauncher.core.database.DatabaseManager import foundation.e.blisslauncher.core.database.model.ApplicationItem -import foundation.e.blisslauncher.core.database.model.FolderItem import foundation.e.blisslauncher.core.database.model.LauncherItem import foundation.e.blisslauncher.core.database.model.ShortcutItem -import foundation.e.blisslauncher.core.executors.AppExecutors -import foundation.e.blisslauncher.core.utils.AppUtils import foundation.e.blisslauncher.core.utils.Constants import foundation.e.blisslauncher.core.utils.IntSet import foundation.e.blisslauncher.core.utils.IntegerArray -import foundation.e.blisslauncher.core.utils.ListUtil import foundation.e.blisslauncher.core.utils.PackageUserKey -import foundation.e.blisslauncher.core.utils.UserHandle -import foundation.e.blisslauncher.features.folder.FolderPagerAdapter +import foundation.e.blisslauncher.features.launcher.AppsRepository import foundation.e.blisslauncher.features.launcher.Hotseat -import foundation.e.blisslauncher.features.launcher.SearchInputDisposableObserver import foundation.e.blisslauncher.features.notification.DotInfo import foundation.e.blisslauncher.features.notification.NotificationDataProvider import foundation.e.blisslauncher.features.notification.NotificationListener import foundation.e.blisslauncher.features.shortcuts.DeepShortcutManager import foundation.e.blisslauncher.features.shortcuts.ShortcutKey -import foundation.e.blisslauncher.features.suggestions.AutoCompleteAdapter import foundation.e.blisslauncher.features.suggestions.SearchSuggestionUtil -import foundation.e.blisslauncher.features.suggestions.SuggestionsResult import foundation.e.blisslauncher.features.test.LauncherState.* import foundation.e.blisslauncher.features.test.RotationHelper.REQUEST_NONE import foundation.e.blisslauncher.features.test.dragndrop.DragController import foundation.e.blisslauncher.features.test.dragndrop.DragLayer import foundation.e.blisslauncher.features.test.graphics.RotationMode -import foundation.e.blisslauncher.features.usagestats.AppUsageStats -import foundation.e.blisslauncher.features.weather.DeviceStatusService -import foundation.e.blisslauncher.features.weather.ForecastBuilder import foundation.e.blisslauncher.features.weather.WeatherPreferences -import foundation.e.blisslauncher.features.weather.WeatherSourceListenerService import foundation.e.blisslauncher.features.weather.WeatherUpdateService -import foundation.e.blisslauncher.features.widgets.WidgetManager -import foundation.e.blisslauncher.features.widgets.WidgetViewBuilder -import foundation.e.blisslauncher.features.widgets.WidgetsActivity -import foundation.e.blisslauncher.features.widgets.WidgetsRootView import foundation.e.blisslauncher.uioverrides.OverlayCallbackImpl import foundation.e.blisslauncher.uioverrides.UiFactory -import io.reactivex.Observable -import io.reactivex.ObservableSource import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.observers.DisposableObserver -import io.reactivex.schedulers.Schedulers -import java.lang.Exception import java.net.URISyntaxException import java.util.ArrayList -import java.util.Arrays -import java.util.Comparator -import java.util.Locale -import java.util.concurrent.TimeUnit import java.util.function.Predicate -import me.relex.circleindicator.CircleIndicator -class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionClickListener, - LauncherModel.Callbacks { +class TestActivity : BaseDraggingActivity(), LauncherModel.Callbacks { + private var mIsEditingName: Boolean = false private var mModel: LauncherModel? = null - private lateinit var widgetRootView: WidgetsRootView private lateinit var overlayCallbackImpl: OverlayCallbackImpl // Folder start scale @@ -164,9 +111,6 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli private lateinit var dragLayer: DragLayer private lateinit var workspace: LauncherPagedView private lateinit var hotseat: Hotseat - private lateinit var widgetPage: InsettableFrameLayout - - private lateinit var mSearchInput: BlissInput private val REQUEST_LOCATION_SOURCE_SETTING = 267 @@ -177,43 +121,13 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli // UI and state for the overview panel private lateinit var overviewPanel: View - private lateinit var folderContainer: RelativeLayout - private val mOnResumeCallbacks = ArrayList() - private lateinit var mAppWidgetManager: AppWidgetManager - private lateinit var mAppWidgetHost: WidgetHost - private lateinit var widgetContainer: LinearLayout - private var activeRoundedWidgetView: RoundedWidgetView? = null - - private lateinit var mWeatherPanel: View - private lateinit var mWeatherSetupTextView: View - private val allAppsDisplayed = false - private val forceRefreshSuggestedApps = false - - private var mUsageStats: List? = null - - private var enableLocationDialog: AlertDialog? = null - - private var currentAnimator: AnimatorSet? = null - - private var activeFolder: FolderItem? = null - private var activeFolderView: IconTextView? = null - private var activeFolderStartBounds = Rect() - - private var mFolderAppsViewPager: ViewPager? = null - private var mFolderTitleInput: BlissInput? = null + lateinit var mAppWidgetManager: AppWidgetManager + lateinit var mAppWidgetHost: WidgetHost private val mainHandler = Handler(Looper.getMainLooper()) - private val mWeatherReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (!intent.getBooleanExtra(WeatherUpdateService.EXTRA_UPDATE_CANCELLED, false)) { - updateWeatherPanel() - } - } - } - private lateinit var notificationDataProvider: NotificationDataProvider val mHandler = Handler() private val mHandleDeferredResume = Runnable { this.handleDeferredResume() } @@ -221,6 +135,8 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli private val TAG = "TestActivity" + private var wallpaperChangeReceiver: WallpaperChangeReceiver? = null + override fun onCreate(savedInstanceState: Bundle?) { if (DEBUG_STRICT_MODE) { StrictMode.setThreadPolicy( @@ -253,6 +169,8 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli mAppWidgetHost = BlissLauncher.getApplication(this).appWidgetHost initDeviceProfile(BlissLauncher.getApplication(this).invariantDeviceProfile) + val wm = getSystemService(WALLPAPER_SERVICE) as WallpaperManager + wm.suggestDesiredDimensions(mDeviceProfile.widthPx, mDeviceProfile.heightPx) dragController = DragController(this) rotationHelper = RotationHelper(this) mStateManager = LauncherStateManager(this) @@ -277,7 +195,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli systemUiController.updateUiState( SystemUiController.UI_STATE_BASE_WINDOW, - true + false ) rotationHelper.initialize() TraceHelper.endSection("Launcher-onCreate") @@ -293,9 +211,20 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } } }) + + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + requestPermissions( + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + STORAGE_PERMISSION_REQUEST_CODE + ) + } createOrUpdateIconGrid() overlayCallbackImpl = OverlayCallbackImpl(this) - setLauncherOverlay(overlayCallbackImpl) + // setLauncherOverlay(overlayCallbackImpl) } private fun askForNotificationIfFirstTime() { @@ -416,193 +345,45 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli workspace.initParentViews(dragLayer) overviewPanel = findViewById(R.id.overview_panel) hotseat = findViewById(R.id.hotseat) - folderContainer = findViewById(R.id.folder_window_container) - mFolderAppsViewPager = findViewById(R.id.folder_apps) - mFolderTitleInput = findViewById(R.id.folder_title) + launcherView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) dragLayer.setup(dragController, workspace) mCancelTouchController = UiFactory.enableLiveUIChanges(this) workspace.setup(dragController) - setupWidgetPage() - workspace.bindAndInitFirstScreen(null) dragController.addDragListener(workspace) dragController.addDropTarget(workspace) // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setMoveTarget(workspace) - } - - private fun setupWidgetPage() { - widgetPage = - layoutInflater.inflate(R.layout.widgets_page, rootView, false) as InsettableFrameLayout - rootView.addView(widgetPage) - - widgetContainer = widgetPage.findViewById(R.id.widget_container) - - widgetPage.visibility = View.VISIBLE - widgetPage.post { - widgetPage.translationX = -(widgetPage.measuredWidth * 1.00f) - } - widgetRootView = widgetPage.findViewById(R.id.widgets_scroll_container) - widgetPage.findViewById(R.id.used_apps_layout).clipToOutline = true - widgetPage.tag = "Widget page" - - // TODO: replace with app predictions - // Prepare app suggestions view - // [[BEGIN]] - widgetPage.findViewById(R.id.openUsageAccessSettings).setOnClickListener { - startActivity( - Intent( - Settings.ACTION_USAGE_ACCESS_SETTINGS - ) - ) - } - - // divided by 2 because of left and right padding. - val padding = (mDeviceProfile.availableWidthPx / 2 - Utilities.pxFromDp(8, this) - - (2 * - mDeviceProfile.cellWidthPx)).toInt() - widgetPage.findViewById(R.id.suggestedAppGrid).setPadding(padding, 0, padding, 0) - // [[END]] - - // Prepare search suggestion view - // [[BEGIN]] - mSearchInput = widgetPage.findViewById(R.id.search_input) - val clearSuggestions: ImageView = - widgetPage.findViewById(R.id.clearSuggestionImageView) - clearSuggestions.setOnClickListener { - mSearchInput.setText("") - mSearchInput.clearFocus() - } - - mSearchInput.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.toString().trim { it <= ' ' }.isEmpty()) { - clearSuggestions.visibility = View.GONE - } else { - clearSuggestions.visibility = View.VISIBLE - } - } - - override fun afterTextChanged(s: Editable) {} - }) - val suggestionRecyclerView: RecyclerView = - widgetPage.findViewById(R.id.suggestionRecyclerView) - val suggestionAdapter = AutoCompleteAdapter(this) - suggestionRecyclerView.setHasFixedSize(true) - suggestionRecyclerView.layoutManager = - LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - suggestionRecyclerView.adapter = suggestionAdapter - val dividerItemDecoration = DividerItemDecoration( - this, - DividerItemDecoration.VERTICAL - ) - suggestionRecyclerView.addItemDecoration(dividerItemDecoration) - getCompositeDisposable().add( - mSearchInput.textChanges() - .debounce(300, TimeUnit.MILLISECONDS) - .map { obj: CharSequence -> obj.toString() } - .distinctUntilChanged() - .switchMap { charSequence: String? -> - if (charSequence != null && charSequence.isNotEmpty()) { - searchForQuery(charSequence) - } else { - Observable.just( - SuggestionsResult(charSequence) - ) - } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith( - SearchInputDisposableObserver(this, suggestionAdapter, widgetPage) - ) - ) - mSearchInput.onFocusChangeListener = - View.OnFocusChangeListener { v: View, hasFocus: Boolean -> - if (!hasFocus) { - hideKeyboard(v) - } + wallpaperChangeReceiver = WallpaperChangeReceiver(workspace) + workspace.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + wallpaperChangeReceiver?.setWindowToken(v.windowToken) } - mSearchInput.setOnEditorActionListener { _: TextView?, action: Int, _: KeyEvent? -> - if (action == EditorInfo.IME_ACTION_SEARCH) { - hideKeyboard(mSearchInput) - runSearch(mSearchInput.text.toString()) - mSearchInput.setText("") - mSearchInput.clearFocus() - return@setOnEditorActionListener true - } - false - } - // [[END]] - - // Prepare edit widgets button - findViewById(R.id.edit_widgets_button).setOnClickListener { view: View? -> - startActivity( - Intent( - this, - WidgetsActivity::class.java - ) - ) - } - - // Prepare weather widget view - // [[BEGIN]] - findViewById(R.id.weather_setting_imageview).setOnClickListener { v: View? -> - startActivity( - Intent( - this, - WeatherPreferences::class.java - ) - ) - } - - mWeatherSetupTextView = findViewById(R.id.weather_setup_textview) - mWeatherPanel = findViewById(R.id.weather_panel) - mWeatherPanel.setOnClickListener(View.OnClickListener { v: View? -> - val launchIntent = - packageManager.getLaunchIntentForPackage( - "foundation.e.weather" - ) - if (launchIntent != null) { - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(launchIntent) + override fun onViewDetachedFromWindow(v: View) { + wallpaperChangeReceiver?.setWindowToken(null) } }) - updateWeatherPanel() - - if (foundation.e.blisslauncher.features.weather.WeatherUtils.isWeatherServiceAvailable( - this - ) - ) { - startService(Intent(this, WeatherSourceListenerService::class.java)) - startService(Intent(this, DeviceStatusService::class.java)) - } - - LocalBroadcastManager.getInstance(this).registerReceiver( - mWeatherReceiver, IntentFilter( - WeatherUpdateService.ACTION_UPDATE_FINISHED - ) - ) + } - if (!Preferences.useCustomWeatherLocation(this)) { - if (!WeatherPreferences.hasLocationPermission(this)) { - val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) - requestPermissions( - permissions, - WeatherPreferences.LOCATION_PERMISSION_REQUEST_CODE - ) - } else { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == WeatherPreferences.LOCATION_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + // We only get here if user tried to enable the preference, + // hence safe to turn it on after permission is granted val lm = getSystemService(LOCATION_SERVICE) as LocationManager - if (!lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) && - Preferences.getEnableLocation(this) - ) { - showLocationEnableDialog() + if (!lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + workspace.showLocationEnableDialog() Preferences.setEnableLocation(this) } else { startService( @@ -611,85 +392,13 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli ) } } - } else { - startService( - Intent(this, WeatherUpdateService::class.java) - .setAction(WeatherUpdateService.ACTION_FORCE_UPDATE) - ) - } - // [[END]] - - val widgetIds: IntArray = mAppWidgetHost.appWidgetIds - Arrays.sort(widgetIds) - for (id in widgetIds) { - val appWidgetInfo: AppWidgetProviderInfo? = mAppWidgetManager.getAppWidgetInfo(id) - if (appWidgetInfo != null) { - val hostView: RoundedWidgetView = mAppWidgetHost.createView( - applicationContext, id, - appWidgetInfo - ) as RoundedWidgetView - hostView.setAppWidget(id, appWidgetInfo) - getCompositeDisposable().add(DatabaseManager.getManager(this).getHeightOfWidget(id) - .subscribeOn(Schedulers.from(AppExecutors.getInstance().diskIO())) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ height: Int -> - val widgetView = WidgetViewBuilder.create(this, hostView) - if (height != 0) { - val minHeight = hostView.appWidgetInfo.minResizeHeight - val maxHeight = mDeviceProfile.availableHeightPx * 3 / 4 - val normalisedDifference = (maxHeight - minHeight) / 100 - val newHeight = minHeight + normalisedDifference * height - widgetView.layoutParams.height = newHeight - } - addWidgetToContainer(widgetView) - }) { obj: Throwable -> obj.printStackTrace() }) + } else if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + BlurWallpaperProvider.getInstance(applicationContext).updateAsync() } - } - } - - private fun addWidgetToContainer(widgetView: RoundedWidgetView) { - widgetView.setPadding(0, 0, 0, 0) - widgetContainer.addView(widgetView) - } - - private fun updateWeatherPanel() { - if (Preferences.getCachedWeatherInfo(this) == null) { - mWeatherSetupTextView.visibility = View.VISIBLE - mWeatherPanel.visibility = View.GONE - mWeatherSetupTextView.setOnClickListener { v: View? -> - startActivity( - Intent(this, WeatherPreferences::class.java) - ) - } - return - } - mWeatherSetupTextView.visibility = View.GONE - mWeatherPanel.visibility = View.VISIBLE - ForecastBuilder.buildLargePanel( - this, mWeatherPanel, - Preferences.getCachedWeatherInfo(this) - ) - } - - private fun showLocationEnableDialog() { - val builder = AlertDialog.Builder(this) - // Build and show the dialog - builder.setTitle(R.string.weather_retrieve_location_dialog_title) - builder.setMessage(R.string.weather_retrieve_location_dialog_message) - builder.setCancelable(false) - builder.setPositiveButton( - R.string.weather_retrieve_location_dialog_enable_button - ) { dialog1, whichButton -> - val intent = - Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivityForResult( - intent, REQUEST_LOCATION_SOURCE_SETTING - ) - } - builder.setNegativeButton(R.string.cancel, null) - enableLocationDialog = builder.create() - enableLocationDialog?.show() + } else super.onRequestPermissionsResult(requestCode, permissions, grantResults) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -711,123 +420,6 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } } - private fun searchForQuery( - charSequence: CharSequence - ): ObservableSource? { - val launcherItems = searchForLauncherItems( - charSequence.toString() - ).subscribeOn(Schedulers.io()) - val networkItems = searchForNetworkItems( - charSequence - ).subscribeOn(Schedulers.io()) - return launcherItems.mergeWith(networkItems) - } - - private fun searchForLauncherItems( - charSequence: CharSequence - ): Observable { - val query = charSequence.toString().toLowerCase() - val suggestionsResult = SuggestionsResult( - query - ) - val launcherItems: MutableList = ArrayList() - workspace.mWorkspaceScreens.forEach { gridLayout: GridLayout -> - for (i in 0 until gridLayout.childCount) { - val blissFrameLayout = gridLayout.getChildAt(i) as BlissFrameLayout - val launcherItem = blissFrameLayout.launcherItem - if (launcherItem.itemType == Constants.ITEM_TYPE_FOLDER) { - val folderItem = launcherItem as FolderItem - for (item in folderItem.items) { - if (item.title.toString().toLowerCase().contains(query)) { - launcherItems.add(item) - } - } - } else if (launcherItem.title.toString().toLowerCase().contains(query)) { - launcherItems.add(launcherItem) - } - } - } - val hotseat = getHotseat() - for (i in 0 until hotseat.childCount) { - val blissFrameLayout = hotseat.getChildAt(i) as BlissFrameLayout - val launcherItem = blissFrameLayout.launcherItem - if (launcherItem.itemType == Constants.ITEM_TYPE_FOLDER) { - val folderItem = launcherItem as FolderItem - for (item in folderItem.items) { - if (item.title.toString().toLowerCase().contains(query)) { - launcherItems.add(item) - } - } - } else if (launcherItem.title.toString().toLowerCase().contains(query)) { - launcherItems.add(launcherItem) - } - } - launcherItems.sortWith(Comparator.comparing { launcherItem: LauncherItem -> - launcherItem.title.toString().toLowerCase().indexOf(query) - }) - if (launcherItems.size > 4) { - suggestionsResult.launcherItems = launcherItems.subList(0, 4) - } else { - suggestionsResult.launcherItems = launcherItems - } - return Observable.just(suggestionsResult) - .onErrorReturn { throwable: Throwable? -> - suggestionsResult.launcherItems = ArrayList() - suggestionsResult - } - } - - private fun searchForNetworkItems(charSequence: CharSequence): Observable { - val query = charSequence.toString().lowercase(Locale.getDefault()).trim { it <= ' ' } - val suggestionProvider = SearchSuggestionUtil().getSuggestionProvider( - this - ) - return suggestionProvider.query(query).toObservable() - } - - fun hideWidgetResizeContainer() { - val widgetResizeContainer: RelativeLayout = widgetPage.findViewById( - R.id.widget_resizer_container - ) - if (widgetResizeContainer.visibility == View.VISIBLE) { - currentAnimator?.cancel() - val set = AnimatorSet() - set.play( - ObjectAnimator.ofFloat( - widgetResizeContainer, View.Y, - mDeviceProfile.availableHeightPx - .toFloat() - ) - ) - set.duration = 200 - set.interpolator = LinearInterpolator() - set.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - super.onAnimationStart(animation) - (widgetPage.findViewById( - R.id.widget_resizer_seekbar - ) as SeekBar).setOnSeekBarChangeListener(null) - } - - override fun onAnimationCancel(animation: Animator) { - super.onAnimationCancel(animation) - currentAnimator = null - widgetResizeContainer.visibility = View.VISIBLE - } - - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - currentAnimator = null - widgetResizeContainer.visibility = View.GONE - activeRoundedWidgetView?.removeBorder() - } - } - ) - set.start() - currentAnimator = set - } - } - fun hideKeyboard(view: View) { val inputMethodManager = (getSystemService( INPUT_METHOD_SERVICE @@ -842,7 +434,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli inputMethodManager.showSoftInput(view, 0) } - private fun runSearch(query: String) { + fun runSearch(query: String) { val intent = Intent( Intent.ACTION_VIEW, SearchSuggestionUtil().getUriForQuery(this, query) @@ -851,92 +443,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } fun showWidgetResizeContainer(roundedWidgetView: RoundedWidgetView) { - val widgetResizeContainer: RelativeLayout = widgetPage.findViewById( - R.id.widget_resizer_container - ) - if (widgetResizeContainer.visibility != View.VISIBLE) { - activeRoundedWidgetView = roundedWidgetView - val seekBar = widgetResizeContainer.findViewById(R.id.widget_resizer_seekbar) - if (currentAnimator != null) { - currentAnimator!!.cancel() - } - seekBar.setOnTouchListener { v: View?, event: MotionEvent? -> - seekBar.parent.requestDisallowInterceptTouchEvent(true) - false - } - val set = AnimatorSet() - set.play( - ObjectAnimator.ofFloat( - widgetResizeContainer, View.Y, - mDeviceProfile.availableHeightPx.toFloat(), - mDeviceProfile.availableHeightPx - Utilities.pxFromDp(48, this) - ) - ) - set.duration = 200 - set.interpolator = LinearInterpolator() - set.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - super.onAnimationStart(animation) - widgetResizeContainer.visibility = View.VISIBLE - } - - override fun onAnimationCancel(animation: Animator) { - super.onAnimationCancel(animation) - currentAnimator = null - widgetResizeContainer.visibility = View.GONE - roundedWidgetView.removeBorder() - } - - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - currentAnimator = null - prepareWidgetResizeSeekBar(seekBar) - roundedWidgetView.addBorder() - } - } - ) - set.start() - currentAnimator = set - } - } - - private fun prepareWidgetResizeSeekBar(seekBar: SeekBar) { - val minHeight = activeRoundedWidgetView!!.appWidgetInfo.minResizeHeight - val maxHeight = mDeviceProfile.availableHeightPx * 3 / 4 - val normalisedDifference = (maxHeight - minHeight) / 100 - val defaultHeight = activeRoundedWidgetView!!.height - val currentProgress = (defaultHeight - minHeight) * 100 / (maxHeight - minHeight) - seekBar.max = 100 - seekBar.progress = currentProgress - seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - val newHeight = minHeight + normalisedDifference * progress - val layoutParams = - activeRoundedWidgetView!!.layoutParams as LinearLayout.LayoutParams - layoutParams.height = newHeight - activeRoundedWidgetView!!.layoutParams = layoutParams - val newOps = Bundle() - newOps.putInt( - AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, - mDeviceProfile.maxWidgetWidth - ) - newOps.putInt( - AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - mDeviceProfile.maxWidgetWidth - ) - newOps.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newHeight) - newOps.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newHeight) - activeRoundedWidgetView!!.updateAppWidgetOptions(newOps) - activeRoundedWidgetView!!.requestLayout() - } - - override fun onStartTrackingTouch(seekBar: SeekBar) {} - override fun onStopTrackingTouch(seekBar: SeekBar) { - DatabaseManager.getManager(this@TestActivity).saveWidget( - activeRoundedWidgetView!!.appWidgetId, seekBar.progress - ) - } - }) + workspace.showWidgetResizeContainer(roundedWidgetView) } override fun findViewById(id: Int): T { @@ -962,6 +469,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli super.onPause() dragController.cancelDrag() dragController.resetLastGestureUpTime() + workspace.hideWidgetResizeContainer() } override fun onResume() { @@ -978,37 +486,9 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli resumeCallbacks.clear() } - if (mWeatherPanel != null) { - updateWeatherPanel() - } - - if (widgetPage != null) { - refreshSuggestedApps(widgetPage, forceRefreshSuggestedApps) - } - - if (widgetContainer != null) { - val widgetManager = WidgetManager.getInstance() - var id = widgetManager.dequeRemoveId() - while (id != null) { - for (i in 0 until widgetContainer.childCount) { - if (widgetContainer.getChildAt(i) is RoundedWidgetView) { - val appWidgetHostView = widgetContainer.getChildAt(i) as RoundedWidgetView - if (appWidgetHostView.appWidgetId == id) { - widgetContainer.removeViewAt(i) - DatabaseManager.getManager(this).removeWidget(id) - break - } - } - } - id = widgetManager.dequeRemoveId() - } - var widgetView = widgetManager.dequeAddWidgetView() - while (widgetView != null) { - widgetView = WidgetViewBuilder.create(this, widgetView) - addWidgetToContainer(widgetView) - widgetView = widgetManager.dequeAddWidgetView() - } - } + workspace.updateWeatherPanel() + workspace.refreshSuggestedApps(false) // TODO: Update with app remove event + workspace.updateWidgets() } override fun onStop() { @@ -1021,6 +501,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli override fun onDestroy() { super.onDestroy() + workspace.removeFolderListeners() if (mCancelTouchController != null) { mCancelTouchController!!.run() mCancelTouchController = null @@ -1070,7 +551,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli fun isWorkspaceLocked() = false - private fun getCompositeDisposable(): CompositeDisposable { + fun getCompositeDisposable(): CompositeDisposable { if (mCompositeDisposable == null || mCompositeDisposable!!.isDisposed) { mCompositeDisposable = CompositeDisposable() } @@ -1082,12 +563,12 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli BlissLauncher.getApplication(this) .appProvider .appsRepository - .appsRelay + .allItemsRelay .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableObserver>() { - override fun onNext(launcherItems: List) { - mainHandler.post { showApps(launcherItems) } + .subscribeWith(object : DisposableObserver() { + override fun onNext(items: AppsRepository.AllItems) { + mainHandler.post { showApps(items) } } override fun onError(e: Throwable) { @@ -1099,14 +580,16 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli ) } - private fun showApps(launcherItems: List) { - Log.d(TAG, "showApps() called with: launcherItems = $launcherItems") + private fun showApps(allItems: AppsRepository.AllItems) { hotseat.resetLayout(false) - val populatedItems = populateItemPositions(launcherItems) + val populatedItems = populateItemPositions(allItems.items) val orderedScreenIds = IntegerArray() orderedScreenIds.addAll(collectWorkspaceScreens(populatedItems)) + workspace.removeAllWorkspaceScreens() workspace.bindScreens(orderedScreenIds) + workspace.post(workspace::moveToDefaultScreen) workspace.bindItems(populatedItems, false) + workspace.bindItemsAdded(allItems.newAddedItems) } private fun populateItemPositions(launcherItems: List): List { @@ -1165,11 +648,11 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli /** * Call this after onCreate to set or clear overlay. */ - private fun setLauncherOverlay(overlay: LauncherOverlay) { + /*private fun setLauncherOverlay(overlay: LauncherOverlay) { overlay.setOverlayCallbacks(LauncherOverlayCallbacksImpl()) workspace.setLauncherOverlay(overlay) widgetRootView.setLauncherOverlay(overlay) - } + }*/ fun isInState(state: LauncherState): Boolean { return mStateManager.state === state @@ -1179,45 +662,6 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli mOnResumeCallbacks.add(callback) } - fun refreshSuggestedApps(viewGroup: ViewGroup, forceRefresh: Boolean) { - val openUsageAccessSettingsTv = - viewGroup.findViewById(R.id.openUsageAccessSettings) - val suggestedAppsGridLayout = viewGroup.findViewById(R.id.suggestedAppGrid) - val appUsageStats = AppUsageStats(this) - val usageStats = appUsageStats.usageStats - if (usageStats.size > 0) { - openUsageAccessSettingsTv.visibility = View.GONE - suggestedAppsGridLayout.visibility = View.VISIBLE - - // Check if usage stats have been changed or not to avoid unnecessary flickering - if (forceRefresh || mUsageStats == null || mUsageStats!!.size != usageStats.size || !ListUtil.areEqualLists( - mUsageStats, - usageStats - ) - ) { - mUsageStats = usageStats - if (suggestedAppsGridLayout.childCount > 0) { - suggestedAppsGridLayout.removeAllViews() - } - var i = 0 - while (suggestedAppsGridLayout.childCount < 4 && i < mUsageStats!!.size) { - val appItem = AppUtils.createAppItem( - this, - mUsageStats!![i].packageName, UserHandle() - ) - if (appItem != null) { - val view: BlissFrameLayout = prepareSuggestedApp(appItem) - addAppToGrid(suggestedAppsGridLayout, view) - } - i++ - } - } - } else { - openUsageAccessSettingsTv.visibility = View.VISIBLE - suggestedAppsGridLayout.visibility = View.GONE - } - } - fun prepareSuggestedApp(launcherItem: LauncherItem): BlissFrameLayout { val v = layoutInflater.inflate( R.layout.app_view, @@ -1239,8 +683,17 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli val rowSpec = GridLayout.spec(GridLayout.UNDEFINED) val colSpec = GridLayout.spec(GridLayout.UNDEFINED) val iconLayoutParams = GridLayout.LayoutParams(rowSpec, colSpec) - iconLayoutParams.height = mDeviceProfile.cellHeightPx - iconLayoutParams.width = mDeviceProfile.cellWidthPx + val emptySpace = mDeviceProfile.availableWidthPx - 2 * Utilities.pxFromDp(16, this) - 4 * + mDeviceProfile.cellWidthPx + val padding = emptySpace / 10 + val topBottomPadding = Utilities.pxFromDp(8, this).toInt() + iconLayoutParams.height = (mDeviceProfile.cellHeightPx + topBottomPadding) + iconLayoutParams.width = (mDeviceProfile.cellWidthPx + padding * 2).toInt() + + view.setPadding( + padding.toInt(), + (topBottomPadding / 2), padding.toInt(), topBottomPadding / 2 + ) view.findViewById(R.id.app_label).visibility = View.VISIBLE view.layoutParams = iconLayoutParams view.setWithText(true) @@ -1257,7 +710,8 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli // Check this condition before handling isActionMain, as this will get reset. val shouldMoveToDefaultScreen = (alreadyOnHome && isInState(NORMAL) && - AbstractFloatingView.getTopOpenView(this) == null) + AbstractFloatingView.getTopOpenView(this) == null) && + ((workspace.activeRoundedWidgetView?.isWidgetActivated ?: false).not()) val isActionMain = Intent.ACTION_MAIN == intent!!.action val internalStateHandled = InternalStateHandler .handleNewIntent(this, intent, isStarted) @@ -1266,11 +720,6 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli if (!internalStateHandled) { // In all these cases, only animate if we're already on home AbstractFloatingView.closeAllOpenViews(this, isStarted) - - if (folderContainer.visibility == View.VISIBLE) { - closeFolder() - return - } if (!isInState(NORMAL)) { // Only change state, if not already the same. This prevents cancelling any // animations running as part of resume @@ -1280,6 +729,8 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli // Reset the apps view if (!alreadyOnHome) { // TODO: maybe stop icon giggling or search view here. + } else { + workspace.hideWidgetResizeContainer() } if (shouldMoveToDefaultScreen && !workspace.isHandlingTouch) { workspace.post(workspace::moveToDefaultScreen) @@ -1310,14 +761,13 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli return } + workspace.clearWidgetState() + // Note: There should be at most one log per method call. This is enforced implicitly // by using if-else statements. val topView = AbstractFloatingView.getTopOpenView(this) if (topView != null && topView.onBackPressed()) { - // Handled by the floating view. - } else if (folderContainer.visibility == View.VISIBLE) { - closeFolder() } else { mStateManager.state.onBackPressed(this) } @@ -1334,6 +784,8 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli const val DEBUG_STRICT_MODE = false + private val STORAGE_PERMISSION_REQUEST_CODE: Int = 586 + // TODO: Remove after test is finished fun getLauncher(context: Context): TestActivity { return if (context is TestActivity) { @@ -1380,7 +832,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } // TODO: Maybe we need to simplify the logic here. - inner class LauncherOverlayCallbacksImpl : LauncherOverlayCallbacks { + /*inner class LauncherOverlayCallbacksImpl : LauncherOverlayCallbacks { private var currentProgress = 0f private var isScrolling = false @@ -1393,8 +845,11 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli if (scrollFromWorkspace) { workspace.onOverlayScrollChanged(progress) widgetPage.translationX = widgetPage.measuredWidth * (progress - 1) + widgetPage.changeBlurBounds(progress, true) } else { workspace.onOverlayScrollChanged(progress) + Log.i(TAG, "onScrollChanged: $progress") + widgetPage.changeBlurBounds(progress, false) } } } @@ -1411,6 +866,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli workspace.onOverlayScrollChanged(it.animatedValue as Float) widgetPage.translationX = widgetPage.measuredWidth * (it.animatedValue as Float - 1) + widgetPage.changeBlurBounds(it.animatedValue as Float, true) } workspaceAnim.duration = 300 workspaceAnim.interpolator = AccelerateDecelerateInterpolator() @@ -1422,6 +878,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli override fun onAnimationCancel(animation: Animator) { workspace.onOverlayScrollChanged(0f) widgetPage.translationX = (-widgetPage.measuredWidth).toFloat() + widgetPage.changeBlurBounds(0f, true) animator = null } }) @@ -1432,6 +889,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli workspace.onOverlayScrollChanged(it.animatedValue as Float) widgetPage.translationX = widgetPage.measuredWidth * (it.animatedValue as Float - 1) + widgetPage.changeBlurBounds(it.animatedValue as Float, false) } workspaceAnim.duration = 300 workspaceAnim.interpolator = AccelerateDecelerateInterpolator() @@ -1443,6 +901,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli override fun onAnimationCancel(animation: Animator) { workspace.onOverlayScrollChanged(1f) widgetPage.translationX = 0f + widgetPage.changeBlurBounds(1f, false) animator = null } }) @@ -1450,14 +909,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } animator?.start() } - } - - override fun onClick(suggestion: String) { - mSearchInput.setText(suggestion) - runSearch(suggestion) - mSearchInput.clearFocus() - mSearchInput.setText("") - } + }*/ fun updateNotificationDots(updatedDots: Predicate) { workspace.updateNotificationBadge(updatedDots) @@ -1564,7 +1016,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } } - fun openFolder(folderView: View) { + /*fun openFolder(folderView: View) { if (currentAnimator != null) { currentAnimator!!.cancel() } @@ -1613,12 +1065,13 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli // Construct and run the parallel animation of the four translation and // scale properties (X, Y, SCALE_X, and SCALE_Y). val set = AnimatorSet() - /*ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 18); + *//*ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 18); valueAnimator.addUpdateListener(animation -> - BlurWallpaperProvider.getInstance(this).blur((Integer) animation.getAnimatedValue()));*/ - /*ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 18); + BlurWallpaperProvider.getInstance(this).blur((Integer) animation.getAnimatedValue()));*//* + *//*ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 18); valueAnimator.addUpdateListener(animation -> - BlurWallpaperProvider.getInstance(this).blur((Integer) animation.getAnimatedValue()));*/set.play( + BlurWallpaperProvider.getInstance(this).blur((Integer) animation.getAnimatedValue()));*//* + set.play( ObjectAnimator.ofFloat( folderContainer, View.X, startBounds.left.toFloat(), finalBounds.left @@ -1646,7 +1099,7 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli ) .with(ObjectAnimator.ofFloat(getLauncherPagedView(), View.ALPHA, 0f)) // .with(ObjectAnimator.ofFloat(mIndicator, View.ALPHA, 0f)) - .with(ObjectAnimator.ofFloat(hotseat, View.ALPHA, 0f)) + .with() set.duration = 300 set.interpolator = LinearInterpolator() set.addListener(object : AnimatorListenerAdapter() { @@ -1702,19 +1155,22 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli } fun closeFolder() { - mFolderTitleInput?.clearFocus() + if (isEditingName()) { + mFolderTitleInput.dispatchBackKey() + } + currentAnimator?.cancel() // Animate the four positioning/sizing properties in parallel, // back to their original values. val set = AnimatorSet() - /*ValueAnimator valueAnimator = ValueAnimator.ofInt(18, 0); + *//*ValueAnimator valueAnimator = ValueAnimator.ofInt(18, 0); + valueAnimator.addUpdateListener(animati on -> + BlurWallpaperProvider.getInstance(this).blurWithLauncherView(mergedView, (Integer) animation.getAnimatedValue()));*//* + *//*ValueAnimator valueAnimator = ValueAnimator.ofInt(18, 0); valueAnimator.addUpdateListener(animation -> - BlurWallpaperProvider.getInstance(this).blurWithLauncherView(mergedView, (Integer) animation.getAnimatedValue()));*/ - /*ValueAnimator valueAnimator = ValueAnimator.ofInt(18, 0); - valueAnimator.addUpdateListener(animation -> - BlurWallpaperProvider.getInstance(this).blurWithLauncherView(mergedView, (Integer) animation.getAnimatedValue()));*/set.play( + BlurWallpaperProvider.getInstance(this).blurWithLauncherView(mergedView, (Integer) animation.getAnimatedValue()));*//*set.play( ObjectAnimator .ofFloat(folderContainer, View.X, activeFolderStartBounds.left.toFloat()) ) @@ -1774,11 +1230,22 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli }) set.start() currentAnimator = set - } + }*/ override fun bindAppsAdded(items: MutableList) { if (items.isEmpty()) return + workspace.bindItemsAdded(items) + } - workspace?.bindItemsAdded(items) + /** + * A package was uninstalled/updated. We take both the super set of packageNames + * in addition to specific applications to remove, the reason being that + * this can be called when a package is updated as well. In that scenario, + * we only remove specific components from the workspace and hotseat, where as + * package-removal should clear all items by package name. + */ + override fun bindWorkspaceComponentsRemoved(matcher: LauncherItemMatcher) { + workspace.removeItemsByMatcher(matcher) + dragController.onAppsRemoved(matcher) } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/VariantDeviceProfile.kt b/app/src/main/java/foundation/e/blisslauncher/features/test/VariantDeviceProfile.kt index 9939c758d803466d0858aa56f4814e079ae271f9..f613b15b1483687c287b9e4d30911a2f96fccb16 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/VariantDeviceProfile.kt +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/VariantDeviceProfile.kt @@ -29,11 +29,12 @@ import android.view.WindowInsets import android.view.WindowManager import foundation.e.blisslauncher.R import foundation.e.blisslauncher.core.Utilities -import foundation.e.blisslauncher.core.utils.Constants +import foundation.e.blisslauncher.core.utils.ResourceUtils import foundation.e.blisslauncher.features.notification.DotRenderer import foundation.e.blisslauncher.features.test.uninstall.UninstallButtonRenderer import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt class VariantDeviceProfile( val context: Context, @@ -46,7 +47,6 @@ class VariantDeviceProfile( @JvmField var isMultiWindowMode: Boolean ) { - val mDotRenderer: DotRenderer val uninstallRenderer: UninstallButtonRenderer @@ -64,7 +64,7 @@ class VariantDeviceProfile( // Workspace val desiredWorkspaceLeftRightMarginPx: Int val cellLayoutPaddingLeftRightPx: Int - val cellLayoutBottomPaddingPx: Int + var cellLayoutBottomPaddingPx: Int val edgeMarginPx: Int val defaultWidgetPadding: Rect val defaultPageSpacingPx: Int @@ -78,7 +78,7 @@ class VariantDeviceProfile( var cellWidthPx = 0 var cellHeightPx = 0 var workspaceCellPaddingXPx: Int - var workspacePageIndicatorHeight: Int + val workspacePageIndicatorHeight: Int // Folder var folderIconSizePx = 0 @@ -211,8 +211,11 @@ class VariantDeviceProfile( 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 + hotseatBarSizePx = ResourceUtils.pxFromDp( + inv.iconSize, + dm + ) + (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size) + + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx) workspacePageIndicatorHeight = res.getDimensionPixelSize(R.dimen.dotSize) * 2 + res.getDimensionPixelSize(R.dimen.dotPadding) * 2 @@ -409,7 +412,7 @@ class VariantDeviceProfile( } /** - * Updates [.workspacePadding] as a result of any internal value change to reflect the + * Updates [workspacePadding] as a result of any internal value change to reflect the * new workspace padding */ private fun updateWorkspacePadding() { @@ -448,11 +451,12 @@ class VariantDeviceProfile( val workspaceCellWidth = widthPx.toFloat() / inv.numColumns val hotseatCellWidth = widthPx.toFloat() / inv.numHotseatIcons val hotseatAdjustment = - Math.round((workspaceCellWidth - hotseatCellWidth) / 2) + ((workspaceCellWidth - hotseatCellWidth) / 2).roundToInt() - mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, hotseatBarTopPaddingPx, hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = + mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, + hotseatBarTopPaddingPx, + hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = hotseatBarBottomPaddingPx + insets.bottom + cellLayoutBottomPaddingPx - Log.d(TAG, "Hotseat padding: $mHotseatPadding, insets: $insets") 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 @@ -467,10 +471,10 @@ class VariantDeviceProfile( insets.top + availableHeightPx - hotseatBarSizePx - -edgeMarginPx ) - fun getCellHeight(containerType: Long): Int { + fun getCellHeight(containerType: Int): Int { return when (containerType) { - Constants.CONTAINER_DESKTOP -> cellHeightPx - Constants.CONTAINER_HOTSEAT -> hotseatCellHeightPx + CellLayout.WORKSPACE -> cellHeightPx + CellLayout.HOTSEAT -> hotseatCellHeightPx else -> 0 } } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/WorkspaceStateTransitionAnimation.java b/app/src/main/java/foundation/e/blisslauncher/features/test/WorkspaceStateTransitionAnimation.java index 9e7729346c92945b55d83ffa5e1654f96f059298..c10c768378d29d73047c4ea97dd4858f651c7db8 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/WorkspaceStateTransitionAnimation.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/WorkspaceStateTransitionAnimation.java @@ -76,7 +76,7 @@ public class WorkspaceStateTransitionAnimation { LauncherState.PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher); final int childCount = mWorkspace.getChildCount(); for (int i = 0; i < childCount; i++) { - applyChildState(state, (CellLayout) mWorkspace.getChildAt(i), i, pageAlphaProvider, + applyChildState(state, mWorkspace.getChildAt(i), i, pageAlphaProvider, propertySetter, builder, config); } @@ -144,7 +144,7 @@ public class WorkspaceStateTransitionAnimation { NO_ANIM_PROPERTY_SETTER, new AnimatorSetBuilder(), new LauncherStateManager.AnimationConfig()); } - private void applyChildState(LauncherState state, CellLayout cl, int childIndex, + private void applyChildState(LauncherState state, View cl, int childIndex, LauncherState.PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter, AnimatorSetBuilder builder, LauncherStateManager.AnimationConfig config) { float pageAlpha = pageAlphaProvider.getPageAlpha(childIndex); diff --git a/app/src/main/java/foundation/e/blisslauncher/features/test/dragndrop/DragController.java b/app/src/main/java/foundation/e/blisslauncher/features/test/dragndrop/DragController.java index 0535824a672f2f418c9e64e2ddb2b9eaead70a07..298f7dc073e5ab2f6a40603d781022dc4b77473b 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/test/dragndrop/DragController.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/test/dragndrop/DragController.java @@ -16,6 +16,9 @@ package foundation.e.blisslauncher.features.test.dragndrop; +import static foundation.e.blisslauncher.features.test.LauncherState.NORMAL; +import static foundation.e.blisslauncher.features.test.anim.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; + import android.content.ComponentName; import android.content.res.Resources; import android.graphics.Bitmap; @@ -28,9 +31,6 @@ import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; - -import java.util.ArrayList; - import foundation.e.blisslauncher.R; import foundation.e.blisslauncher.core.database.model.ApplicationItem; import foundation.e.blisslauncher.core.database.model.LauncherItem; @@ -39,6 +39,7 @@ import foundation.e.blisslauncher.features.test.LauncherItemMatcher; import foundation.e.blisslauncher.features.test.TestActivity; import foundation.e.blisslauncher.features.test.TouchController; import foundation.e.blisslauncher.features.test.UiThreadHelper; +import java.util.ArrayList; /** * Class for initiating a drag within a view or across multiple views. @@ -251,8 +252,7 @@ public class DragController implements DragDriver.EventListener, TouchController if (!accepted) { // If it was not accepted, cleanup the state. If it was accepted, it is the // responsibility of the drop target to cleanup the state. - // mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); - //TODO: Go to normal state here. + mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); mDragObject.deferDragViewCleanupPostAnimation = false; } diff --git a/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetPageLayer.kt b/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetPageLayer.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1bd897c57b880e57000e18339f91cf65a73e720 --- /dev/null +++ b/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetPageLayer.kt @@ -0,0 +1,107 @@ +package foundation.e.blisslauncher.features.widgets + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider +import foundation.e.blisslauncher.core.blur.ShaderBlurDrawable +import foundation.e.blisslauncher.core.customviews.Insettable +import foundation.e.blisslauncher.core.customviews.InsettableFrameLayout +import foundation.e.blisslauncher.core.runOnMainThread + +class WidgetPageLayer @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : InsettableFrameLayout(context, attrs), Insettable, + BlurWallpaperProvider.Listener { + private val blurWallpaperProvider: BlurWallpaperProvider + private var fullBlurDrawable: ShaderBlurDrawable? = null + private val blurAlpha = 255 + private val blurDrawableCallback: Drawable.Callback = object : Drawable.Callback { + override fun invalidateDrawable(who: Drawable) { + runOnMainThread { + invalidate() + } + } + + override fun scheduleDrawable( + who: Drawable, + what: Runnable, + `when`: Long + ) { + } + + override fun unscheduleDrawable( + who: Drawable, + what: Runnable + ) { + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + BlurWallpaperProvider.getInstance(context).addListener(this) + fullBlurDrawable?.startListening() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + BlurWallpaperProvider.getInstance(context).removeListener(this) + fullBlurDrawable?.stopListening() + } + + override fun onDraw(canvas: Canvas) { + fullBlurDrawable?.alpha = blurAlpha + // fullBlurDrawable?.draw(canvas) + super.onDraw(canvas) + } + + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + super.onLayout(changed, left, top, right, bottom) + if (changed) { + fullBlurDrawable?.setBounds(left, top, right, bottom) + } + } + + /** + * We only need to change right bound for widget page blur layer. + */ + fun changeBlurBounds(factor: Float, isLeftToRight: Boolean) { + fullBlurDrawable?.setBounds(left, top, (right * factor).toInt(), bottom) + if (isLeftToRight) { + fullBlurDrawable?.canvasOffset = + (right - left) * (1 - factor) + } + fullBlurDrawable?.invalidateSelf() + } + + private fun createBlurDrawable() { + if (isAttachedToWindow) { + fullBlurDrawable?.stopListening() + } + fullBlurDrawable = blurWallpaperProvider.createDrawable().apply { + callback = blurDrawableCallback + setBounds(left, top, right, bottom) + } + if (isAttachedToWindow) fullBlurDrawable?.startListening() + } + + override fun onEnabledChanged() { + createBlurDrawable() + } + + override fun onWallpaperChanged() {} + + init { + setWillNotDraw(false) + blurWallpaperProvider = BlurWallpaperProvider.getInstance(context) + createBlurDrawable() + } +} diff --git a/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetsRootView.java b/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetsRootView.java index e9e7b74d06d0d43d7757126f3ebfb509cadbbf01..8631ed6a1577e53143fb4395d8773a627def10da 100644 --- a/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetsRootView.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/widgets/WidgetsRootView.java @@ -19,6 +19,10 @@ public class WidgetsRootView extends HorizontalScrollView { private boolean shouldScrollWorkspace = true; + // 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; + public WidgetsRootView(Context context) { super(context); init(); @@ -55,6 +59,16 @@ public class WidgetsRootView extends HorizontalScrollView { } } + @Override + public void fling(int velocityX) { + super.fling(velocityX); + float density = getResources().getDisplayMetrics().density; + if (Math.abs(velocityX) > FLING_THRESHOLD_VELOCITY * density) { + shouldScrollWorkspace = !shouldScrollWorkspace; + overlay.onScrollInteractionEnd(); + } + } + @Override protected boolean overScrollBy( int deltaX, diff --git a/app/src/main/res/anim/folder_interpolator.xml b/app/src/main/res/anim/folder_interpolator.xml new file mode 100644 index 0000000000000000000000000000000000000000..b95d4548ffb74b4689d7f25a03dffef5683e5468 --- /dev/null +++ b/app/src/main/res/anim/folder_interpolator.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml index 3f92b6f2934619c9718e3f2a7f9e6893ae3fc063..15769e455e4319685a79a7b98e857f397a02c584 100644 --- a/app/src/main/res/layout/activity_test.xml +++ b/app/src/main/res/layout/activity_test.xml @@ -13,8 +13,14 @@ android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false" + android:background="@android:color/transparent" android:importantForAccessibility="no"> + + - - \ No newline at end of file diff --git a/app/src/main/res/layout/folder_icon.xml b/app/src/main/res/layout/folder_icon.xml new file mode 100644 index 0000000000000000000000000000000000000000..d277f88ae1c4eec8ae3d8b56a3ed68824f37cc8e --- /dev/null +++ b/app/src/main/res/layout/folder_icon.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/layout/layout_folder.xml b/app/src/main/res/layout/layout_folder.xml index c3772e34d935a90198beaf8b652eaec36e5b1b45..03d72950a7e0d90f7ddaa82f4d1f5f1d80e3aae9 100644 --- a/app/src/main/res/layout/layout_folder.xml +++ b/app/src/main/res/layout/layout_folder.xml @@ -1,25 +1,26 @@ - - @@ -31,7 +32,7 @@ android:background="@drawable/folder_window" android:orientation="vertical"> - @@ -44,4 +45,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search_suggestion.xml b/app/src/main/res/layout/layout_search_suggestion.xml index 2ab03468f4c5e6d1be43ef3460b5fe009bc75912..0366d5500de59d69f186065ffd5322358a2d8702 100644 --- a/app/src/main/res/layout/layout_search_suggestion.xml +++ b/app/src/main/res/layout/layout_search_suggestion.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/layout_used_apps.xml b/app/src/main/res/layout/layout_used_apps.xml index 5b26e9678dc074c98d41ff41d296aeec96eddea2..1435459de0054a03a428b3e7b96a477ffcc3b24e 100755 --- a/app/src/main/res/layout/layout_used_apps.xml +++ b/app/src/main/res/layout/layout_used_apps.xml @@ -34,7 +34,7 @@ - + android:animateLayoutChanges="true" + android:paddingStart="8dp" + android:paddingEnd="8dp"> - + android:id="@+id/widgets_scroll_container" + android:overScrollMode="never" + android:scrollbars="none"> - + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:orientation="vertical" + android:paddingBottom="@dimen/dp_16"> + + + + - - + android:orientation="vertical" /> - - - - - - - -