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