1616import android .view .View ;
1717import android .view .ViewGroup ;
1818import android .widget .ImageView ;
19+ import android .widget .LinearLayout ;
1920
2021import androidx .annotation .NonNull ;
2122
@@ -63,13 +64,12 @@ private static class ViewHolder {
6364 * Globals for drag and drop support
6465 */
6566 private static long startTime = 0 ; // Start of the drag and drop, used for long press menu
66- private float currentX = 0.0f ; // Current X position of the drag op, this is 0 on DRAG END so we keep a copy here
67- private ViewHolder overApp ; // the view for the DRAG_END event is typically wrong, so we store a reference of the last dragged over app.
6867
6968 // Use so we don't over process on the drag events.
7069 private boolean mDragEnabled = true ;
7170 private boolean isDragging = false ;
7271 private boolean contextMenuShown = false ;
72+ private int potentialNewIndex = -1 ;
7373 private int favCount = -1 ;
7474
7575 private SharedPreferences notificationPrefs = null ;
@@ -137,18 +137,18 @@ void onFavoriteChange() {
137137 holders .add (viewHolder );
138138
139139 // We don't have enough data in our current ViewHolder, add a new one
140- if (i >= currentFavCount ) {
140+ if (i >= currentFavCount ) {
141141 favoritesBar .addView (viewHolder .view );
142- }
143- else {
142+ } else {
144143 // Check if view is different
145144 View currentView = favoritesBar .getChildAt (i );
146- if (currentView != viewHolder .view ) {
147- if (viewHolder .view .getParent () != null ) {
145+ if (currentView != viewHolder .view ) {
146+ if (viewHolder .view .getParent () != null ) {
147+ // We need to remove the view from its parent first
148148 ((ViewGroup ) viewHolder .view .getParent ()).removeView (viewHolder .view );
149149 }
150150 favoritesBar .addView (viewHolder .view , i );
151- };
151+ }
152152 }
153153
154154 if (notificationPrefs != null ) {
@@ -169,7 +169,10 @@ void onFavoriteChange() {
169169 }
170170
171171 // Remove any leftover views from previous renders
172- for (int i = favCount ; i < favoritesBar .getChildCount (); i ++) {
172+ for (int i = favCount ; i < favoritesBar .getChildCount (); i ++) {
173+ View toBeDisposed = favoritesBar .getChildAt (i );
174+ toBeDisposed .setOnDragListener (null );
175+ toBeDisposed .setOnTouchListener (null );
173176 favoritesBar .removeViewAt (i );
174177 }
175178
@@ -194,73 +197,6 @@ void onDataSetChanged() {
194197 }
195198 }
196199
197- /**
198- * On first run, fill the favorite bar with sensible defaults
199- */
200- private void addDefaultAppsToFavs () {
201- {
202- // Default phone call app
203- Intent phoneIntent = new Intent (Intent .ACTION_DIAL );
204- phoneIntent .setData (Uri .parse ("tel:0000" ));
205- ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (phoneIntent , PackageManager .MATCH_DEFAULT_ONLY );
206- if (resolveInfo != null ) {
207- String packageName = resolveInfo .activityInfo .packageName ;
208- Log .i (TAG , "Dialer resolves to:" + packageName + "/" + resolveInfo .activityInfo .name );
209-
210- if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
211- String activityName = resolveInfo .activityInfo .name ;
212- if (packageName .equals ("com.google.android.dialer" )) {
213- // Default dialer has two different activities, one when calling a phone number and one when opening the app from the launcher.
214- // (com.google.android.apps.dialer.extensions.GoogleDialtactsActivity vs. com.google.android.dialer.extensions.GoogleDialtactsActivity)
215- // (notice the .apps. in the middle)
216- // The phoneIntent above resolve to the former, which isn't exposed as a Launcher activity and thus can't be displayed as a favorite
217- // This hack uses the correct resolver when the application id is the default dialer.
218- // In terms of maintenance, if Android was to change the name of the phone's main resolver, the favorite would simply not appear
219- // and we would have to update the String below to the new default resolver
220- activityName = "com.google.android.dialer.extensions.GoogleDialtactsActivity" ;
221- }
222- KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + activityName );
223- }
224- }
225- }
226- {
227- // Default contacts app
228- Intent contactsIntent = new Intent (Intent .ACTION_DEFAULT , ContactsContract .Contacts .CONTENT_URI );
229- ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (contactsIntent , PackageManager .MATCH_DEFAULT_ONLY );
230- if (resolveInfo != null ) {
231- String packageName = resolveInfo .activityInfo .packageName ;
232- Log .i (TAG , "Contacts resolves to:" + packageName );
233- if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
234- KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + resolveInfo .activityInfo .name );
235- }
236- }
237-
238- }
239- {
240- // Default browser
241- Intent browserIntent = new Intent ("android.intent.action.VIEW" , Uri .parse ("http://" ));
242- ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (browserIntent , PackageManager .MATCH_DEFAULT_ONLY );
243- if (resolveInfo != null ) {
244- String packageName = resolveInfo .activityInfo .packageName ;
245- Log .i (TAG , "Browser resolves to:" + packageName );
246-
247- if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
248- String activityName = resolveInfo .activityInfo .name ;
249- if (packageName .equalsIgnoreCase ("com.android.chrome" )) {
250- // Chrome has two different activities, one for Launcher and one when opening an URL.
251- // The browserIntent above resolve to the latter, which isn't exposed as a Launcher activity and thus can't be displayed
252- // This hack uses the correct resolver when the application is Chrome.
253- // In terms of maintenance, if Chrome was to change the name of the main resolver, the favorite would simply not appear
254- // and we would have to update the String below to the new default resolver
255- activityName = "com.google.android.apps.chrome.Main" ;
256- }
257- KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + activityName );
258- }
259- }
260- }
261- mainActivity .onFavoriteChange ();
262- }
263-
264200 @ Override
265201 public void onClick (View v ) {
266202 ViewHolder viewHolder = (ViewHolder ) v .getTag ();
@@ -317,7 +253,7 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
317253 int MOVE_SENSITIVITY = 8 ;
318254 boolean hasMoved = (Math .abs (intCurrentX - intStartX ) > MOVE_SENSITIVITY ) || (Math .abs (intCurrentY - intStartY ) > MOVE_SENSITIVITY );
319255
320- if (hasMoved && mDragEnabled ) {
256+ if (hasMoved && mDragEnabled && ! isDragging ) {
321257 view .performHapticFeedback (HapticFeedbackConstants .LONG_PRESS );
322258
323259 if (contextMenuShown ) {
@@ -327,10 +263,13 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
327263 mDragEnabled = false ;
328264 mainActivity .dismissPopup ();
329265 mainActivity .closeContextMenu ();
266+
267+ mainActivity .favoritesBar .setOnDragListener (this );
330268 View .DragShadowBuilder shadowBuilder = new View .DragShadowBuilder (view );
331- view .startDrag (null , shadowBuilder , view , 0 );
332269 view .setVisibility (View .INVISIBLE );
333270 isDragging = true ;
271+ view .startDrag (null , shadowBuilder , view , 0 );
272+ Log .e ("WTF" , "Starting drag of " + ((ViewHolder ) view .getTag ()).pojo .id );
334273 return true ;
335274 } else if (!contextMenuShown && !isDragging ) {
336275 view .performHapticFeedback (HapticFeedbackConstants .LONG_PRESS );
@@ -345,71 +284,139 @@ public boolean onTouch(View view, MotionEvent motionEvent) {
345284 }
346285
347286 @ Override
348- public boolean onDrag (View v , final DragEvent event ) {
287+ public boolean onDrag (View targetView , final DragEvent event ) {
288+ final View draggedView = (View ) event .getLocalState ();
349289
290+ String [] actions = new String []{"" , "started" , "location" , "drop" , "ended" , "entered" , "exited" };
291+ Log .e ("WTF" , "Drag " + actions [event .getAction ()] + " / " + targetView .toString ());
350292 switch (event .getAction ()) {
351293 case DragEvent .ACTION_DRAG_STARTED :
352- // Inform the system that we are interested in being a potential drop target
353- return true ;
294+ return targetView instanceof LinearLayout ;
354295 case DragEvent .ACTION_DRAG_ENTERED :
355- case DragEvent .ACTION_DRAG_EXITED :
356- case DragEvent .ACTION_DROP :
357- if (!isDragging ) {
358- return true ;
296+ return isDragging ;
297+ case DragEvent .ACTION_DRAG_LOCATION :
298+ ViewGroup bar = ((ViewGroup ) targetView );
299+ float x = event .getX ();
300+ int width = targetView .getWidth ();
301+
302+ int currentPos = (int ) (favCount * x / width );
303+
304+ View currentChildAtPos = bar .getChildAt (currentPos );
305+ if (currentChildAtPos != draggedView ) {
306+ bar .removeView (draggedView );
307+ try {
308+ bar .addView (draggedView , currentPos );
309+ }
310+ catch (IllegalStateException e ) {
311+ // In some situations,
312+ // removeView() somehow fails (this especially happens if you start the drag and immediately moves to the left or right)
313+ // and we can't add the children back, because it still has a parent. In this case, do nothing, this should fix itself on the next iteration.
314+ potentialNewIndex = -1 ;
315+ return false ;
316+ }
359317 }
360- overApp = (ViewHolder ) v .getTag ();
361- currentX = (event .getX () != 0.0f ) ? event .getX () : currentX ;
362318
363- break ;
319+ potentialNewIndex = currentPos ;
364320
321+ return true ;
322+ case DragEvent .ACTION_DROP :
323+ // Accept the drop, will be followed by ACTION_DRAG_ENDED
324+ return isDragging ;
365325 case DragEvent .ACTION_DRAG_ENDED :
366- // Only need to handle this action once.
367- if (!isDragging ) {
368- return true ;
326+ // Sometimes we don't trigger onDrag over another app, in which case just drop.
327+ if (potentialNewIndex == -1 ) {
328+ Log .w (TAG , "Wasn't dragged over a favorite, returning app to starting position" );
329+ } else {
330+ final ViewHolder draggedApp = (ViewHolder ) draggedView .getTag ();
331+ int newIndex = potentialNewIndex ;
332+ draggedView .post (() -> {
333+ // Signals to a View that the drag and drop operation has concluded.
334+ // If event result is set, this means the dragged view was dropped in target
335+ if (event .getResult ()) {
336+ KissApplication .getApplication (mainActivity ).getDataHandler ().setFavoritePosition (mainActivity , draggedApp .result .getPojoId (), newIndex );
337+ mainActivity .onFavoriteChange ();
338+ }
339+ });
369340 }
370- isDragging = false ;
371341
372342 // Reset dragging to what it should be
343+ draggedView .setVisibility (View .VISIBLE );
373344 mDragEnabled = favCount > 1 ;
345+ potentialNewIndex = -1 ;
346+ isDragging = false ;
347+ return true ;
348+ default :
349+ break ;
350+ }
351+ return isDragging ;
352+ }
374353
375- final View draggedView = (View ) event .getLocalState ();
376-
377- // Sometimes we don't trigger onDrag over another app, in which case just drop.
378- if (overApp == null ) {
379- Log .w (TAG , "Wasn't dragged over an app, returning app to starting position" );
380- draggedView .post (() -> draggedView .setVisibility (View .VISIBLE ));
381- break ;
382- }
383-
384- final ViewHolder draggedApp = (ViewHolder ) draggedView .getTag ();
385-
386- int left = v .getLeft ();
387- int right = v .getRight ();
388- int width = right - left ;
389354
390- // currentX is relative to the view not the screen, so add the current X of the view.
391- final boolean leftSide = (left + currentX < left + (width / 2 ));
355+ /**
356+ * On first run, fill the favorite bar with sensible defaults
357+ */
358+ private void addDefaultAppsToFavs () {
359+ {
360+ // Default phone call app
361+ Intent phoneIntent = new Intent (Intent .ACTION_DIAL );
362+ phoneIntent .setData (Uri .parse ("tel:0000" ));
363+ ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (phoneIntent , PackageManager .MATCH_DEFAULT_ONLY );
364+ if (resolveInfo != null ) {
365+ String packageName = resolveInfo .activityInfo .packageName ;
366+ Log .i (TAG , "Dialer resolves to:" + packageName + "/" + resolveInfo .activityInfo .name );
392367
393- final int pos = KissApplication .getApplication (mainActivity ).getDataHandler ().getFavoritePosition (overApp .result .getPojoId ());
368+ if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
369+ String activityName = resolveInfo .activityInfo .name ;
370+ if (packageName .equals ("com.google.android.dialer" )) {
371+ // Default dialer has two different activities, one when calling a phone number and one when opening the app from the launcher.
372+ // (com.google.android.apps.dialer.extensions.GoogleDialtactsActivity vs. com.google.android.dialer.extensions.GoogleDialtactsActivity)
373+ // (notice the .apps. in the middle)
374+ // The phoneIntent above resolve to the former, which isn't exposed as a Launcher activity and thus can't be displayed as a favorite
375+ // This hack uses the correct resolver when the application id is the default dialer.
376+ // In terms of maintenance, if Android was to change the name of the phone's main resolver, the favorite would simply not appear
377+ // and we would have to update the String below to the new default resolver
378+ activityName = "com.google.android.dialer.extensions.GoogleDialtactsActivity" ;
379+ }
380+ KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + activityName );
381+ }
382+ }
383+ }
384+ {
385+ // Default contacts app
386+ Intent contactsIntent = new Intent (Intent .ACTION_DEFAULT , ContactsContract .Contacts .CONTENT_URI );
387+ ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (contactsIntent , PackageManager .MATCH_DEFAULT_ONLY );
388+ if (resolveInfo != null ) {
389+ String packageName = resolveInfo .activityInfo .packageName ;
390+ Log .i (TAG , "Contacts resolves to:" + packageName );
391+ if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
392+ KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + resolveInfo .activityInfo .name );
393+ }
394+ }
394395
395- draggedView .post (() -> {
396- // Signals to a View that the drag and drop operation has concluded.
397- // If event result is set, this means the dragged view was dropped in target
398- if (event .getResult ()) {
399- KissApplication .getApplication (mainActivity ).getDataHandler ().setFavoritePosition (mainActivity , draggedApp .result .getPojoId (), leftSide ? pos - 1 : pos );
400- draggedView .post (() -> draggedView .setVisibility (View .VISIBLE ));
396+ }
397+ {
398+ // Default browser
399+ Intent browserIntent = new Intent ("android.intent.action.VIEW" , Uri .parse ("http://" ));
400+ ResolveInfo resolveInfo = mainActivity .getPackageManager ().resolveActivity (browserIntent , PackageManager .MATCH_DEFAULT_ONLY );
401+ if (resolveInfo != null ) {
402+ String packageName = resolveInfo .activityInfo .packageName ;
403+ Log .i (TAG , "Browser resolves to:" + packageName );
401404
402- mainActivity .onFavoriteChange ();
403- } else {
404- draggedView .setVisibility (View .VISIBLE );
405+ if (resolveInfo .activityInfo .name != null && !resolveInfo .activityInfo .name .equals (DEFAULT_RESOLVER )) {
406+ String activityName = resolveInfo .activityInfo .name ;
407+ if (packageName .equalsIgnoreCase ("com.android.chrome" )) {
408+ // Chrome has two different activities, one for Launcher and one when opening an URL.
409+ // The browserIntent above resolve to the latter, which isn't exposed as a Launcher activity and thus can't be displayed
410+ // This hack uses the correct resolver when the application is Chrome.
411+ // In terms of maintenance, if Chrome was to change the name of the main resolver, the favorite would simply not appear
412+ // and we would have to update the String below to the new default resolver
413+ activityName = "com.google.android.apps.chrome.Main" ;
405414 }
406- });
407-
408- break ;
409- default :
410- break ;
415+ KissApplication .getApplication (mainActivity ).getDataHandler ().addToFavorites ("app://" + packageName + "/" + activityName );
416+ }
417+ }
411418 }
412- return true ;
419+ mainActivity . onFavoriteChange () ;
413420 }
414421}
415422
0 commit comments