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