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