diff --git a/app/src/main/java/fr/neamar/kiss/DataHandler.java b/app/src/main/java/fr/neamar/kiss/DataHandler.java index 00236640b..5388925a7 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,52 @@ 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"); + 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/MainActivity.java b/app/src/main/java/fr/neamar/kiss/MainActivity.java index 047286393..02bf05536 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; @@ -276,6 +277,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 @@ -360,9 +373,7 @@ protected void onResume() { return; } - if (mPopup != null) { - mPopup.dismiss(); - } + dismissPopup(); if (KissApplication.getApplication(this).getDataHandler().allProvidersHaveLoaded) { displayLoader(false); @@ -531,24 +542,9 @@ 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; - } + if (mPopup != null && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + dismissPopup(); + return true; } return super.dispatchTouchEvent(ev); } @@ -675,9 +671,7 @@ public void updateSearchRecords() { */ private void updateSearchRecords(String query) { resetTask(); - - if (mPopup != null) - mPopup.dismiss(); + dismissPopup(); forwarderManager.updateSearchRecords(query); @@ -722,8 +716,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() { @@ -764,8 +757,7 @@ public void hideKeyboard() { } systemUiVisibilityHelper.onKeyboardVisibilityChanged(false); - if (mPopup != null) - mPopup.dismiss(); + dismissPopup(); } @Override @@ -795,4 +787,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 0e2864b48..f2ee78b33 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,9 @@ import android.net.Uri; 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; @@ -25,7 +28,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 +56,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 = 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 contextMenuShown = false; + Favorites(MainActivity mainActivity) { super(mainActivity); } @@ -73,8 +93,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,52 +244,169 @@ 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); + v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); + } @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); + v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); return true; } private boolean isExternalFavoriteBarEnabled() { return prefs.getBoolean("enable-favorites-bar", true); } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + startTime = motionEvent.getEventTime(); + contextMenuShown = false; + return true; + } + // No need to do the extra work + if(isDragging) { + return true; + } + + // Click handlers first + long holdTime = motionEvent.getEventTime() - startTime; + if (holdTime < LONG_PRESS_DELAY && motionEvent.getAction() == MotionEvent.ACTION_UP) { + 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()); + 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) { + mainActivity.dismissPopup(); + mainActivity.closeContextMenu(); + View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view); + view.startDrag(null, shadowBuilder, view, 0); + view.setVisibility(View.INVISIBLE); + return true; + } + + return false; + } + + @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(); + + // 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); + + 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; + } } + 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 @@