/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import android.text.InputType;
import android.text.Selection;
import android.text.Spannable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.ActionMode;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.launcher3.CellLayout.CellInfo;
import com.android.launcher3.DragController.DragListener;
import com.android.launcher3.FolderInfo.FolderListener;
import com.android.launcher3.UninstallDropTarget.UninstallSource;
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
import com.android.launcher3.settings.SettingsProvider;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.UiThreadCircularReveal;
import static cyanogenmod.content.Intent.ACTION_PROTECTED;
import static cyanogenmod.content.Intent.EXTRA_PROTECTED_COMPONENTS;
import static cyanogenmod.content.Intent.EXTRA_PROTECTED_STATE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Represents a set of icons chosen by the user or generated by the system.
*/
public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource,
Stats.LaunchSourceProvider {
private static final String TAG = "Launcher.Folder";
/**
* We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
* results in CellLayout being measured as UNSPECIFIED, which it does not support.
*/
private static final int MIN_CONTENT_DIMEN = 5;
static final int STATE_NONE = -1;
static final int STATE_SMALL = 0;
static final int STATE_ANIMATING = 1;
static final int STATE_OPEN = 2;
/**
* Time for which the scroll hint is shown before automatically changing page.
*/
public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
private static final int CLOSE_FOLDER_DELAY_MS = 150;
private static final int ALPHA_DELAY_MULT = 15;
/**
* Fraction of icon width which behave as scroll region.
*/
private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
private static final int REORDER_ANIMATION_DURATION = 230;
private static final int REORDER_DELAY = 250;
private static final int ON_EXIT_CLOSE_DELAY = 400;
private static final Rect sTempRect = new Rect();
private static String sDefaultFolderName;
private static String sHintText;
private final Alarm mReorderAlarm = new Alarm();
private final Alarm mOnExitAlarm = new Alarm();
private final Alarm mOnScrollHintAlarm = new Alarm();
@Thunk final Alarm mScrollPauseAlarm = new Alarm();
@Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
private final int mExpandDuration;
private final int mMaterialExpandDuration;
private final int mMaterialExpandStagger;
private final InputMethodManager mInputMethodManager;
private final PowerManager mPowerManager;
protected final Launcher mLauncher;
protected DragController mDragController;
protected FolderInfo mInfo;
@Thunk FolderIcon mFolderIcon;
@Thunk FolderPagedView mContent;
@Thunk View mContentWrapper;
ExtendedEditText mFolderName;
private View mFooter;
// Cell ranks used for drag and drop
@Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
@Thunk int mState = STATE_NONE;
private boolean mRearrangeOnClose = false;
boolean mItemsInvalidated = false;
private ShortcutInfo mCurrentDragInfo;
private View mCurrentDragView;
private boolean mIsExternalDrag;
boolean mSuppressOnAdd = false;
private boolean mDragInProgress = false;
private boolean mDeleteFolderOnDropCompleted = false;
private boolean mSuppressFolderDeletion = false;
private boolean mItemAddedBackToSelfViaIcon = false;
@Thunk float mFolderIconPivotX;
@Thunk float mFolderIconPivotY;
private boolean mIsEditingName = false;
ImageView mFolderLock;
private boolean mDestroyed;
@Thunk Runnable mDeferredAction;
private boolean mDeferDropAfterUninstall;
private boolean mUninstallSuccessful;
// Folder scrolling
private int mScrollAreaOffset;
private Handler mHandler;
@Thunk int mScrollHintDir = DragController.SCROLL_NONE;
@Thunk int mCurrentScrollDir = DragController.SCROLL_NONE;
/**
* Used to inflate the Workspace from XML.
*
* @param context The application's context.
* @param attrs The attributes set containing the Workspace's customization values.
*/
public Folder(Context context, AttributeSet attrs) {
super(context, attrs);
setAlwaysDrawnWithCacheEnabled(false);
mHandler = new Handler();
mInputMethodManager = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
Resources res = getResources();
mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration);
mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger);
if (sDefaultFolderName == null) {
sDefaultFolderName = res.getString(R.string.folder_name);
}
if (sHintText == null) {
sHintText = res.getString(R.string.folder_hint_text);
}
mLauncher = (Launcher) context;
// We need this view to be focusable in touch mode so that when text editing of the folder
// name is complete, we have something to focus on, thus hiding the cursor and giving
// reliable behavior when clicking the text field (since it will always gain focus on click).
setFocusableInTouchMode(true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentWrapper = findViewById(R.id.folder_content_wrapper);
mContent = (FolderPagedView) findViewById(R.id.folder_content);
mContent.setFolder(this);
mFolderName = (ExtendedEditText) findViewById(R.id.folder_name);
mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() {
@Override
public boolean onBackKey() {
// Close the activity on back key press
doneEditingFolderName(true);
return false;
}
});
mFolderName.setOnFocusChangeListener(this);
// We disable action mode for now since it messes up the view on phones
mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback);
mFolderName.setOnEditorActionListener(this);
mFolderName.setSelectAllOnFocus(true);
mFolderName.setInputType(mFolderName.getInputType() |
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
boolean hideLabels = SettingsProvider.getBoolean(mLauncher,
SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS,
R.bool.preferences_interface_homescreen_hide_icon_labels_default);
if (hideLabels) {
mFolderName.setVisibility(View.GONE);
}
mFolderLock = (ImageView) findViewById(R.id.folder_lock);
mFolderLock.setOnClickListener(this);
mFooter = findViewById(R.id.folder_footer);
}
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onDestroyActionMode(ActionMode mode) {
}
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
};
public void onClick(View v) {
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {
mLauncher.onClick(v);
}
if (v.getId() == R.id.folder_lock) {
startHiddenFolderManager(mInfo.hidden ?
Launcher.REQUEST_UNPROTECT_FOLDER :
Launcher.REQUEST_PROTECT_FOLDER);
}
}
public void startHiddenFolderManager(int action) {
mLauncher.validateLockForHiddenFolders(mFolderIcon, action);
}
public List<Pair<ComponentName, CharSequence>> getComponents() {
int size = mItemsInReadingOrder.size();
List<Pair<ComponentName, CharSequence>> components =
new ArrayList<Pair<ComponentName, CharSequence>>();
for (int i = 0; i < size; i++) {
View v = mItemsInReadingOrder.get(i);
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {
ShortcutInfo shortcut = (ShortcutInfo) tag;
components.add(Pair.create(shortcut.getIntent().getComponent(), shortcut.title));
}
}
return components;
}
public void modifyProtectedApps(boolean protect) {
ArrayList<ComponentName> components = new ArrayList<>();
for (Pair<ComponentName, CharSequence> item : getComponents()) {
if (item.first != null) {
components.add(item.first);
}
}
Intent intent = new Intent();
intent.setAction(ACTION_PROTECTED);
// flip the boolean value to accomodate framework
// in framework "false" is "protected" and "true" is "visible"
intent.putExtra(EXTRA_PROTECTED_STATE, !protect);
intent.putExtra(EXTRA_PROTECTED_COMPONENTS, components);
mLauncher.sendBroadcast(intent);
}
private void removeProtectedApp(ComponentName componentName) {
ArrayList<ComponentName> components = new ArrayList<>();
components.add(componentName);
Intent intent = new Intent();
intent.setAction(ACTION_PROTECTED);
// flip the boolean value to accomodate framework
// in framework "false" is "protected" and "true" is "visible"
intent.putExtra(EXTRA_PROTECTED_STATE, true);
intent.putExtra(EXTRA_PROTECTED_COMPONENTS, components);
mLauncher.sendBroadcast(intent);
}
public void saveHiddenFolderState(boolean protect) {
mInfo.hidden = protect;
modifyProtectedApps(protect);
LauncherModel.updateItemInDatabase(mLauncher, mInfo);
mLauncher.mModel.flushWorkerThread();
rebind(mInfo);
}
public boolean onLongClick(View v) {
// Return if global dragging is not enabled
if (!mLauncher.isDraggingEnabled()) return true;
return beginDrag(v, false);
}
private boolean beginDrag(View v, boolean accessible) {
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {
ShortcutInfo item = (ShortcutInfo) tag;
if (!v.isInTouchMode()) {
return false;
}
mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
mCurrentDragInfo = item;
mEmptyCellRank = item.rank;
mCurrentDragView = v;
mContent.removeItem(mCurrentDragView);
mInfo.remove(mCurrentDragInfo);
mDragInProgress = true;
mItemAddedBackToSelfViaIcon = false;
}
return true;
}
@Override
public void startDrag(CellInfo cellInfo, boolean accessible) {
beginDrag(cellInfo.cell, accessible);
}
@Override
public void enableAccessibleDrag(boolean enable) {
mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable);
for (int i = 0; i < mContent.getChildCount(); i++) {
mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG);
}
mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS :
IMPORTANT_FOR_ACCESSIBILITY_AUTO);
mLauncher.getWorkspace().setAddNewPageOnDrag(!enable);
}
public boolean isEditingName() {
return mIsEditingName;
}
public void startEditingFolderName() {
mFolderName.setHint("");
mIsEditingName = true;
}
public void dismissEditingName() {
mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
doneEditingFolderName(true);
}
public void doneEditingFolderName(boolean commit) {
mFolderName.setHint(sHintText);
// Convert to a string here to ensure that no other state associated with the text field
// gets saved.
String newTitle = mFolderName.getText().toString();
if (!SettingsProvider.getBoolean(mLauncher,
SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS,
R.bool.preferences_interface_homescreen_hide_icon_labels_default)) {
mInfo.setTitle(newTitle);
}
LauncherModel.updateItemInDatabase(mLauncher, mInfo);
if (commit) {
sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
String.format(getContext().getString(R.string.folder_renamed), newTitle));
}
// In order to clear the focus from the text field, we set the focus on ourself. This
// ensures that every time the field is clicked, focus is gained, giving reliable behavior.
requestFocus();
Selection.setSelection((Spannable) mFolderName.getText(), 0, 0);
mIsEditingName = false;
mLauncher.notifyFolderNameChanged();
}
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
dismissEditingName();
return true;
}
return false;
}
public View getEditTextRegion() {
return mFolderName;
}
/**
* We need to handle touch events to prevent them from falling through to the workspace below.
*/
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
return true;
}
public void setDragController(DragController dragController) {
mDragController = dragController;
}
public void setFolderIcon(FolderIcon icon) {
mFolderIcon = icon;
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
// When the folder gets focus, we don't want to announce the list of items.
return true;
}
/**
* @return the FolderInfo object associated with this folder
*/
public FolderInfo getInfo() {
return mInfo;
}
void bind(final FolderInfo info) {
mInfo = info;
ArrayList<ShortcutInfo> children = info.contents;
Collections.sort(children, ITEM_POS_COMPARATOR);
ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
// If our folder has too many items we prune them from the list. This is an issue
// when upgrading from the old Folders implementation which could contain an unlimited
// number of items.
for (ShortcutInfo item: overflow) {
mInfo.remove(item);
LauncherModel.deleteItemFromDatabase(mLauncher, item);
}
mItemsInvalidated = true;
updateTextViewFocus();
mInfo.addListener(this);
setFolderName();
updateLock();
}
public void rebind(final FolderInfo info) {
bind(info);
}
public void setFolderName() {
if (!sDefaultFolderName.contentEquals(mInfo.title)) {
mFolderName.setText(mInfo.title);
} else {
mFolderName.setText("");
}
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon.post(new Runnable() {
public void run() {
if (getItemCount() <= 1) {
replaceFolderWithFinalItem();
}
}
});
}
private void updateLock() {
if (mInfo != null) {
mFolderLock.setImageResource(mInfo.hidden ?
R.drawable.folder_locked :
R.drawable.folder_unlocked);
}
}
/**
* Creates a new UserFolder, inflated from R.layout.user_folder.
*
* @param context The application's context.
* @param root The {@link View} parent of this folder.
*
* @return A new UserFolder.
*/
@SuppressLint("InflateParams")
static Folder fromXml(Launcher launcher, ViewGroup root) {
return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, root, false);
}
/**
* This method is intended to make the UserFolder to be visually identical in size and position
* to its associated FolderIcon. This allows for a seamless transition into the expanded state.
*/
private void positionAndSizeAsIcon() {
if (!(getParent() instanceof DragLayer)) return;
setScaleX(0.8f);
setScaleY(0.8f);
setAlpha(0f);
mState = STATE_SMALL;
}
private void prepareReveal() {
setScaleX(1f);
setScaleY(1f);
setAlpha(1f);
mState = STATE_SMALL;
View reveal = mLauncher.findViewById(R.id.reveal_fake_page_container);
reveal.setVisibility(View.VISIBLE);
View revealFolderIcon = mLauncher.findViewById(R.id.reveal_fake_folder_icon);
revealFolderIcon.setVisibility(View.INVISIBLE);
}
private void prepareFakeFolderIcon() {
mFolderIcon.destroyDrawingCache();
mFolderIcon.buildDrawingCache(true);
Bitmap fakeFolderIcon = Bitmap.createBitmap(mFolderIcon.getDrawingCache());
View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon);
FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams)
fakeFolderIconView.getLayoutParams();
// Get globalVisibleRect of the folderIcon. getWidth and getHeight are inaccurate for
// hotseat icons
Rect rect = new Rect();
mFolderIcon.getGlobalVisibleRect(rect);
flp.height = rect.height();
flp.width = rect.width();
fakeFolderIconView.setLayoutParams(flp);
int [] folderIconXY = new int[2];
mFolderIcon.getLocationOnScreen(folderIconXY);
fakeFolderIconView.setX(folderIconXY[0]);
fakeFolderIconView.setY(folderIconXY[1]);
fakeFolderIconView.setBackground(new BitmapDrawable(null, fakeFolderIcon));
fakeFolderIconView.setVisibility(View.INVISIBLE);
}
public void animateOpen() {
if (!(getParent() instanceof DragLayer)) return;
mContent.completePendingPageChanges();
if (!mDragInProgress) {
// Open on the first page.
mContent.snapToPageImmediately(0);
}
Animator openFolderAnim = null;
final Runnable onCompleteRunnable;
if (!Utilities.ATLEAST_LOLLIPOP) {
positionAndSizeAsIcon();
calculatePivot();
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
final ObjectAnimator oa =
LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
oa.setDuration(mExpandDuration);
openFolderAnim = oa;
setLayerType(LAYER_TYPE_HARDWARE, null);
onCompleteRunnable = new Runnable() {
@Override
public void run() {
setLayerType(LAYER_TYPE_NONE, null);
}
};
} else {
calculatePivot();
AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
int width = getMeasuredWidth();
int height = getMeasuredHeight();
float transX = - 0.075f * (width / 2 - getPivotX());
float transY = - 0.075f * (height / 2 - getPivotY());
setTranslationX(transX);
setTranslationY(transY);
PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0);
PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0);
Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty);
drift.setDuration(mMaterialExpandDuration);
drift.setStartDelay(mMaterialExpandStagger);
drift.setInterpolator(new LogDecelerateInterpolator(60, 0));
int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX());
int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY());
float radius = (float) Math.hypot(rx, ry);
Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
(int) getPivotY(), 0, radius);
reveal.setDuration(mMaterialExpandDuration);
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
mContentWrapper.setAlpha(0f);
Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f);
iconsAlpha.setDuration(mMaterialExpandDuration);
iconsAlpha.setStartDelay(mMaterialExpandStagger);
iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
mFooter.setAlpha(0f);
Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f);
textAlpha.setDuration(mMaterialExpandDuration);
textAlpha.setStartDelay(mMaterialExpandStagger);
textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
prepareFakeFolderIcon();
float iconTransY = getResources().getInteger(R.integer.folder_icon_translate_y_dist);
final View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon);
float baseIconTranslationY = fakeFolderIconView.getTranslationY();
PropertyValuesHolder iconty = PropertyValuesHolder.ofFloat("translationY",
baseIconTranslationY, baseIconTranslationY + iconTransY);
PropertyValuesHolder iconAlpha = PropertyValuesHolder.ofFloat("alpha", 1f, 0f);
Animator fakeFolderIcon = LauncherAnimUtils.ofPropertyValuesHolder(fakeFolderIconView,
iconty, iconAlpha);
fakeFolderIcon.setDuration(mMaterialExpandDuration);
fakeFolderIcon.setInterpolator(new AccelerateInterpolator(1.5f));
fakeFolderIcon.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mFolderIcon.setAlpha(0);
fakeFolderIconView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
fakeFolderIconView.setVisibility(View.INVISIBLE);
}
});
prepareReveal();
anim.play(drift);
anim.play(iconsAlpha);
anim.play(textAlpha);
anim.play(reveal);
openFolderAnim = anim;
mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
mFooter.setLayerType(LAYER_TYPE_HARDWARE, null);
onCompleteRunnable = new Runnable() {
@Override
public void run() {
mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
}
};
}
openFolderAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
mContent.getAccessibilityDescription());
hideWorkspace();
mState = STATE_ANIMATING;
}
@Override
public void onAnimationEnd(Animator animation) {
mState = STATE_OPEN;
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
mContent.setFocusOnFirstChild();
}
});
// Footer animation
if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) {
int footerWidth = mContent.getDesiredWidth()
- mFooter.getPaddingLeft() - mFooter.getPaddingRight();
float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
float translation = (footerWidth - textWidth) / 2;
mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
mContent.setMarkerScale(0);
// Do not update the flag if we are in drag mode. The flag will be updated, when we
// actually drop the icon.
final boolean updateAnimationFlag = !mDragInProgress;
openFolderAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
.translationX(0)
.setInterpolator(Utilities.ATLEAST_LOLLIPOP ?
AnimationUtils.loadInterpolator(mLauncher,
android.R.interpolator.fast_out_slow_in)
: new LogDecelerateInterpolator(100, 0));
mContent.animateMarkers();
if (updateAnimationFlag) {
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
}
}
});
} else {
mFolderName.setTranslationX(0);
mContent.setMarkerScale(1);
}
openFolderAnim.start();
// Make sure the folder picks up the last drag move even if the finger doesn't move.
if (mDragController.isDragging()) {
mDragController.forceTouchMove();
}
FolderPagedView pages = (FolderPagedView) mContent;
pages.verifyVisibleHighResIcons(pages.getNextPage());
}
public void beginExternalDrag(ShortcutInfo item) {
mCurrentDragInfo = item;
mEmptyCellRank = mContent.allocateRankForNewItem(item);
mIsExternalDrag = true;
mDragInProgress = true;
// Since this folder opened by another controller, it might not get onDrop or
// onDropComplete. Perform cleanup once drag-n-drop ends.
mDragController.addDragListener(this);
}
@Override
public void onDragStart(DragSource source, Object info, int dragAction) { }
@Override
public void onDragEnd() {
if (mIsExternalDrag && mDragInProgress) {
completeDragExit();
}
mDragController.removeDragListener(this);
}
@Thunk void sendCustomAccessibilityEvent(int type, String text) {
AccessibilityManager accessibilityManager = (AccessibilityManager)
getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager.isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(type);
onInitializeAccessibilityEvent(event);
event.getText().add(text);
accessibilityManager.sendAccessibilityEvent(event);
}
}
public int getState() {
return mState;
}
public void animateClosed(final boolean animate) {
if (!(getParent() instanceof DragLayer)) return;
AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f);
float transY = getResources().getInteger(R.integer.folder_translate_y_dist);
PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", 0f,
transY);
setLayerType(LAYER_TYPE_HARDWARE, null);
float animatorDurationScale = Settings.Global.getFloat(getContext().getContentResolver(),
Settings.Global.ANIMATOR_DURATION_SCALE, 1);
ObjectAnimator oa;
if (mPowerManager.isPowerSaveMode() || animatorDurationScale < 0.01f) {
// power save mode is no fun - skip alpha animation and just set it to 0
// otherwise the icons will stay around until the duration of the animation
if (animate) {
oa = LauncherAnimUtils.ofPropertyValuesHolder(this, translationY);
} else {
oa = LauncherAnimUtils.ofPropertyValuesHolder(this);
}
setAlpha(0f);
} else {
if (animate) {
oa = LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, translationY);
} else {
oa = LauncherAnimUtils.ofPropertyValuesHolder(this, alpha);
}
}
oa.setDuration(mMaterialExpandDuration);
oa.setInterpolator(new LogDecelerateInterpolator(60, 0));
anim.play(oa);
Animator reverseRevealAnim = null;
Animator fakeFolderIconAnim = null;
if (animate) {
prepareFakeFolderIcon();
float iconTransY = getResources().getInteger(R.integer.folder_icon_translate_y_dist);
final View fakeFolderIconView = mLauncher.findViewById(R.id.reveal_fake_folder_icon);
float baseIconTranslationY = fakeFolderIconView.getTranslationY();
PropertyValuesHolder iconty = PropertyValuesHolder.ofFloat("translationY",
baseIconTranslationY + iconTransY, baseIconTranslationY);
PropertyValuesHolder iconAlpha = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);
fakeFolderIconAnim = LauncherAnimUtils.ofPropertyValuesHolder(fakeFolderIconView,
iconty, iconAlpha);
fakeFolderIconAnim.setDuration(mMaterialExpandDuration);
fakeFolderIconAnim.setInterpolator(new DecelerateInterpolator(2f));
fakeFolderIconAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mFolderIcon.setAlpha(0);
fakeFolderIconView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
fakeFolderIconView.setVisibility(View.INVISIBLE);
mFolderIcon.setAlpha(1);
View revealView = mLauncher.findViewById(R.id.reveal_fake_page_container);
revealView.setVisibility(View.INVISIBLE);
}
});
} else {
View revealView = mLauncher.findViewById(R.id.reveal_fake_page_container);
revealView.setVisibility(View.INVISIBLE);
mFolderIcon.setAlpha(1);
}
if (reverseRevealAnim != null) {
anim.play(reverseRevealAnim);
}
if (fakeFolderIconAnim != null) {
anim.play(fakeFolderIconAnim);
}
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
getContext().getString(R.string.folder_closed));
unHideWorkspace();
mState = STATE_ANIMATING;
}
@Override
public void onAnimationEnd(Animator animation) {
onCloseComplete();
setLayerType(LAYER_TYPE_NONE, null);
mState = STATE_SMALL;
}
});
anim.start();
}
private int mSavedWidgetsVisibilityState = INVISIBLE;
private void hideWorkspace() {
mSavedWidgetsVisibilityState = mLauncher.getWidgetsView().getVisibility();
mLauncher.getWidgetsView().setVisibility(INVISIBLE);
mLauncher.getWorkspace().setVisibility(INVISIBLE);
mLauncher.getHotseat().setVisibility(INVISIBLE);
mLauncher.getSearchDropTargetBar().setVisibility(INVISIBLE);
mLauncher.getPageIndicator().setVisibility(INVISIBLE);
}
private void unHideWorkspace() {
mLauncher.getWidgetsView().setVisibility(mSavedWidgetsVisibilityState);
mLauncher.getWorkspace().setVisibility(VISIBLE);
mLauncher.getHotseat().setVisibility(VISIBLE);
mLauncher.getSearchDropTargetBar().setVisibility(VISIBLE);
mLauncher.getPageIndicator().setVisibility(VISIBLE);
}
public boolean acceptDrop(DragObject d) {
final ItemInfo item = (ItemInfo) d.dragInfo;
final int itemType = item.itemType;
return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
!isFull());
}
public void onDragEnter(DragObject d) {
mPrevTargetRank = -1;
mOnExitAlarm.cancelAlarm();
// Get the area offset such that the folder only closes if half the drag icon width
// is outside the folder area
mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
}
OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
public void onAlarm(Alarm alarm) {
mContent.realTimeReorder(mEmptyCellRank, mTargetRank);
mEmptyCellRank = mTargetRank;
}
};
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public boolean isLayoutRtl() {
return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
}
@Override
public void onDragOver(DragObject d) {
onDragOver(d, REORDER_DELAY);
}
private int getTargetRank(DragObject d, float[] recycle) {
recycle = d.getVisualCenter(recycle);
return mContent.findNearestArea(
(int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop());
}
@Thunk void onDragOver(DragObject d, int reorderDelay) {
if (mScrollPauseAlarm.alarmPending()) {
return;
}
final float[] r = new float[2];
mTargetRank = getTargetRank(d, r);
if (mTargetRank != mPrevTargetRank) {
mReorderAlarm.cancelAlarm();
mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
mReorderAlarm.setAlarm(REORDER_DELAY);
mPrevTargetRank = mTargetRank;
}
float x = r[0];
int currentPage = mContent.getNextPage();
float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
* ICON_OVERSCROLL_WIDTH_FACTOR;
boolean isOutsideLeftEdge = x < cellOverlap;
boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);
if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) {
showScrollHint(DragController.SCROLL_LEFT, d);
} else if (currentPage < (mContent.getPageCount() - 1)
&& (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) {
showScrollHint(DragController.SCROLL_RIGHT, d);
} else {
mOnScrollHintAlarm.cancelAlarm();
if (mScrollHintDir != DragController.SCROLL_NONE) {
mContent.clearScrollHint();
mScrollHintDir = DragController.SCROLL_NONE;
}
}
}
private void showScrollHint(int direction, DragObject d) {
// Show scroll hint on the right
if (mScrollHintDir != direction) {
mContent.showScrollHint(direction);
mScrollHintDir = direction;
}
// Set alarm for when the hint is complete
if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) {
mCurrentScrollDir = direction;
mOnScrollHintAlarm.cancelAlarm();
mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d));
mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION);
mReorderAlarm.cancelAlarm();
mTargetRank = mEmptyCellRank;
}
}
OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
public void onAlarm(Alarm alarm) {
completeDragExit();
}
};
public void completeDragExit() {
if (mInfo.opened) {
mLauncher.closeFolder();
mRearrangeOnClose = true;
} else if (mState == STATE_ANIMATING) {
mRearrangeOnClose = true;
} else {
rearrangeChildren();
clearDragInfo();
}
}
private void clearDragInfo() {
mCurrentDragInfo = null;
mCurrentDragView = null;
mSuppressOnAdd = false;
mIsExternalDrag = false;
}
public void onDragExit(DragObject d) {
// We only close the folder if this is a true drag exit, ie. not because
// a drop has occurred above the folder.
if (!d.dragComplete) {
mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
}
mReorderAlarm.cancelAlarm();
mOnScrollHintAlarm.cancelAlarm();
mScrollPauseAlarm.cancelAlarm();
if (mScrollHintDir != DragController.SCROLL_NONE) {
mContent.clearScrollHint();
mScrollHintDir = DragController.SCROLL_NONE;
}
}
/**
* When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we
* need to complete all transient states based on timers.
*/
@Override
public void prepareAccessibilityDrop() {
if (mReorderAlarm.alarmPending()) {
mReorderAlarm.cancelAlarm();
mReorderAlarmListener.onAlarm(mReorderAlarm);
}
}
public void onDropCompleted(final View target, final DragObject d,
final boolean isFlingToDelete, final boolean success) {
if (mDeferDropAfterUninstall) {
Log.d(TAG, "Deferred handling drop because waiting for uninstall.");
mDeferredAction = new Runnable() {
public void run() {
onDropCompleted(target, d, isFlingToDelete, success);
mDeferredAction = null;
}
};
return;
}
boolean beingCalledAfterUninstall = mDeferredAction != null;
boolean successfulDrop =
success && (!beingCalledAfterUninstall || mUninstallSuccessful);
if (successfulDrop) {
if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
replaceFolderWithFinalItem();
}
} else {
// The drag failed, we need to return the item to the folder
ShortcutInfo info = (ShortcutInfo) d.dragInfo;
View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
? mCurrentDragView : mContent.createNewView(info);
ArrayList<View> views = getItemsInReadingOrder();
views.add(info.rank, icon);
mContent.arrangeChildren(views, views.size());
mItemsInvalidated = true;
mSuppressOnAdd = true;
mFolderIcon.onDrop(d);
mSuppressOnAdd = false;
}
if (target != this) {
if (mOnExitAlarm.alarmPending()) {
mOnExitAlarm.cancelAlarm();
if (!successfulDrop) {
mSuppressFolderDeletion = true;
}
mScrollPauseAlarm.cancelAlarm();
completeDragExit();
if (successfulDrop) {
ShortcutInfo info = (ShortcutInfo) d.dragInfo;
Intent intent = info.getIntent();
if (intent != null && mInfo.hidden) {
removeProtectedApp(intent.getComponent());
}
}
}
}
mDeleteFolderOnDropCompleted = false;
mDragInProgress = false;
mItemAddedBackToSelfViaIcon = false;
mCurrentDragInfo = null;
mCurrentDragView = null;
mSuppressOnAdd = false;
// Reordering may have occured, and we need to save the new item locations. We do this once
// at the end to prevent unnecessary database operations.
updateItemLocationsInDatabaseBatch();
// Use the item count to check for multi-page as the folder UI may not have
// been refreshed yet.
if (getItemCount() <= mContent.itemsPerPage()) {
// Show the animation, next time something is added to the folder.
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher);
}
}
@Override
public void deferCompleteDropAfterUninstallActivity() {
mDeferDropAfterUninstall = true;
}
@Override
public void onUninstallActivityReturned(boolean success) {
mDeferDropAfterUninstall = false;
mUninstallSuccessful = success;
if (mDeferredAction != null) {
mDeferredAction.run();
}
}
@Override
public float getIntrinsicIconScaleFactor() {
return 1f;
}
@Override
public boolean supportsFlingToDelete() {
return true;
}
@Override
public boolean supportsAppInfoDropTarget() {
return false;
}
@Override
public boolean supportsDeleteDropTarget() {
return true;
}
@Override
public void onFlingToDelete(DragObject d, PointF vec) {
// Do nothing
}
@Override
public void onFlingToDeleteCompleted() {
// Do nothing
}
/**
* @return true if contents should persist their status to the database.
*/
protected boolean shouldUpdateContentsInDatabase() {
return true;
}
private void updateItemLocationsInDatabaseBatch() {
if (!shouldUpdateContentsInDatabase()) return;
ArrayList<View> list = getItemsInReadingOrder();
ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
for (int i = 0; i < list.size(); i++) {
View v = list.get(i);
ItemInfo info = (ItemInfo) v.getTag();
info.rank = i;
items.add(info);
}
LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0);
}
public void addItemLocationsInDatabase() {
if (!shouldUpdateContentsInDatabase()) return;
ArrayList<View> list = getItemsInReadingOrder();
for (int i = 0; i < list.size(); i++) {
View v = list.get(i);
ItemInfo info = (ItemInfo) v.getTag();
LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
info.cellX, info.cellY);
}
}
public void notifyDrop() {
if (mDragInProgress) {
mItemAddedBackToSelfViaIcon = true;
}
}
public boolean isDropEnabled() {
return true;
}
public boolean isFull() {
return mContent.isFull();
}
protected void calculatePivot() {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
// If we haven't measured ourselves yet, force one now to determine our dimensions.
if (width <= 0 && height <= 0) {
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
width = getMeasuredWidth();
height = getMeasuredHeight();
}
calculatePivot(width, height);
}
private void calculatePivot(int width, int height) {
DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect);
DeviceProfile grid = mLauncher.getDeviceProfile();
int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2);
int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2);
int centeredLeft = centerX - width / 2;
int centeredTop = centerY - height / 2;
int currentPage = mLauncher.getWorkspace().getNextPage();
// We first fetch the currently visible CellLayoutChildren
CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage);
ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets();
Rect bounds = new Rect();
parent.getDescendantRectRelativeToSelf(boundingLayout, bounds);
// Center the folder
int left = (grid.availableWidthPx - width) / 2;
// Drop the top down a little so it isn't bounded by the page indicators
int top = (int) (bounds.top + (bounds.height() * 1.15) - height);
if (width >= bounds.width()) {
// If the folder doesn't fit within the bounds, center it about the desired bounds
left = bounds.left + (bounds.width() - width) / 2;
}
int folderPivotX = width / 2 + (centeredLeft - left);
int folderPivotY = height / 2 + (centeredTop - top);
setPivotX(folderPivotX);
setPivotY(folderPivotY);
mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
(1.0f * folderPivotX / width));
mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
(1.0f * folderPivotY / height));
}
float getPivotXForIconAnimation() {
return mFolderIconPivotX;
}
float getPivotYForIconAnimation() {
return mFolderIconPivotY;
}
protected int getContentAreaHeight() {
return Math.max(mContent.getDesiredHeight(), MIN_CONTENT_DIMEN);
}
protected int getContentAreaWidth() {
return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight());
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* Rearranges the children based on their rank.
*/
public void rearrangeChildren() {
rearrangeChildren(-1);
}
/**
* Rearranges the children based on their rank.
* @param itemCount if greater than the total children count, empty spaces are left at the end,
* otherwise it is ignored.
*/
public void rearrangeChildren(int itemCount) {
ArrayList<View> views = getItemsInReadingOrder();
mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
mItemsInvalidated = true;
}
// TODO remove this once GSA code fix is submitted
public ViewGroup getContent() {
return (ViewGroup) mContent;
}
public int getItemCount() {
return mContent.getItemCount();
}
@Thunk void onCloseComplete() {
DragLayer parent = (DragLayer) getParent();
if (parent != null) {
parent.removeView(this);
}
mDragController.removeDropTarget((DropTarget) this);
clearFocus();
mFolderIcon.requestFocus();
if (mRearrangeOnClose) {
rearrangeChildren();
mRearrangeOnClose = false;
}
if (getItemCount() <= 1) {
if (!mDragInProgress && !mSuppressFolderDeletion) {
replaceFolderWithFinalItem();
} else if (mDragInProgress) {
mDeleteFolderOnDropCompleted = true;
}
}
mSuppressFolderDeletion = false;
clearDragInfo();
}
@Thunk void replaceFolderWithFinalItem() {
// Add the last remaining child to the workspace in place of the folder
Runnable onCompleteRunnable = new Runnable() {
@Override
public void run() {
CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId);
View child = null;
// Move the item from the folder to the workspace, in the position of the folder
if (!mInfo.hidden && getItemCount() == 1) {
ShortcutInfo finalItem = mInfo.contents.get(0);
child = mLauncher.createShortcut(cellLayout, finalItem);
LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
mInfo.screenId, mInfo.cellX, mInfo.cellY);
}
if (getItemCount() <= 1) {
// Remove the folder
LauncherModel.deleteItemFromDatabase(mLauncher, mInfo);
if (cellLayout != null) {
// b/12446428 -- sometimes the cell layout has already gone away?
cellLayout.removeView(mFolderIcon);
}
if (mFolderIcon instanceof DropTarget) {
mDragController.removeDropTarget((DropTarget) mFolderIcon);
}
mLauncher.removeFolder(mInfo);
}
// We add the child after removing the folder to prevent both from existing at
// the same time in the CellLayout. We need to add the new item with addInScreenFromBind()
// to ensure that hotseat items are placed correctly.
if (child != null) {
mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId,
mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
}
}
};
View finalChild = mContent.getLastItem();
if (finalChild != null) {
mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
} else {
onCompleteRunnable.run();
}
mDestroyed = true;
}
boolean isDestroyed() {
return mDestroyed;
}
// This method keeps track of the last item in the folder for the purposes
// of keyboard focus
public void updateTextViewFocus() {
View lastChild = mContent.getLastItem();
if (lastChild != null) {
mFolderName.setNextFocusDownId(lastChild.getId());
mFolderName.setNextFocusRightId(lastChild.getId());
mFolderName.setNextFocusLeftId(lastChild.getId());
mFolderName.setNextFocusUpId(lastChild.getId());
}
}
public void onDrop(DragObject d) {
Runnable cleanUpRunnable = null;
// If we are coming from All Apps space, we defer removing the extra empty screen
// until the folder closes
if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) {
cleanUpRunnable = new Runnable() {
@Override
public void run() {
mLauncher.exitSpringLoadedDragModeDelayed(true,
Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT,
null);
}
};
}
// If the icon was dropped while the page was being scrolled, we need to compute
// the target location again such that the icon is placed of the final page.
if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
// Reorder again.
mTargetRank = getTargetRank(d, null);
// Rearrange items immediately.
mReorderAlarmListener.onAlarm(mReorderAlarm);
mOnScrollHintAlarm.cancelAlarm();
mScrollPauseAlarm.cancelAlarm();
}
mContent.completePendingPageChanges();
View currentDragView;
ShortcutInfo si = mCurrentDragInfo;
if (mIsExternalDrag) {
currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
// Actually move the item in the database if it was an external drag. Call this
// before creating the view, so that ShortcutInfo is updated appropriately.
if (shouldUpdateContentsInDatabase()) {
LauncherModel.addOrMoveItemInDatabase(
mLauncher, si, mInfo.id, 0, si.cellX, si.cellY);
}
// We only need to update the locations if it doesn't get handled in #onDropCompleted.
if (d.dragSource != this) {
updateItemLocationsInDatabaseBatch();
}
} else {
currentDragView = mCurrentDragView;
mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
}
if (d.dragView.hasDrawn()) {
// Temporarily reset the scale such that the animation target gets calculated correctly.
float scaleX = getScaleX();
float scaleY = getScaleY();
setScaleX(1.0f);
setScaleY(1.0f);
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView,
cleanUpRunnable, null);
setScaleX(scaleX);
setScaleY(scaleY);
} else {
d.deferDragViewCleanupPostAnimation = false;
currentDragView.setVisibility(VISIBLE);
}
mItemsInvalidated = true;
rearrangeChildren();
// Temporarily suppress the listener, as we did all the work already here.
mSuppressOnAdd = true;
mInfo.add(si);
mSuppressOnAdd = false;
// Clear the drag info, as it is no longer being dragged.
mCurrentDragInfo = null;
mDragInProgress = false;
if (mContent.getPageCount() > 1) {
// The animation has already been shown while opening the folder.
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
}
if (mIsExternalDrag) {
// Delay the close animation to reduce jank
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mLauncher.closeFolder(Folder.this);
}
}, 100);
mIsExternalDrag = false;
}
}
// This is used so the item doesn't immediately appear in the folder when added. In one case
// we need to create the illusion that the item isn't added back to the folder yet, to
// to correspond to the animation of the icon back into the folder. This is
public void hideItem(ShortcutInfo info) {
View v = getViewForInfo(info);
v.setVisibility(INVISIBLE);
}
public void showItem(ShortcutInfo info) {
View v = getViewForInfo(info);
v.setVisibility(VISIBLE);
}
@Override
public void onAdd(ShortcutInfo item) {
// If the item was dropped onto this open folder, we have done the work associated
// with adding the item to the folder, as indicated by mSuppressOnAdd being set
if (mSuppressOnAdd) return;
mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item));
mItemsInvalidated = true;
if (shouldUpdateContentsInDatabase()) {
LauncherModel.addOrMoveItemInDatabase(
mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
}
}
public void onRemove(ShortcutInfo item) {
mItemsInvalidated = true;
// If this item is being dragged from this open folder, we have already handled
// the work associated with removing the item, so we don't have to do anything here.
if (item == mCurrentDragInfo) return;
mContent.removeItem(getViewForInfo(item));
if (mState == STATE_ANIMATING) {
mRearrangeOnClose = true;
} else {
rearrangeChildren();
}
if (getItemCount() <= 1) {
replaceFolderWithFinalItem();
}
}
@Override
public void onRemoveAll() {
// Clear the UX after folder contents are removed from the DB
removeViewsForItems(null);
mLauncher.closeFolder(this);
replaceFolderWithFinalItem();
}
@Override
public void onRemoveAll(ArrayList<ShortcutInfo> items) {
removeViewsForItems(items);
if (mState == STATE_ANIMATING) {
mRearrangeOnClose = true;
} else {
rearrangeChildren();
}
if (mInfo.contents.isEmpty()) {
mLauncher.closeFolder(this);
}
replaceFolderWithFinalItem();
}
/**
* Remove all the supplied item views from this folder.
* @param items info of views to remove, or null if all views should be removed.
*/
protected void removeViewsForItems(ArrayList<ShortcutInfo> items) {
mItemsInvalidated = true;
if (items == null) {
mContent.removeAllItems();
} else {
for (ShortcutInfo item : items) {
mContent.removeItem(getViewForInfo(item));
}
}
}
/**
* Update the view tied to this shortcut.
* @param info updated info to be applied to view.
* @return true if view for info was found, false otherwise.
*/
@SuppressWarnings("unused")
public boolean updateViewForInfo(final ShortcutInfo info) {
View v = getViewForInfo(info);
if (v != null & v instanceof BubbleTextView) {
((BubbleTextView) v).reapplyItemInfo(info);
mItemsInvalidated = true;
return true;
}
return false;
}
public View getViewForInfo(ShortcutInfo item) {
View v = mContent.getChildAtRank(item.rank);
if (v != null && v.getTag() == item) {
return v;
}
return null;
}
public void onItemsChanged() {
updateTextViewFocus();
}
public void onTitleChanged(CharSequence title) {
}
public ArrayList<View> getItemsInReadingOrder() {
if (mItemsInvalidated) {
mItemsInReadingOrder.clear();
mContent.iterateOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view, View parent) {
mItemsInReadingOrder.add(view);
return false;
}
});
mItemsInvalidated = false;
}
return mItemsInReadingOrder;
}
public void getLocationInDragLayer(int[] loc) {
mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
}
public void onFocusChange(View v, boolean hasFocus) {
if (v == mFolderName && hasFocus) {
startEditingFolderName();
}
}
@Override
public void getHitRectRelativeToDragLayer(Rect outRect) {
getHitRect(outRect);
outRect.left -= mScrollAreaOffset;
outRect.right += mScrollAreaOffset;
}
public View getViewFromPosition(int position) {
return mItemsInReadingOrder.get(position);
}
@Override
public void fillInLaunchSourceData(Bundle sourceData) {
// Fill in from the folder icon's launch source provider first
Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData);
sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER);
sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage());
}
private class OnScrollHintListener implements OnAlarmListener {
private final DragObject mDragObject;
OnScrollHintListener(DragObject object) {
mDragObject = object;
}
/**
* Scroll hint has been shown long enough. Now scroll to appropriate page.
*/
@Override
public void onAlarm(Alarm alarm) {
if (mCurrentScrollDir == DragController.SCROLL_LEFT) {
mContent.scrollLeft();
mScrollHintDir = DragController.SCROLL_NONE;
} else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) {
mContent.scrollRight();
mScrollHintDir = DragController.SCROLL_NONE;
} else {
// This should not happen
return;
}
mCurrentScrollDir = DragController.SCROLL_NONE;
// Pause drag event until the scrolling is finished
mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject));
mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY);
}
}
private class OnScrollFinishedListener implements OnAlarmListener {
private final DragObject mDragObject;
OnScrollFinishedListener(DragObject object) {
mDragObject = object;
}
/**
* Page scroll is complete.
*/
@Override
public void onAlarm(Alarm alarm) {
// Reorder immediately on page change.
onDragOver(mDragObject, 1);
}
}
// Compares item position based on rank and position giving priority to the rank.
public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() {
@Override
public int compare(ItemInfo lhs, ItemInfo rhs) {
if (lhs.rank != rhs.rank) {
return lhs.rank - rhs.rank;
} else if (lhs.cellY != rhs.cellY) {
return lhs.cellY - rhs.cellY;
} else {
return lhs.cellX - rhs.cellX;
}
}
};
}