DeleteDropTarget.java revision d31df54073fbd80a9db9d277dde8c0e7421ffa14
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.launcher3; 18 19import android.animation.TimeInterpolator; 20import android.animation.ValueAnimator; 21import android.animation.ValueAnimator.AnimatorUpdateListener; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.ResolveInfo; 26import android.content.res.ColorStateList; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.graphics.PointF; 30import android.graphics.Rect; 31import android.graphics.drawable.TransitionDrawable; 32import android.os.AsyncTask; 33import android.util.AttributeSet; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewGroup; 37import android.view.animation.AnimationUtils; 38import android.view.animation.DecelerateInterpolator; 39import android.view.animation.LinearInterpolator; 40 41import com.android.launcher3.compat.LauncherAppsCompat; 42import com.android.launcher3.compat.UserHandleCompat; 43 44import java.util.List; 45import java.util.Set; 46 47public class DeleteDropTarget extends ButtonDropTarget { 48 private static int DELETE_ANIMATION_DURATION = 285; 49 private static int FLING_DELETE_ANIMATION_DURATION = 350; 50 private static float FLING_TO_DELETE_FRICTION = 0.035f; 51 private static int MODE_FLING_DELETE_TO_TRASH = 0; 52 private static int MODE_FLING_DELETE_ALONG_VECTOR = 1; 53 54 private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; 55 56 private ColorStateList mOriginalTextColor; 57 private TransitionDrawable mUninstallDrawable; 58 private TransitionDrawable mRemoveDrawable; 59 private TransitionDrawable mCurrentDrawable; 60 61 private boolean mWaitingForUninstall = false; 62 63 public DeleteDropTarget(Context context, AttributeSet attrs) { 64 this(context, attrs, 0); 65 } 66 67 public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) { 68 super(context, attrs, defStyle); 69 } 70 71 @Override 72 protected void onFinishInflate() { 73 super.onFinishInflate(); 74 75 // Get the drawable 76 mOriginalTextColor = getTextColors(); 77 78 // Get the hover color 79 Resources r = getResources(); 80 mHoverColor = r.getColor(R.color.delete_target_hover_tint); 81 mUninstallDrawable = (TransitionDrawable) 82 r.getDrawable(R.drawable.uninstall_target_selector); 83 mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); 84 85 mRemoveDrawable.setCrossFadeEnabled(true); 86 mUninstallDrawable.setCrossFadeEnabled(true); 87 88 // The current drawable is set to either the remove drawable or the uninstall drawable 89 // and is initially set to the remove drawable, as set in the layout xml. 90 mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); 91 92 // Remove the text in the Phone UI in landscape 93 int orientation = getResources().getConfiguration().orientation; 94 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 95 if (!LauncherAppState.getInstance().isScreenLarge()) { 96 setText(""); 97 } 98 } 99 } 100 101 private boolean isAllAppsApplication(DragSource source, Object info) { 102 return source.supportsAppInfoDropTarget() && (info instanceof AppInfo); 103 } 104 private boolean isAllAppsWidget(DragSource source, Object info) { 105 if (source instanceof AppsCustomizePagedView) { 106 if (info instanceof PendingAddItemInfo) { 107 PendingAddItemInfo addInfo = (PendingAddItemInfo) info; 108 switch (addInfo.itemType) { 109 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 110 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: 111 return true; 112 } 113 } 114 } 115 return false; 116 } 117 private boolean isDragSourceWorkspaceOrFolder(DragObject d) { 118 return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder); 119 } 120 private boolean isWorkspaceOrFolderApplication(DragObject d) { 121 return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo); 122 } 123 private boolean isWorkspaceOrFolderWidget(DragObject d) { 124 return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo); 125 } 126 private boolean isWorkspaceFolder(DragObject d) { 127 return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo); 128 } 129 130 private void setHoverColor() { 131 mCurrentDrawable.startTransition(mTransitionDuration); 132 setTextColor(mHoverColor); 133 } 134 private void resetHoverColor() { 135 mCurrentDrawable.resetTransition(); 136 setTextColor(mOriginalTextColor); 137 } 138 139 @Override 140 public boolean acceptDrop(DragObject d) { 141 return willAcceptDrop(d.dragInfo); 142 } 143 144 public static boolean willAcceptDrop(Object info) { 145 if (info instanceof ItemInfo) { 146 ItemInfo item = (ItemInfo) info; 147 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || 148 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { 149 return true; 150 } 151 152 if (!LauncherAppState.isDisableAllApps() && 153 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 154 return true; 155 } 156 157 if (!LauncherAppState.isDisableAllApps() && 158 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && 159 item instanceof AppInfo) { 160 AppInfo appInfo = (AppInfo) info; 161 return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; 162 } 163 164 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && 165 item instanceof ShortcutInfo) { 166 if (LauncherAppState.isDisableAllApps()) { 167 ShortcutInfo shortcutInfo = (ShortcutInfo) info; 168 return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; 169 } else { 170 return true; 171 } 172 } 173 } 174 return false; 175 } 176 177 @Override 178 public void onDragStart(DragSource source, Object info, int dragAction) { 179 boolean isVisible = true; 180 boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() && 181 isAllAppsApplication(source, info); 182 boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget(); 183 184 // If we are dragging an application from AppsCustomize, only show the control if we can 185 // delete the app (it was downloaded), and rename the string to "uninstall" in such a case. 186 // Hide the delete target if it is a widget from AppsCustomize. 187 if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) { 188 isVisible = false; 189 } 190 191 if (useUninstallLabel) { 192 setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); 193 } else if (useDeleteLabel) { 194 setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); 195 } else { 196 isVisible = false; 197 } 198 mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); 199 200 mActive = isVisible; 201 resetHoverColor(); 202 ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); 203 if (isVisible && getText().length() > 0) { 204 setText(useUninstallLabel ? R.string.delete_target_uninstall_label 205 : R.string.delete_target_label); 206 } 207 } 208 209 @Override 210 public void onDragEnd() { 211 super.onDragEnd(); 212 mActive = false; 213 } 214 215 public void onDragEnter(DragObject d) { 216 super.onDragEnter(d); 217 218 setHoverColor(); 219 } 220 221 public void onDragExit(DragObject d) { 222 super.onDragExit(d); 223 224 if (!d.dragComplete) { 225 resetHoverColor(); 226 } else { 227 // Restore the hover color if we are deleting 228 d.dragView.setColor(mHoverColor); 229 } 230 } 231 232 private void animateToTrashAndCompleteDrop(final DragObject d) { 233 final DragLayer dragLayer = mLauncher.getDragLayer(); 234 final Rect from = new Rect(); 235 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 236 final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), 237 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); 238 final float scale = (float) to.width() / from.width(); 239 240 mSearchDropTargetBar.deferOnDragEnd(); 241 deferCompleteDropIfUninstalling(d); 242 243 Runnable onAnimationEndRunnable = new Runnable() { 244 @Override 245 public void run() { 246 completeDrop(d); 247 mSearchDropTargetBar.onDragEnd(); 248 mLauncher.exitSpringLoadedDragMode(); 249 } 250 }; 251 dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, 252 DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), 253 new LinearInterpolator(), onAnimationEndRunnable, 254 DragLayer.ANIMATION_END_DISAPPEAR, null); 255 } 256 257 private void deferCompleteDropIfUninstalling(DragObject d) { 258 mWaitingForUninstall = false; 259 if (isUninstallFromWorkspace(d)) { 260 if (d.dragSource instanceof Folder) { 261 ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity(); 262 } else if (d.dragSource instanceof Workspace) { 263 ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity(); 264 } 265 mWaitingForUninstall = true; 266 } 267 } 268 269 private boolean isUninstallFromWorkspace(DragObject d) { 270 if (LauncherAppState.isDisableAllApps() && isWorkspaceOrFolderApplication(d)) { 271 ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo; 272 // Only allow manifest shortcuts to initiate an un-install. 273 return !InstallShortcutReceiver.isValidShortcutLaunchIntent(shortcut.intent); 274 } 275 return false; 276 } 277 278 private void completeDrop(DragObject d) { 279 ItemInfo item = (ItemInfo) d.dragInfo; 280 boolean wasWaitingForUninstall = mWaitingForUninstall; 281 mWaitingForUninstall = false; 282 if (isAllAppsApplication(d.dragSource, item)) { 283 // Uninstall the application if it is being dragged from AppsCustomize 284 AppInfo appInfo = (AppInfo) item; 285 mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags, 286 appInfo.user); 287 } else if (isUninstallFromWorkspace(d)) { 288 ShortcutInfo shortcut = (ShortcutInfo) item; 289 if (shortcut.intent != null && shortcut.intent.getComponent() != null) { 290 final ComponentName componentName = shortcut.intent.getComponent(); 291 final DragSource dragSource = d.dragSource; 292 final UserHandleCompat user = shortcut.user; 293 mWaitingForUninstall = mLauncher.startApplicationUninstallActivity( 294 componentName, shortcut.flags, user); 295 if (mWaitingForUninstall) { 296 final Runnable checkIfUninstallWasSuccess = new Runnable() { 297 @Override 298 public void run() { 299 mWaitingForUninstall = false; 300 String packageName = componentName.getPackageName(); 301 boolean uninstallSuccessful = !AllAppsList.packageHasActivities( 302 getContext(), packageName, user); 303 if (dragSource instanceof Folder) { 304 ((Folder) dragSource). 305 onUninstallActivityReturned(uninstallSuccessful); 306 } else if (dragSource instanceof Workspace) { 307 ((Workspace) dragSource). 308 onUninstallActivityReturned(uninstallSuccessful); 309 } 310 } 311 }; 312 mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); 313 } 314 } 315 } else if (isWorkspaceOrFolderApplication(d)) { 316 LauncherModel.deleteItemFromDatabase(mLauncher, item); 317 } else if (isWorkspaceFolder(d)) { 318 // Remove the folder from the workspace and delete the contents from launcher model 319 FolderInfo folderInfo = (FolderInfo) item; 320 mLauncher.removeFolder(folderInfo); 321 LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo); 322 } else if (isWorkspaceOrFolderWidget(d)) { 323 // Remove the widget from the workspace 324 mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); 325 LauncherModel.deleteItemFromDatabase(mLauncher, item); 326 327 final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; 328 final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); 329 if (appWidgetHost != null) { 330 // Deleting an app widget ID is a void call but writes to disk before returning 331 // to the caller... 332 new AsyncTask<Void, Void, Void>() { 333 public Void doInBackground(Void ... args) { 334 appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); 335 return null; 336 } 337 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 338 } 339 } 340 if (wasWaitingForUninstall && !mWaitingForUninstall) { 341 if (d.dragSource instanceof Folder) { 342 ((Folder) d.dragSource).onUninstallActivityReturned(false); 343 } else if (d.dragSource instanceof Workspace) { 344 ((Workspace) d.dragSource).onUninstallActivityReturned(false); 345 } 346 } 347 } 348 349 public void onDrop(DragObject d) { 350 animateToTrashAndCompleteDrop(d); 351 } 352 353 /** 354 * Creates an animation from the current drag view to the delete trash icon. 355 */ 356 private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, 357 DragObject d, PointF vel, ViewConfiguration config) { 358 final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), 359 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); 360 final Rect from = new Rect(); 361 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 362 363 // Calculate how far along the velocity vector we should put the intermediate point on 364 // the bezier curve 365 float velocity = Math.abs(vel.length()); 366 float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); 367 int offsetY = (int) (-from.top * vp); 368 int offsetX = (int) (offsetY / (vel.y / vel.x)); 369 final float y2 = from.top + offsetY; // intermediate t/l 370 final float x2 = from.left + offsetX; 371 final float x1 = from.left; // drag view t/l 372 final float y1 = from.top; 373 final float x3 = to.left; // delete target t/l 374 final float y3 = to.top; 375 376 final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { 377 @Override 378 public float getInterpolation(float t) { 379 return t * t * t * t * t * t * t * t; 380 } 381 }; 382 return new AnimatorUpdateListener() { 383 @Override 384 public void onAnimationUpdate(ValueAnimator animation) { 385 final DragView dragView = (DragView) dragLayer.getAnimatedView(); 386 float t = ((Float) animation.getAnimatedValue()).floatValue(); 387 float tp = scaleAlphaInterpolator.getInterpolation(t); 388 float initialScale = dragView.getInitialScale(); 389 float finalAlpha = 0.5f; 390 float scale = dragView.getScaleX(); 391 float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; 392 float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; 393 float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + 394 (t * t) * x3; 395 float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + 396 (t * t) * y3; 397 398 dragView.setTranslationX(x); 399 dragView.setTranslationY(y); 400 dragView.setScaleX(initialScale * (1f - tp)); 401 dragView.setScaleY(initialScale * (1f - tp)); 402 dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); 403 } 404 }; 405 } 406 407 /** 408 * Creates an animation from the current drag view along its current velocity vector. 409 * For this animation, the alpha runs for a fixed duration and we update the position 410 * progressively. 411 */ 412 private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { 413 private DragLayer mDragLayer; 414 private PointF mVelocity; 415 private Rect mFrom; 416 private long mPrevTime; 417 private boolean mHasOffsetForScale; 418 private float mFriction; 419 420 private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); 421 422 public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, 423 long startTime, float friction) { 424 mDragLayer = dragLayer; 425 mVelocity = vel; 426 mFrom = from; 427 mPrevTime = startTime; 428 mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction); 429 } 430 431 @Override 432 public void onAnimationUpdate(ValueAnimator animation) { 433 final DragView dragView = (DragView) mDragLayer.getAnimatedView(); 434 float t = ((Float) animation.getAnimatedValue()).floatValue(); 435 long curTime = AnimationUtils.currentAnimationTimeMillis(); 436 437 if (!mHasOffsetForScale) { 438 mHasOffsetForScale = true; 439 float scale = dragView.getScaleX(); 440 float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; 441 float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; 442 443 mFrom.left += xOffset; 444 mFrom.top += yOffset; 445 } 446 447 mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); 448 mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); 449 450 dragView.setTranslationX(mFrom.left); 451 dragView.setTranslationY(mFrom.top); 452 dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); 453 454 mVelocity.x *= mFriction; 455 mVelocity.y *= mFriction; 456 mPrevTime = curTime; 457 } 458 }; 459 private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, 460 DragObject d, PointF vel, final long startTime, final int duration, 461 ViewConfiguration config) { 462 final Rect from = new Rect(); 463 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 464 465 return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime, 466 FLING_TO_DELETE_FRICTION); 467 } 468 469 public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { 470 final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; 471 472 // Don't highlight the icon as it's animating 473 d.dragView.setColor(0); 474 d.dragView.updateInitialScaleToCurrentScale(); 475 // Don't highlight the target if we are flinging from AllApps 476 if (isAllApps) { 477 resetHoverColor(); 478 } 479 480 if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { 481 // Defer animating out the drop target if we are animating to it 482 mSearchDropTargetBar.deferOnDragEnd(); 483 mSearchDropTargetBar.finishAnimations(); 484 } 485 486 final ViewConfiguration config = ViewConfiguration.get(mLauncher); 487 final DragLayer dragLayer = mLauncher.getDragLayer(); 488 final int duration = FLING_DELETE_ANIMATION_DURATION; 489 final long startTime = AnimationUtils.currentAnimationTimeMillis(); 490 491 // NOTE: Because it takes time for the first frame of animation to actually be 492 // called and we expect the animation to be a continuation of the fling, we have 493 // to account for the time that has elapsed since the fling finished. And since 494 // we don't have a startDelay, we will always get call to update when we call 495 // start() (which we want to ignore). 496 final TimeInterpolator tInterpolator = new TimeInterpolator() { 497 private int mCount = -1; 498 private float mOffset = 0f; 499 500 @Override 501 public float getInterpolation(float t) { 502 if (mCount < 0) { 503 mCount++; 504 } else if (mCount == 0) { 505 mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - 506 startTime) / duration); 507 mCount++; 508 } 509 return Math.min(1f, mOffset + t); 510 } 511 }; 512 AnimatorUpdateListener updateCb = null; 513 if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { 514 updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); 515 } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { 516 updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, 517 duration, config); 518 } 519 deferCompleteDropIfUninstalling(d); 520 521 Runnable onAnimationEndRunnable = new Runnable() { 522 @Override 523 public void run() { 524 // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up 525 // itself, otherwise, complete the drop to initiate the deletion process 526 if (!isAllApps) { 527 mLauncher.exitSpringLoadedDragMode(); 528 completeDrop(d); 529 } 530 mLauncher.getDragController().onDeferredEndFling(d); 531 } 532 }; 533 dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, 534 DragLayer.ANIMATION_END_DISAPPEAR, null); 535 } 536} 537