Commit f7a132e5 authored by Amit Kumar's avatar Amit Kumar 💻
Browse files

Add uninstall button and wobble animation

parent 436f044b
Pipeline #129098 passed with stage
in 8 minutes and 21 seconds
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_29.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_29.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-08-08T17:07:28.754132Z" />
</component>
</project>
\ No newline at end of file
......@@ -30,6 +30,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.GridLayout;
import android.widget.Toast;
import foundation.e.blisslauncher.BuildConfig;
......@@ -66,6 +68,7 @@ import foundation.e.blisslauncher.features.test.dragndrop.DragView;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
......@@ -75,7 +78,7 @@ import org.jetbrains.annotations.NotNull;
public class LauncherPagedView extends PagedView<PageIndicatorDots> implements View.OnTouchListener,
Insettable, DropTarget, DragSource, DragController.DragListener,
LauncherStateManager.StateHandler {
LauncherStateManager.StateHandler, OnAlarmListener {
private static final String TAG = "LauncherPagedView";
private static final int DEFAULT_PAGE = 0;
......@@ -169,6 +172,10 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
// Total over scrollX in the overlay direction.
private float mOverlayTranslation;
private Alarm wobbleExpireAlarm = new Alarm();
private static final int WOBBLE_EXPIRATION_TIMEOUT = 25000;
public LauncherPagedView(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
......@@ -184,6 +191,8 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
setMotionEventSplittingEnabled(true);
setOnTouchListener((v, event) -> false);
wobbleExpireAlarm.setOnAlarmListener(this);
}
private void initWorkspace() {
......@@ -1881,6 +1890,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
@Override
public void onDragExit(DragObject dragObject) {
Log.d(TAG, "onDragExit() called with: dragObject = [" + dragObject + "]");
// Here we store the final page that will be dropped to, if the workspace in fact
// receives the drop
mDropToLayout = mDragTargetLayout;
......@@ -1895,6 +1905,9 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
setCurrentDragOverlappingLayout(null);
mSpringLoadedDragController.cancel();
// Reset the grid state by stopping the animation and removing uninstall icon after 25 seconds
wobbleExpireAlarm.setAlarm(WOBBLE_EXPIRATION_TIMEOUT);
}
@Override
......@@ -2176,13 +2189,13 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
final int curPage = getCurrentPage();
final CellLayout currentPage = (CellLayout) getPageAt(curPage);
final LauncherPagedView.ItemOperator packageAndUser =
(LauncherItem info, View view) -> info != null
(LauncherItem info, View view, int index) -> info != null
&& info.getTargetComponent() != null
&& TextUtils.equals(info.getTargetComponent().getPackageName(), packageName)
&& info.user.equals(user);
final LauncherPagedView.ItemOperator packageAndUserAndApp =
(LauncherItem info, View view) ->
packageAndUser.evaluate(info, view) && info.itemType == ITEM_TYPE_APPLICATION;
(LauncherItem info, View view, int index) ->
packageAndUser.evaluate(info, view, index) && info.itemType == ITEM_TYPE_APPLICATION;
return getFirstMatch(
new CellLayout[]{mLauncher.getHotseat(), currentPage},
......@@ -2200,9 +2213,9 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
final View[] matches = new View[operators.length];
// For efficiency, the outer loop should be CellLayout.
for (CellLayout cellLayout : cellLayouts) {
mapOverCellLayout(MAP_NO_RECURSE, cellLayout, (info, v) -> {
mapOverCellLayout(MAP_NO_RECURSE, cellLayout, (info, v, idx) -> {
for (int i = 0; i < operators.length; ++i) {
if (matches[i] == null && operators[i].evaluate(info, v)) {
if (matches[i] == null && operators[i].evaluate(info, v, idx)) {
matches[i] = v;
if (i == 0) {
// We can return since this is the best match possible.
......@@ -2255,12 +2268,12 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
final int childCount = folder.items.size();
for (int childIdx = 0; childIdx < childCount; childIdx++) {
LauncherItem childItem = folderChildren.get(childIdx);
if (op.evaluate(info, item)) {
if (op.evaluate(info, item, itemIdx)) {
return true;
}
}
} else {
if (op.evaluate(info, item)) {
if (op.evaluate(info, item, itemIdx)) {
return true;
}
}
......@@ -2271,7 +2284,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
public void updateNotificationBadge(Predicate<PackageUserKey> updatedDots) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
final Set<String> folderIds = new HashSet<>();
mapOverItems(MAP_RECURSE, (info, v) -> {
mapOverItems(MAP_RECURSE, (info, v, itemIdx) -> {
if ((info instanceof ApplicationItem || info instanceof ShortcutItem) && v instanceof IconTextView) {
if (!packageUserKey.updateFromItemInfo(info)
|| updatedDots.test(packageUserKey)) {
......@@ -2284,7 +2297,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
});
// Update folder icons
mapOverItems(MAP_NO_RECURSE, (info, v) -> {
mapOverItems(MAP_NO_RECURSE, (info, v, itemIdx) -> {
if (info instanceof FolderItem && folderIds.contains(info.id)
&& v instanceof IconTextView) {
FolderDotInfo folderDotInfo = new FolderDotInfo();
......@@ -2352,15 +2365,72 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
return mLauncher.getHotseat();
}
/**
* It starts to animate all the grid item and also add uninstall button to each item if the item supports it.
*/
public void wobbleLayouts() {
// Adds uninstall icon.
Animation wobbleAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.wobble);
Animation reverseWobbleAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.wobble_reverse);
mapOverItems(MAP_NO_RECURSE, (info, v, itemIdx) -> {
if ((info instanceof ApplicationItem || info instanceof ShortcutItem)
&& v instanceof IconTextView
&& !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) {
return false;
}
}
}
Log.d(TAG, "wobbleLayouts: "+info);
((IconTextView)v).applyUninstallIconState(true);
}
if(itemIdx % 2 == 0) {
v.startAnimation(wobbleAnimation);
} else {
v.startAnimation(reverseWobbleAnimation);
}
// process all the items
return false;
});
}
/**
* Triggered when wobble animation expire after timeout.
* @param alarm
*/
@Override
public void onAlarm(Alarm alarm) {
// Adds uninstall icon.
mapOverItems(MAP_NO_RECURSE, (info, v, idx) -> {
if (v instanceof IconTextView) {
((IconTextView)v).applyUninstallIconState(false);
}
// Clears if there is any running animation on the view.
v.clearAnimation();
// process all the items
return false;
});
}
public interface ItemOperator {
/**
* Process the next itemInfo, possibly with side-effect on the next item.
*
* @param info info for the shortcut
* @param view view for the shortcut
* @param index index of the view in the parent layout.
* @return true if done, false to continue the map
*/
boolean evaluate(LauncherItem info, View view);
boolean evaluate(LauncherItem info, View view, int index);
}
class FolderCreationAlarmListener implements OnAlarmListener {
......
......@@ -16,6 +16,7 @@
package foundation.e.blisslauncher.core.touch;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
......@@ -50,6 +51,11 @@ public class ItemClickHandler {
return;
}*/
if(v instanceof IconTextView) {
boolean result = ((IconTextView) v).tryToHandleUninstallClick(launcher);
if(result) return;
}
Object tag = v.getTag();
if (tag instanceof ShortcutItem) {
onClickAppShortcut(v, (ShortcutItem) tag, launcher);
......
......@@ -15,10 +15,11 @@
*/
package foundation.e.blisslauncher.core.touch;
import android.util.Log;
import static foundation.e.blisslauncher.features.test.LauncherState.NORMAL;
import static foundation.e.blisslauncher.features.test.LauncherState.OVERVIEW;
import android.view.View;
import android.view.View.OnLongClickListener;
import foundation.e.blisslauncher.core.database.model.LauncherItem;
import foundation.e.blisslauncher.features.test.CellLayout;
import foundation.e.blisslauncher.features.test.TestActivity;
......@@ -37,19 +38,21 @@ public class ItemLongClickListener {
private static boolean onWorkspaceItemLongClick(View v) {
int[] temp = new int[2];
v.getLocationOnScreen(temp);
Log.i(TAG,
"onWorkspaceItemLongClick: [" + v.getLeft() + ", " + v.getTop() + "] ["+temp[0]+", "+temp[1]+"]"
);
TestActivity launcher = TestActivity.Companion.getLauncher(v.getContext());
if (!canStartDrag(launcher)) return false;
//if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
if (!(v.getTag() instanceof LauncherItem)) return false;
//launcher.setWaitingForResult(null);
addWobbleAnimation(launcher);
beginDrag(v, launcher, (LauncherItem) v.getTag(), new DragOptions());
return true;
}
private static void addWobbleAnimation(TestActivity launcher) {
launcher.getLauncherPagedView().wobbleLayouts();
}
public static void beginDrag(
View v, TestActivity launcher, LauncherItem info,
DragOptions dragOptions
......
package foundation.e.blisslauncher.features.test
import android.R
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
......@@ -13,6 +12,7 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.TextUtils.TruncateAt
import android.util.AttributeSet
import android.util.Log
import android.util.Property
import android.util.TypedValue
import android.view.MotionEvent
......@@ -21,10 +21,13 @@ import android.view.ViewConfiguration
import android.widget.TextView
import androidx.core.graphics.ColorUtils
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.notification.DotInfo
import foundation.e.blisslauncher.features.notification.DotRenderer
import java.lang.IllegalArgumentException
import foundation.e.blisslauncher.features.test.uninstall.UninstallButtonRenderer
import kotlin.math.roundToInt
/**
......@@ -40,7 +43,9 @@ class IconTextView @JvmOverloads constructor(
companion object {
private const val DISPLAY_WORKSPACE = 1
private const val DISPLAY_FOLDER = 2
private val STATE_PRESSED = intArrayOf(R.attr.state_pressed)
private val STATE_PRESSED = intArrayOf(android.R.attr.state_pressed)
private const val TAG = "IconTextView"
private val DOT_SCALE_PROPERTY: Property<IconTextView, Float> =
object : Property<IconTextView, Float>(
......@@ -59,9 +64,28 @@ class IconTextView @JvmOverloads constructor(
iconTextView.invalidate()
}
}
private val UNINSTALL_SCALE_PROPERTY: Property<IconTextView, Float> =
object : Property<IconTextView, Float>(
java.lang.Float.TYPE,
"uninstallButtonScale"
) {
override fun get(iconTextView: IconTextView): Float {
return iconTextView.uninstallButtonScale
}
override fun set(
iconTextView: IconTextView,
value: Float
) {
iconTextView.uninstallButtonScale = value
iconTextView.invalidate()
}
}
}
private lateinit var mDotRenderer: DotRenderer
private lateinit var mUninstallRenderer: UninstallButtonRenderer
private val mActivity: TestActivity = if (context is TestActivity) context
else throw IllegalArgumentException("Cannot find TestActivity in context tree")
private var mStayPressed: Boolean = false
......@@ -78,6 +102,7 @@ class IconTextView @JvmOverloads constructor(
private val defaultIconSize = dp.iconSizePx
private var dotScale: Float = 0f
private var uninstallButtonScale: Float = 0f
private var mTextAlpha = 1f
private var mTextColor = Color.WHITE
......@@ -93,6 +118,12 @@ class IconTextView @JvmOverloads constructor(
private var mDotScaleAnim: Animator? = null
private var mForceHideDot = false
private var isUninstallVisible: Boolean = false
private var mUninstallIconScaleAnim: Animator? = null
private var touchX = 0
private var touchY = 0
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
......@@ -132,6 +163,7 @@ class IconTextView @JvmOverloads constructor(
applyIconAndLabel(item)
tag = item
applyDotState(item, false)
applyUninstallIconState(false)
}
private fun applyIconAndLabel(item: LauncherItem) {
......@@ -146,10 +178,6 @@ class IconTextView @JvmOverloads constructor(
super.setTag(tag)
}
override fun refreshDrawableState() {
super.refreshDrawableState()
}
override fun onCreateDrawableState(extraSpace: Int): IntArray? {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (mStayPressed) {
......@@ -186,6 +214,15 @@ class IconTextView @JvmOverloads constructor(
cancelDotScaleAnim()
dotScale = 0f
mForceHideDot = false
isUninstallVisible = false
cancelUninstallScaleAnim()
uninstallButtonScale = 0f
}
private fun cancelUninstallScaleAnim() {
Log.d(TAG, "cancelUninstallScaleAnim() called")
mUninstallIconScaleAnim?.cancel()
}
private fun cancelDotScaleAnim() {
......@@ -196,7 +233,7 @@ class IconTextView @JvmOverloads constructor(
cancelDotScaleAnim()
mDotScaleAnim = ObjectAnimator.ofFloat(
this,
DOT_SCALE_PROPERTY,
UNINSTALL_SCALE_PROPERTY,
*dotScales
).apply {
addListener(object : AnimatorListenerAdapter() {
......@@ -255,6 +292,11 @@ class IconTextView @JvmOverloads constructor(
var result = super.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// We store these value to check if the click has been made on uninstallIcon or not.
touchX = event.x.toInt()
touchY = event.y.toInt()
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> longPressHelper.cancelLongPress()
MotionEvent.ACTION_MOVE -> if (!Utilities.pointInView(this, event.x, event.y, slop)) {
longPressHelper.cancelLongPress()
......@@ -281,6 +323,7 @@ class IconTextView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawDotIfNecessary(canvas)
drawUninstallIcon(canvas)
}
/**
......@@ -303,15 +346,89 @@ class IconTextView @JvmOverloads constructor(
return mDotInfo != null
}
fun getIconBounds(outBounds: Rect) {
private fun getIconBounds(outBounds: Rect) {
getIconBounds(this, outBounds, defaultIconSize)
}
fun getIconBounds(iconView: View, outBounds: Rect, iconSize: Int) {
private fun getIconBounds(iconView: View, outBounds: Rect, iconSize: Int) {
val top = iconView.paddingTop
val left = (iconView.width - iconSize) / 2
val right = left + iconSize
val bottom = top + iconSize
outBounds[left, top, right] = bottom
}
/**
* Draws the uninstall button in the top right corner of the icon bounds.
* @param canvas The canvas to draw to.
*/
private fun drawUninstallIcon(canvas: Canvas?) {
if (isUninstallVisible || uninstallButtonScale > 0) {
Log.d(TAG, "drawUninstallIcon() called with: $isUninstallVisible $uninstallButtonScale")
val tempBounds = Rect()
getIconBounds(tempBounds)
val scrollX = scrollX
val scrollY = scrollY
canvas?.translate(scrollX.toFloat(), scrollY.toFloat())
mUninstallRenderer.draw(canvas, tempBounds)
canvas?.translate(-scrollX.toFloat(), -scrollY.toFloat())
}
}
fun applyUninstallIconState(showUninstallIcon: Boolean) {
Log.d(TAG, "applyUninstallIconState() called with: showUninstallIcon = $showUninstallIcon")
val wasUninstallVisible = isUninstallVisible
isUninstallVisible = showUninstallIcon
val newScale: Float = if (isUninstallVisible) 1f else 0f
mUninstallRenderer = mActivity.deviceProfile.uninstallRenderer
if (wasUninstallVisible || isUninstallVisible) {
// Animate when a dot is first added or when it is removed.
if (wasUninstallVisible xor isUninstallVisible && isShown) {
animateUninstallScale(newScale)
} else {
cancelUninstallScaleAnim()
uninstallButtonScale = newScale
invalidate()
}
}
}
private fun animateUninstallScale(vararg scales: Float) {
cancelUninstallScaleAnim()
mUninstallIconScaleAnim = ObjectAnimator.ofFloat(
this,
UNINSTALL_SCALE_PROPERTY,
*scales
).apply {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mUninstallIconScaleAnim = null
}
})
}
mUninstallIconScaleAnim?.start()
}
fun tryToHandleUninstallClick(launcher: TestActivity): Boolean {
if (!isUninstallVisible) {
return false
}
val iconBounds = Rect()
getIconBounds(iconBounds)
val uninstallIconBounds = mUninstallRenderer.getBoundsScaled(iconBounds)
if (uninstallIconBounds.contains(touchX, touchY)) {
val tag = tag as LauncherItem
if (tag.itemType == Constants.ITEM_TYPE_APPLICATION) {
launcher.uninstallApplication(tag as ApplicationItem)
} else if (tag.itemType == Constants.ITEM_TYPE_SHORTCUT) {
launcher.removeShortcut(tag as ShortcutItem)
}
// Reset touch coordinates
touchX = 0
touchY = 0
return true
}
return false
}
}
......@@ -17,9 +17,13 @@ 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.res.Configuration
import android.graphics.Point
import android.location.LocationManager
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.StrictMode
......@@ -27,6 +31,8 @@ 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
......@@ -62,8 +68,10 @@ 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
......@@ -76,6 +84,8 @@ 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.notification.NotificationService
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
......@@ -101,6 +111,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observers.DisposableObserver
import io.reactivex.schedulers.Schedulers
import java.net.URISyntaxException
import java.util.ArrayList
import java.util.Arrays
import java.util.Comparator
......@@ -1312,4 +1323,97 @@ class TestActivity : BaseDraggingActivity(), AutoCompleteAdapter.OnSuggestionCli
fun getDotInfoForItem(info: LauncherItem?): DotInfo? {
return notificationDataProvider.getDotInfoForItem(info)
}
fun removeShortcut(shortcut: ShortcutItem): Boolean {
val dialog: AlertDialog = AlertDialog.Builder(
ContextThemeWrapper(