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

Add shortcut support

parent 1e4acffa
Pipeline #180700 failed with stage
in 1 second
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<targetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="ZF65256BV3" />
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_29.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-09-30T08:05:54.967084Z" />
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-09-30T08:57:06.401616Z" />
</component>
</project>
\ No newline at end of file
......@@ -90,7 +90,7 @@
</intent-filter>
</activity>
<activity
android:name=".features.shortcuts.AddItemActivity"
android:name=".features.test.dragndrop.AddItemActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:label="@string/action_add_to_workspace"
......
......@@ -19,6 +19,7 @@ import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
......@@ -589,4 +590,26 @@ public class Utilities {
float progress = getProgress(t, fromMin, fromMax);
return mapRange(interpolator.getInterpolation(progress), toMin, toMax);
}
/**
* Returns true if the intent is a valid launch intent for a launcher activity of an app.
* This is used to identify shortcuts which are different from the ones exposed by the
* applications' manifest file.
*
* @param launchIntent The intent that will be launched when the shortcut is clicked.
*/
public static boolean isLauncherAppTarget(Intent launchIntent) {
if (launchIntent != null
&& Intent.ACTION_MAIN.equals(launchIntent.getAction())
&& launchIntent.getComponent() != null
&& launchIntent.getCategories() != null
&& launchIntent.getCategories().size() == 1
&& launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
&& TextUtils.isEmpty(launchIntent.getDataString())) {
// An app target can either have no extra or have ItemInfo.EXTRA_PROFILE.
Bundle extras = launchIntent.getExtras();
return extras == null || extras.keySet().isEmpty();
}
return false;
}
}
......@@ -7,6 +7,7 @@ import static foundation.e.blisslauncher.features.test.dragndrop.DragLayer.ALPHA
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
......@@ -14,6 +15,7 @@ import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Paint;
import android.graphics.Point;
......@@ -23,6 +25,7 @@ import android.os.UserHandle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.MutableInt;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
......@@ -31,6 +34,7 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.OvershootInterpolator;
import android.widget.GridLayout;
import android.widget.Toast;
import foundation.e.blisslauncher.BuildConfig;
......@@ -51,6 +55,9 @@ import foundation.e.blisslauncher.core.utils.IntegerArray;
import foundation.e.blisslauncher.core.utils.PackageUserKey;
import foundation.e.blisslauncher.features.launcher.Hotseat;
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.test.Alarm;
import foundation.e.blisslauncher.features.test.CellLayout;
import foundation.e.blisslauncher.features.test.IconTextView;
......@@ -62,6 +69,7 @@ import foundation.e.blisslauncher.features.test.VariantDeviceProfile;
import foundation.e.blisslauncher.features.test.WorkspaceStateTransitionAnimation;
import foundation.e.blisslauncher.features.test.anim.AnimatorSetBuilder;
import foundation.e.blisslauncher.features.test.anim.Interpolators;
import foundation.e.blisslauncher.features.test.anim.PropertyListBuilder;
import foundation.e.blisslauncher.features.test.dragndrop.DragController;
import foundation.e.blisslauncher.features.test.dragndrop.DragOptions;
import foundation.e.blisslauncher.features.test.dragndrop.DragSource;
......@@ -71,8 +79,11 @@ import foundation.e.blisslauncher.features.test.dragndrop.SpringLoadedDragContro
import foundation.e.blisslauncher.features.test.graphics.DragPreviewProvider;
import foundation.e.blisslauncher.features.test.uninstall.UninstallHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.jetbrains.annotations.NotNull;
......@@ -96,7 +107,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
private static final boolean MAP_RECURSE = true;
// The screen id used for the empty screen always present to the right.
public static final int EXTRA_EMPTY_SCREEN_ID = -201;
public static final int EXTRA_EMPTY_SCREEN_ID = -201;
// The is the first screen. It is always present, even if its empty.
public static final long FIRST_SCREEN_ID = 0;
private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
......@@ -116,6 +127,12 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
// How long to wait before the new-shortcut animation automatically pans the workspace
private static final int NEW_APPS_PAGE_MOVE_DELAY = 500;
private static final int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5;
static final int NEW_APPS_ANIMATION_DELAY = 500;
private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
private final static Paint sPaint = new Paint();
Runnable mRemoveEmptyScreenRunnable;
......@@ -176,6 +193,11 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
private Alarm wobbleExpireAlarm = new Alarm();
private static final int WOBBLE_EXPIRATION_TIMEOUT = 25000;
/**
* Map of ShortcutKey to the number of times it is pinned.
*/
public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
public LauncherPagedView(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
......@@ -287,7 +309,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
}
public void bindScreens(@NotNull IntegerArray orderedScreenIds) {
if(orderedScreenIds.isEmpty()) {
if (orderedScreenIds.isEmpty()) {
addExtraEmptyScreen();
}
......@@ -297,8 +319,14 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
}
}
public void bindItems(@NotNull List<? extends LauncherItem> launcherItems) {
for (LauncherItem launcherItem : launcherItems) {
public void bindItems(
@NotNull List<? extends LauncherItem> launcherItems,
boolean animateIcons
) {
final Collection<Animator> bounceAnims = new ArrayList<>();
int newItemsScreenId = -1;
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);
......@@ -306,7 +334,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
appView.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
if (launcherItem.container == Constants.CONTAINER_DESKTOP) {
CellLayout cl = getScreenWithId(launcherItem.screenId);
if(cl != null && cl.isOccupied(launcherItem.cell)) {
if (cl != null && cl.isOccupied(launcherItem.cell)) {
// TODO: Add item to the end of the list
}
GridLayout.Spec rowSpec = GridLayout.spec(GridLayout.UNDEFINED);
......@@ -317,7 +345,6 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
iconLayoutParams.width = mLauncher.getDeviceProfile().getCellWidthPx();
appView.setLayoutParams(iconLayoutParams);
appView.setTextVisibility(true);
addInScreenFromBind(appView, launcherItem);
} else if (launcherItem.container == Constants.CONTAINER_HOTSEAT) {
GridLayout.Spec rowSpec = GridLayout.spec(GridLayout.UNDEFINED);
GridLayout.Spec colSpec = GridLayout.spec(GridLayout.UNDEFINED);
......@@ -326,11 +353,254 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
iconLayoutParams.height = mLauncher.getDeviceProfile().getHotseatCellHeightPx();
iconLayoutParams.width = mLauncher.getDeviceProfile().getCellWidthPx();
appView.setLayoutParams(iconLayoutParams);
addInScreenFromBind(appView, launcherItem);
}
addInScreenFromBind(appView, launcherItem);
if (animateIcons) {
// Animate all the applications up now
appView.setAlpha(0f);
appView.setScaleX(0f);
appView.setScaleY(0f);
bounceAnims.add(createNewAppBounceAnimation(appView, i));
newItemsScreenId = launcherItem.screenId;
}
}
// Animate to the correct page
if (animateIcons && newItemsScreenId > -1) {
AnimatorSet anim = new AnimatorSet();
anim.playTogether(bounceAnims);
int currentScreenId = getScreenIdForPageIndex(getNextPage());
final int newScreenIndex = getPageIndexForScreenId(newItemsScreenId);
final Runnable startBounceAnimRunnable = anim::start;
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) () -> {
AbstractFloatingView.closeAllOpenViews(mLauncher, false);
snapToPage(newScreenIndex);
postDelayed(
startBounceAnimRunnable,
NEW_APPS_ANIMATION_DELAY
);
}, NEW_APPS_PAGE_MOVE_DELAY);
} else {
postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY);
}
}
requestLayout();
}
public void bindItemsAdded(@NotNull List<? extends LauncherItem> items) {
final ArrayList<LauncherItem> addedItemsFinal = new ArrayList<>();
final IntegerArray addedWorkspaceScreensFinal = new IntegerArray();
List<LauncherItem> filteredItems = new ArrayList<>();
for (LauncherItem item : items) {
if (item.itemType == Constants.ITEM_TYPE_APPLICATION ||
item.itemType == Constants.ITEM_TYPE_SHORTCUT
) {
// Short-circuit this logic if the icon exists somewhere on the workspace
if (shortcutExists(item.getIntent(), item.user.getRealHandle())) {
continue;
}
}
if (item != null) {
filteredItems.add(item);
}
}
for (LauncherItem item : filteredItems) {
// Find appropriate space for the item.
int[] coords = findSpaceForItem(addedWorkspaceScreensFinal);
int screenId = coords[0];
LauncherItem itemInfo;
if (item instanceof ApplicationItem || item instanceof ShortcutItem ||
item instanceof FolderItem) {
itemInfo = item;
itemInfo.screenId = screenId;
itemInfo.cell = coords[1];
itemInfo.container = Constants.CONTAINER_DESKTOP;
} else {
throw new RuntimeException("Unexpected info type");
}
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);
if (count == null) {
count = new MutableInt(1);
pinnedShortcutCounts.put(pinnedShortcut, count);
} else {
count.value++;
}
// Since this is a new item, pin the shortcut in the system server.
if (count.value == 1) {
DeepShortcutManager.getInstance(getContext()).pinShortcut(pinnedShortcut);
}
}
// Save the WorkspaceItemInfo for binding in the workspace
addedItemsFinal.add(itemInfo);
}
if (!addedItemsFinal.isEmpty()) {
final ArrayList<LauncherItem> addAnimated = new ArrayList<>();
final ArrayList<LauncherItem> addNotAnimated = new ArrayList<>();
if (!addedItemsFinal.isEmpty()) {
LauncherItem info = addedItemsFinal.get(addedItemsFinal.size() - 1);
int lastScreenId = info.screenId;
for (LauncherItem i : addedItemsFinal) {
if (i.screenId == lastScreenId) {
addAnimated.add(i);
} else {
addNotAnimated.add(i);
}
}
}
if (!addedWorkspaceScreensFinal.isEmpty()) {
bindScreens(addedWorkspaceScreensFinal);
}
// We add the items without animation on non-visible pages, and with
// animations on the new page (which we will try and snap to).
if (addNotAnimated != null && !addNotAnimated.isEmpty()) {
bindItems(addNotAnimated, false);
}
if (addAnimated != null && !addAnimated.isEmpty()) {
bindItems(addAnimated, true);
}
// Remove the extra empty screen
removeExtraEmptyScreen(false, false);
updateDatabase(getWorkspaceAndHotseatCellLayouts());
}
}
private int[] findSpaceForItem(IntegerArray addedWorkspaceScreensFinal) {
// Find appropriate space for the item.
int screenId = 0;
int cell = 0;
boolean found = false;
int screenCount = getChildCount();
for (int screen = 0; screen < screenCount; screen++) {
View child = getChildAt(screen);
if (child instanceof CellLayout) {
CellLayout cellLayout = (CellLayout) child;
int index = mWorkspaceScreens.indexOfValue(cellLayout);
screenId = mWorkspaceScreens.keyAt(index);
if (cellLayout.getChildCount() < cellLayout.getMaxChildCount()) {
found = true;
cell = cellLayout.getChildCount();
break;
}
}
}
if (!found) {
screenId = screenId + 1;
addedWorkspaceScreensFinal.add(screenId);
cell = 0;
}
return new int[]{screenId, cell};
}
/**
* 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.
*/
protected boolean shortcutExists(Intent intent, UserHandle user) {
final String compPkgName, intentWithPkg, intentWithoutPkg;
if (intent == null) {
// Skip items with null intents
return true;
}
if (intent.getComponent() != null) {
// If component is not null, an intent with null package will produce
// the same result and should also be a match.
compPkgName = intent.getComponent().getPackageName();
if (intent.getPackage() != null) {
intentWithPkg = intent.toUri(0);
intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
} else {
intentWithPkg = new Intent(intent).setPackage(compPkgName).toUri(0);
intentWithoutPkg = intent.toUri(0);
}
} else {
compPkgName = null;
intentWithPkg = intent.toUri(0);
intentWithoutPkg = intent.toUri(0);
}
boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent);
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
// map over all the shortcuts on the workspace
final int itemCount = layout.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = layout.getChildAt(itemIdx);
LauncherItem info = (LauncherItem) item.getTag();
if (info instanceof FolderItem) {
FolderItem folder = (FolderItem) info;
List<LauncherItem> folderChildren = folder.items;
// map over all the children in the folder
final int childCount = folder.items.size();
for (int childIdx = 0; childIdx < childCount; childIdx++) {
LauncherItem childItem = folderChildren.get(childIdx);
if (childItem.getIntent() != null && childItem.user.equals(user)) {
Intent copyIntent = new Intent(childItem.getIntent());
copyIntent.setSourceBounds(intent.getSourceBounds());
String s = copyIntent.toUri(0);
if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
return true;
}
// checking for existing promise icon with same package name
if (isLauncherAppTarget
&& childItem.getTargetComponent() != null
&& compPkgName != null
&& compPkgName
.equals(childItem.getTargetComponent().getPackageName())) {
return true;
}
}
}
} else {
if (info.getIntent() != null && info.user.equals(user)) {
Intent copyIntent = new Intent(info.getIntent());
copyIntent.setSourceBounds(intent.getSourceBounds());
String s = copyIntent.toUri(0);
if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
return true;
}
// checking for existing promise icon with same package name
if (isLauncherAppTarget
&& info.getTargetComponent() != null
&& compPkgName != null
&& compPkgName.equals(info.getTargetComponent().getPackageName())) {
return true;
}
}
}
}
}
return false;
}
private ValueAnimator createNewAppBounceAnimation(View v, int i) {
ValueAnimator bounceAnim = new PropertyListBuilder().alpha(1).scale(1).build(v)
.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION);
bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY);
bounceAnim.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
return bounceAnim;
}
public GridLayout insertNewWorkspaceScreen(int screenId) {
return insertNewWorkspaceScreen(screenId, getChildCount());
}
......@@ -637,7 +907,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
return indexOfChild(mWorkspaceScreens.get(screenId));
}
public long getScreenIdForPageIndex(int index) {
public int getScreenIdForPageIndex(int index) {
if (0 <= index && index < mScreenOrder.size()) {
return mScreenOrder.get(index);
}
......@@ -666,7 +936,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
for (int i = 0; i < total; i++) {
int id = mWorkspaceScreens.keyAt(i);
GridLayout cl = mWorkspaceScreens.valueAt(i);
if(id > FIRST_SCREEN_ID && cl.getChildCount() == 0) {
if (id > FIRST_SCREEN_ID && cl.getChildCount() == 0) {
removeScreens.add(id);
}
}
......@@ -775,7 +1045,7 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
// It helps in recovering from situation when a layout is not saved correctly.
// TODO: Figure out when it can happen.
if(index > layout.getChildCount()) {
if (index > layout.getChildCount()) {
index = layout.getChildCount();
}
......@@ -2466,8 +2736,6 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
computeScrollHelper(false);
}
public interface ItemOperator {
/**
* Process the next itemInfo, possibly with side-effect on the next item.
......
package foundation.e.blisslauncher.core.executors;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.TimeUnit;
public class MainThreadExecutor extends LooperExecutor {
public MainThreadExecutor() {
super(Looper.getMainLooper());
}
}
......@@ -158,15 +158,14 @@ public class ItemClickHandler {
}
if (item instanceof ShortcutItem) {
ShortcutItem si = (ShortcutItem) item;
/*if (si.hasStatusFlag(ShortcutInfo.FLAG_SUPPORTS_WEB_UI)
&& intent.getAction() == Intent.ACTION_VIEW) {
if (intent.getAction() == Intent.ACTION_VIEW) {
// make a copy of the intent that has the package set to null
// we do this because the platform sometimes disables instant
// apps temporarily (triggered by the user) and fallbacks to the
// web ui. This only works though if the package isn't set
intent = new Intent(intent);
intent.setPackage(null);
}*/
}
}
launcher.startActivitySafely(v, intent, item);
}
......
......@@ -18,6 +18,8 @@ package foundation.e.blisslauncher.core.utils;
import android.os.Looper;
import foundation.e.blisslauncher.features.test.LauncherModel;
/**
* A set of utility methods for thread verification.
*/
......@@ -30,10 +32,9 @@ public class Preconditions {
}
public static void assertWorkerThread() {
//TODO: Uncommnet after LauncherModel
/*if (!isSameLooper(LauncherModel.getWorkerLooper())) {
if (!isSameLooper(LauncherModel.getWorkerLooper())) {
throw new IllegalStateException();
}*/
}
}
public static void assertUIThread() {
......
package foundation.e.blisslauncher.features.launcher.tasks;
import android.content.pm.ShortcutInfo;
import android.os.AsyncTask;
import android.os.Process;
import android.util.Log;
import foundation.e.blisslauncher.features.launcher.AppProvider;
import foundation.e.blisslauncher.features.shortcuts.DeepShortcutManager;
import foundation.e.blisslauncher.features.shortcuts.ShortcutInfoCompat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
......@@ -27,11 +30,15 @@ public class LoadShortcutTask extends AsyncTask<Void, Void, Map<String, Shortcut
@Override
protected Map<String, ShortcutInfoCompat> doInBackground(Void... voids) {
List<ShortcutInfoCompat> list = DeepShortcutManager.getInstance(mAppProvider.getContext()).queryForPinnedShortcuts(null,