From 501765db09ae86c2ca3c59cf03cb4bb0ba1f0738 Mon Sep 17 00:00:00 2001 From: PsyKzz Date: Sun, 29 Apr 2018 01:43:53 +0100 Subject: [PATCH 1/7] Support drag and drop moving of favourites --- .../main/java/fr/neamar/kiss/DataHandler.java | 48 +++++ .../fr/neamar/kiss/forwarder/Favorites.java | 164 +++++++++++++++--- 2 files changed, 191 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/fr/neamar/kiss/DataHandler.java b/app/src/main/java/fr/neamar/kiss/DataHandler.java index 00236640b..74ffe81e4 100644 --- a/app/src/main/java/fr/neamar/kiss/DataHandler.java +++ b/app/src/main/java/fr/neamar/kiss/DataHandler.java @@ -10,6 +10,7 @@ import android.graphics.Bitmap.CompressFormat; import android.os.IBinder; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; import android.widget.Toast; @@ -431,6 +432,53 @@ public ArrayList getFavorites(int limit) { return favorites; } + /** + * This method is used to set the specific position of an app in the fav array. + * + * @param context The mainActivity context + * @param id the app you want to set the position of + * @param position the new position of the fav + */ + public void setFavoritePosition(MainActivity context, String id, int position) { + String favApps = PreferenceManager.getDefaultSharedPreferences(this.context). + getString("favorite-apps-list", ""); + List favAppsList = new ArrayList<>(Arrays.asList(favApps.split(";"))); + + int currentPos = favAppsList.indexOf(id); + if(currentPos == -1) { + Log.e(TAG, "Couldn't find id in favAppsList"); + // TODO: Perhaps we should throw a better error here? + return; + } + // Clamp the position so we dont just extend past the end of the array. + position = Math.min(position, favAppsList.size() - 1); + + favAppsList.remove(currentPos); + // Because we're removing ourselves from the array, positions may change, we should take that into account + favAppsList.add(currentPos > position ? position + 1 : position, id); + String newFavList = TextUtils.join(";", favAppsList); + + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString("favorite-apps-list", newFavList + ";").apply(); + + context.onFavoriteChange(); + } + + /** + * Helper function to get the position of a favorite. Used mainly by the drag and drop system to know where to place the dropped app. + * + * @param context mainActivity context + * @param id the app you want to get the position of. + * @return + */ + public int getFavoritePosition(MainActivity context, String id) { + String favApps = PreferenceManager.getDefaultSharedPreferences(this.context). + getString("favorite-apps-list", ""); + List favAppsList = new ArrayList<>(Arrays.asList(favApps.split(";"))); + + return favAppsList.indexOf(id); + } + public void addToFavorites(MainActivity context, String id) { String favApps = PreferenceManager.getDefaultSharedPreferences(context). diff --git a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java index 0e2864b48..c2a662db6 100644 --- a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java +++ b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java @@ -10,6 +10,8 @@ import android.net.Uri; import android.provider.ContactsContract; import android.util.Log; +import android.view.DragEvent; +import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; @@ -25,7 +27,7 @@ import fr.neamar.kiss.ui.ListPopup; import fr.neamar.kiss.ui.RoundedQuickContactBadge; -public class Favorites extends Forwarder implements View.OnClickListener, View.OnLongClickListener { +public class Favorites extends Forwarder implements View.OnClickListener, View.OnLongClickListener, View.OnTouchListener, View.OnDragListener { private static final String TAG = "FavoriteForwarder"; // Package used by Android when an Intent can be matched with more than one app @@ -53,6 +55,23 @@ public class Favorites extends Forwarder implements View.OnClickListener, View.O */ private ArrayList favoritesPojo = new ArrayList<>(); + /** + * Globals for drag and drop support + */ + private static long startTime = 0; // Start of the drag and drop, used for long press menu + private float currentX = 0.0f; // Current X position of the drag op, this is 0 on DRAG END so we keep a copy here + private Pojo overApp; // the view for the DRAG_END event is typically wrong, so we store a reference of the last dragged over app. + + /** + * Configuration for drag and drop + */ + private final int MOVE_SENSITIVITY = 15; // How much you need to move your finger to be considered "moving" + private final int LONG_PRESS_DELAY = 250; // How long to hold your finger inplace to trigger the app menu. + + // Use so we dont over process on the drag events. + private boolean isDragging = false; + private boolean isTouching = false; + Favorites(MainActivity mainActivity) { super(mainActivity); } @@ -73,8 +92,8 @@ void onCreate() { favoritesViews[i] = mainActivity.favoritesBar.findViewById(FAV_IDS[i]); } - registerClickOnFavorites(); - registerLongClickOnFavorites(); + registerTouchOnFavorites(); + registerDragOnFavorites(); if (prefs.getBoolean("firstRun", true)) { // It is the first run. Make sure this is not an update by checking if history is empty @@ -224,45 +243,40 @@ private void addDefaultAppsToFavs() { } } - private void registerClickOnFavorites() { + private void registerTouchOnFavorites() { for (View v : favoritesViews) { - v.setOnClickListener(this); + v.setOnTouchListener(this); } } - - private void registerLongClickOnFavorites() { + private void registerDragOnFavorites() { for (View v : favoritesViews) { - v.setOnLongClickListener(this); + v.setOnDragListener(this); } } - @Override - public void onClick(View v) { - int favNumber = Integer.parseInt((String) v.getTag()); + private Result getFavResult(int favNumber) { if (favNumber >= favoritesPojo.size()) { // Clicking on a favorite before everything is loaded. Log.i(TAG, "Clicking on an unitialized favorite."); - return; + return null; } // Favorites handling Pojo pojo = favoritesPojo.get(favNumber); - final Result result = Result.fromPojo(mainActivity, pojo); + return Result.fromPojo(mainActivity, pojo); + } + @Override + public void onClick(View v) { + int favNumber = Integer.parseInt((String) v.getTag()); + final Result result = getFavResult(favNumber); result.fastLaunch(mainActivity, v); } @Override public boolean onLongClick(View v) { int favNumber = Integer.parseInt((String) v.getTag()); - if (favNumber >= favoritesPojo.size()) { - // Clicking on a favorite before everything is loaded. - Log.i(TAG, "Long clicking on an unitialized favorite."); - return false; - } - // Favorites handling - Pojo pojo = favoritesPojo.get(favNumber); - final Result result = Result.fromPojo(mainActivity, pojo); + final Result result = getFavResult(favNumber); ListPopup popup = result.getPopupMenu(mainActivity, mainActivity.adapter, v); mainActivity.registerPopup(popup); popup.show(v); @@ -272,4 +286,112 @@ public boolean onLongClick(View v) { private boolean isExternalFavoriteBarEnabled() { return prefs.getBoolean("enable-favorites-bar", true); } + + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + Log.i(TAG, "OnTouch down event, resetting timer"); + startTime = motionEvent.getEventTime(); + isTouching = true; + } + // NO need to do the extra work + if(isDragging || !isTouching) { + return true; + } + + // Click handlers first + long holdTime = motionEvent.getEventTime() - startTime; + if(holdTime > LONG_PRESS_DELAY) { + // Reset so we dont trigger the menu again next time. + isTouching = false; + startTime = motionEvent.getEventTime(); + this.onLongClick(view); + return true; + } + if (holdTime < LONG_PRESS_DELAY && motionEvent.getAction() == MotionEvent.ACTION_UP) { + isTouching = false; + this.onClick(view); + return true; + } + + // Drag handlers + int intCurrentY = Math.round(motionEvent.getY()); + int intCurrentX = Math.round(motionEvent.getX()); + int intStartY = motionEvent.getHistorySize() > 0 ? Math.round(motionEvent.getHistoricalY(0)) : intCurrentY; + int intStartX = motionEvent.getHistorySize() > 0 ? Math.round(motionEvent.getHistoricalX(0)) : intCurrentX; + boolean hasMoved = (Math.abs(intCurrentX - intStartX) > MOVE_SENSITIVITY) || (Math.abs(intCurrentY - intStartY) > MOVE_SENSITIVITY); + + if (hasMoved) { + View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view); + view.startDrag(null, shadowBuilder, view, 0); + view.setVisibility(View.INVISIBLE); + return true; + } + + return true; + } + + @Override + public boolean onDrag(View v, final DragEvent event) { + int overFavIndex; + + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + isDragging = true; + break; + + case DragEvent.ACTION_DRAG_ENTERED: + case DragEvent.ACTION_DRAG_EXITED: + case DragEvent.ACTION_DROP: + if (!isDragging) { + return true; + } + + overFavIndex = Integer.parseInt((String) v.getTag()); + overApp = favoritesPojo.get(overFavIndex); + + currentX = (event.getX() != 0.0f) ? event.getX() : currentX; + break; + + case DragEvent.ACTION_DRAG_ENDED: + // Only need to handle this action once. + if(!isDragging) { + return true; + } + isDragging = false; + + final View draggedView = (View) event.getLocalState(); + int draggedFavIndex = Integer.parseInt((String) draggedView.getTag()); + final Pojo draggedApp = favoritesPojo.get(draggedFavIndex); + + int left = v.getLeft(); + int right = v.getRight(); + int width = right - left; + + // currentX is relative to the view not the screen, so add the current X of the view. + final boolean leftSide = (left + currentX < left + (width / 2)); + + final int pos = KissApplication.getApplication(mainActivity).getDataHandler().getFavoritePosition(mainActivity, overApp.id); + draggedView.post(new Runnable() { + @Override + public void run() { + // Signals to a View that the drag and drop operation has concluded. + // If event result is set, this means the dragged view was dropped in target + if (event.getResult()) { + KissApplication.getApplication(mainActivity).getDataHandler().setFavoritePosition(mainActivity, draggedApp.id, leftSide ? pos - 1 : pos); + mainActivity.onFavoriteChange(); + } else { + draggedView.setVisibility(View.VISIBLE); + } + } + }); + + break; + default: + break; + } + return true; + } } + From 2e020263d6f6ccc8476654ddc758e5e10de6be22 Mon Sep 17 00:00:00 2001 From: PsyKzz Date: Mon, 30 Apr 2018 19:14:27 +0100 Subject: [PATCH 2/7] Fix issue with null overApp --- .../fr/neamar/kiss/forwarder/Favorites.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java index c2a662db6..d55fa2f9c 100644 --- a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java +++ b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java @@ -287,15 +287,13 @@ private boolean isExternalFavoriteBarEnabled() { return prefs.getBoolean("enable-favorites-bar", true); } - @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - Log.i(TAG, "OnTouch down event, resetting timer"); startTime = motionEvent.getEventTime(); isTouching = true; } - // NO need to do the extra work + // No need to do the extra work if(isDragging || !isTouching) { return true; } @@ -305,7 +303,6 @@ public boolean onTouch(View view, MotionEvent motionEvent) { if(holdTime > LONG_PRESS_DELAY) { // Reset so we dont trigger the menu again next time. isTouching = false; - startTime = motionEvent.getEventTime(); this.onLongClick(view); return true; } @@ -362,6 +359,19 @@ public boolean onDrag(View v, final DragEvent event) { isDragging = false; final View draggedView = (View) event.getLocalState(); + + // Sometimes we dont trigger onDrag over another app, in which case just drop. + if (overApp == null) { + Log.w(TAG, "Wasn't dragged over an app, returning app to starting position"); + draggedView.post(new Runnable() { + @Override + public void run() { + draggedView.setVisibility(View.VISIBLE); + } + }); + break; + } + int draggedFavIndex = Integer.parseInt((String) draggedView.getTag()); final Pojo draggedApp = favoritesPojo.get(draggedFavIndex); From b0ed09c28ff35821a336aece3e4e0b2a912866a6 Mon Sep 17 00:00:00 2001 From: PsyKzz Date: Mon, 30 Apr 2018 19:14:38 +0100 Subject: [PATCH 3/7] Animate layout changes --- app/src/main/res/layout/main.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index a13951612..006e6b9d4 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -58,6 +58,7 @@ Date: Wed, 2 May 2018 00:00:44 +0100 Subject: [PATCH 4/7] Add Haptic feedback --- app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java index d55fa2f9c..72ca23eef 100644 --- a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java +++ b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java @@ -11,6 +11,7 @@ import android.provider.ContactsContract; import android.util.Log; import android.view.DragEvent; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; @@ -271,6 +272,8 @@ public void onClick(View v) { int favNumber = Integer.parseInt((String) v.getTag()); final Result result = getFavResult(favNumber); result.fastLaunch(mainActivity, v); + v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); + } @Override @@ -280,6 +283,8 @@ public boolean onLongClick(View v) { ListPopup popup = result.getPopupMenu(mainActivity, mainActivity.adapter, v); mainActivity.registerPopup(popup); popup.show(v); + v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; } From dae4fb7d4a4abd009c79918128fa25e040bd2c1a Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 3 May 2018 17:50:45 +0100 Subject: [PATCH 5/7] Remove TODO --- app/src/main/java/fr/neamar/kiss/DataHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/fr/neamar/kiss/DataHandler.java b/app/src/main/java/fr/neamar/kiss/DataHandler.java index 74ffe81e4..5388925a7 100644 --- a/app/src/main/java/fr/neamar/kiss/DataHandler.java +++ b/app/src/main/java/fr/neamar/kiss/DataHandler.java @@ -447,7 +447,6 @@ public void setFavoritePosition(MainActivity context, String id, int position) { int currentPos = favAppsList.indexOf(id); if(currentPos == -1) { Log.e(TAG, "Couldn't find id in favAppsList"); - // TODO: Perhaps we should throw a better error here? return; } // Clamp the position so we dont just extend past the end of the array. From 9e03f16adf4b9e40fe2310c025fdbd9f567ca596 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Fri, 4 May 2018 18:46:12 +0100 Subject: [PATCH 6/7] Show context menu on long press but dismiss when we start dragging Additionally cleanup dismiss popup and tone up move sensitivity --- .../java/fr/neamar/kiss/MainActivity.java | 55 +++++++++---------- .../fr/neamar/kiss/forwarder/Favorites.java | 26 ++++----- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/fr/neamar/kiss/MainActivity.java b/app/src/main/java/fr/neamar/kiss/MainActivity.java index a5aee67c6..3f1fffa2b 100644 --- a/app/src/main/java/fr/neamar/kiss/MainActivity.java +++ b/app/src/main/java/fr/neamar/kiss/MainActivity.java @@ -18,6 +18,7 @@ import android.text.TextWatcher; import android.util.Log; import android.view.ContextMenu; +import android.view.DragEvent; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; @@ -274,6 +275,18 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { } }); + + // Fixes bug when dropping onto a textEdit widget which can cause a NPE + // This fix should be on ALL TextEdit Widgets !!! + // See : https://stackoverflow.com/a/23483957 + searchEditText.setOnDragListener( new View.OnDragListener() { + @Override + public boolean onDrag( View v, DragEvent event) { + return true; + } + }); + + // On validate, launch first record searchEditText.setOnEditorActionListener(new OnEditorActionListener() { @Override @@ -358,9 +371,7 @@ protected void onResume() { return; } - if (mPopup != null) { - mPopup.dismiss(); - } + dismissPopup(); if (KissApplication.getApplication(this).getDataHandler().allProvidersHaveLoaded) { displayLoader(false); @@ -523,26 +534,11 @@ public void onLauncherButtonClicked(View launcherButton) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { - if (mPopup != null) { - View popupContentView = mPopup.getContentView(); - int[] popupPos = {0, 0}; - popupContentView.getLocationOnScreen(popupPos); - final float offsetX = -popupPos[0]; - final float offsetY = -popupPos[1]; - ev.offsetLocation(offsetX, offsetY); - try { - boolean handled = popupContentView.dispatchTouchEvent(ev); - ev.offsetLocation(-offsetX, -offsetY); - if (!handled) - handled = super.dispatchTouchEvent(ev); - return handled; - } - catch(IllegalArgumentException e) { - // Quick temporary fix for #925 - return false; - } + boolean handled = super.dispatchTouchEvent(ev); + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + dismissPopup(); } - return super.dispatchTouchEvent(ev); + return handled; } private void displayClearOnInput() { @@ -667,9 +663,7 @@ public void updateSearchRecords() { */ private void updateSearchRecords(String query) { resetTask(); - - if (mPopup != null) - mPopup.dismiss(); + dismissPopup(); forwarderManager.updateSearchRecords(query); @@ -714,8 +708,7 @@ public void launchOccurred() { public void registerPopup(ListPopup popup) { if (mPopup == popup) return; - if (mPopup != null) - mPopup.dismiss(); + dismissPopup(); mPopup = popup; popup.setVisibilityHelper(systemUiVisibilityHelper); popup.setOnDismissListener(new PopupWindow.OnDismissListener() { @@ -756,8 +749,7 @@ public void hideKeyboard() { } systemUiVisibilityHelper.onKeyboardVisibilityChanged(false); - if (mPopup != null) - mPopup.dismiss(); + dismissPopup(); } @Override @@ -787,4 +779,9 @@ public void beforeListChange() { public void afterListChange() { list.animateChange(); } + + public void dismissPopup() { + if (mPopup != null) + mPopup.dismiss(); + } } diff --git a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java index 72ca23eef..f2ee78b33 100644 --- a/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java +++ b/app/src/main/java/fr/neamar/kiss/forwarder/Favorites.java @@ -66,12 +66,12 @@ public class Favorites extends Forwarder implements View.OnClickListener, View.O /** * Configuration for drag and drop */ - private final int MOVE_SENSITIVITY = 15; // How much you need to move your finger to be considered "moving" + private final int MOVE_SENSITIVITY = 5; // How much you need to move your finger to be considered "moving" private final int LONG_PRESS_DELAY = 250; // How long to hold your finger inplace to trigger the app menu. // Use so we dont over process on the drag events. private boolean isDragging = false; - private boolean isTouching = false; + private boolean contextMenuShown = false; Favorites(MainActivity mainActivity) { super(mainActivity); @@ -284,7 +284,6 @@ public boolean onLongClick(View v) { mainActivity.registerPopup(popup); popup.show(v); v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - return true; } @@ -296,26 +295,25 @@ private boolean isExternalFavoriteBarEnabled() { public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { startTime = motionEvent.getEventTime(); - isTouching = true; + contextMenuShown = false; + return true; } // No need to do the extra work - if(isDragging || !isTouching) { + if(isDragging) { return true; } // Click handlers first long holdTime = motionEvent.getEventTime() - startTime; - if(holdTime > LONG_PRESS_DELAY) { - // Reset so we dont trigger the menu again next time. - isTouching = false; - this.onLongClick(view); - return true; - } if (holdTime < LONG_PRESS_DELAY && motionEvent.getAction() == MotionEvent.ACTION_UP) { - isTouching = false; this.onClick(view); return true; } + if(!contextMenuShown && holdTime > LONG_PRESS_DELAY) { + contextMenuShown = true; + this.onLongClick(view); + return true; + } // Drag handlers int intCurrentY = Math.round(motionEvent.getY()); @@ -325,13 +323,15 @@ public boolean onTouch(View view, MotionEvent motionEvent) { boolean hasMoved = (Math.abs(intCurrentX - intStartX) > MOVE_SENSITIVITY) || (Math.abs(intCurrentY - intStartY) > MOVE_SENSITIVITY); if (hasMoved) { + mainActivity.dismissPopup(); + mainActivity.closeContextMenu(); View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view); view.startDrag(null, shadowBuilder, view, 0); view.setVisibility(View.INVISIBLE); return true; } - return true; + return false; } @Override From bea096dcda8a6fb3e2c1e5ad457628a564a368aa Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Fri, 4 May 2018 19:14:15 +0100 Subject: [PATCH 7/7] Fix background item taking touchEvent when popup was open --- app/src/main/java/fr/neamar/kiss/MainActivity.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/fr/neamar/kiss/MainActivity.java b/app/src/main/java/fr/neamar/kiss/MainActivity.java index 3f1fffa2b..6f0948537 100644 --- a/app/src/main/java/fr/neamar/kiss/MainActivity.java +++ b/app/src/main/java/fr/neamar/kiss/MainActivity.java @@ -534,11 +534,11 @@ public void onLauncherButtonClicked(View launcherButton) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { - boolean handled = super.dispatchTouchEvent(ev); - if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (mPopup != null && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { dismissPopup(); + return true; } - return handled; + return super.dispatchTouchEvent(ev); } private void displayClearOnInput() {